mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-21 16:56:47 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue5729
This commit is contained in:
@@ -35,12 +35,12 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
|
||||
# Python installs
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
if: ${{ inputs.python == 'true' }}
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: pip
|
||||
@@ -58,7 +58,7 @@ runs:
|
||||
# NPM installs
|
||||
- name: Install node.js ${{ env.node_version }}
|
||||
if: ${{ inputs.npm == 'true' }}
|
||||
uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # pin to v3.5.0
|
||||
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # pin to v3.8.2
|
||||
with:
|
||||
node-version: ${{ env.node_version }}
|
||||
cache: 'npm'
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
)
|
||||
steps:
|
||||
- name: Backport Action
|
||||
uses: sqren/backport-github-action@v8.9.3
|
||||
uses: sqren/backport-github-action@f54e19901f2a57f8b82360f2490d47ee82ec82c6 # pin@v9.2.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Set Up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
|
||||
@@ -20,7 +20,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - 'master'
|
||||
@@ -39,9 +38,9 @@ jobs:
|
||||
python_version: 3.9
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Set Up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Version Check
|
||||
@@ -82,23 +81,23 @@ jobs:
|
||||
docker-compose down
|
||||
- name: Set up QEMU
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # pin@v2.1.0
|
||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # pin@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@95cb08cb2672c73d4ffd2f422e6d11953d2a9c70 # pin@v2.1.0
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # pin@v3.0.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@7cc35d7fdbe70d4278a0c96779081e6fac665f88 # pin@v2.8.0
|
||||
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # pin@v3.1.2
|
||||
- name: Login to Dockerhub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # pin@v2.1.0
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.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@f4ef78c080cd8ba55a85445d5b36e214a81df20a # pin@v2
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -107,7 +106,7 @@ jobs:
|
||||
- name: Extract Docker metadata
|
||||
if: github.event_name != 'pull_request'
|
||||
id: meta
|
||||
uses: docker/metadata-action@12cce9efe0d49980455aaaca9b071c0befcdd702 # pin@v4.1.0
|
||||
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # pin@v5.0.0
|
||||
with:
|
||||
images: |
|
||||
inventree/inventree
|
||||
@@ -116,7 +115,7 @@ jobs:
|
||||
- name: Build and Push
|
||||
id: build-and-push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 # pin@v3.2.0
|
||||
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # pin@v5.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -133,5 +132,4 @@ jobs:
|
||||
if: ${{ false }} # github.event_name != 'pull_request'
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: "true"
|
||||
run: cosign sign ${{ steps.meta.outputs.tags }}@${{
|
||||
steps.build-and-push.outputs.digest }}
|
||||
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
|
||||
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # pin@v2.11.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
if: needs.paths-filter.outputs.server == 'true'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
needs: [ 'pep_style', 'pre-commit' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
@@ -113,9 +113,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Check Config
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -154,8 +154,7 @@ jobs:
|
||||
update: true
|
||||
npm: true
|
||||
- name: Download Python Code For `${{ env.wrapper_name }}`
|
||||
run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }}
|
||||
./${{ env.wrapper_name }}
|
||||
run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
||||
- name: Start InvenTree Server
|
||||
run: |
|
||||
invoke delete-data -f
|
||||
@@ -176,7 +175,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -198,7 +197,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -214,7 +213,7 @@ jobs:
|
||||
- name: Coverage Tests
|
||||
run: invoke test --coverage
|
||||
- name: Upload Coverage Report
|
||||
uses: coverallsapp/github-action@v2
|
||||
uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -248,7 +247,7 @@ jobs:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -287,13 +286,12 @@ jobs:
|
||||
MYSQL_USER: inventree
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s
|
||||
--health-retries=3
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -332,7 +330,7 @@ jobs:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -356,7 +354,7 @@ jobs:
|
||||
INVENTREE_PLUGINS_ENABLED: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
name: Checkout Code
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -405,7 +403,7 @@ jobs:
|
||||
INVENTREE_PLUGINS_ENABLED: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -420,7 +418,7 @@ jobs:
|
||||
run: cd src/frontend && npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: cd src/frontend && npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
@@ -433,7 +431,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -446,7 +444,7 @@ jobs:
|
||||
run: |
|
||||
cd InvenTree/web/static
|
||||
zip -r frontend-build.zip web/
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3
|
||||
with:
|
||||
name: frontend-build
|
||||
path: InvenTree/web/static/web
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Version Check
|
||||
run: |
|
||||
pip install requests
|
||||
python3 ci/version_check.py
|
||||
- name: Push to Stable Branch
|
||||
uses: ad-m/github-push-action@4dcce6dea3e3c8187237fc86b7dfdc93e5aaae58 # pin@master
|
||||
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
|
||||
if: env.stable_release == 'true'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
publish-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: |
|
||||
cd InvenTree/web/static/web
|
||||
zip -r ../frontend-build.zip *
|
||||
- uses: svenstaro/upload-release-action@v2
|
||||
- uses: svenstaro/upload-release-action@1beeb572c19a9242f4361f4cee78f8e0d9aec5df # pin@2.7.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: InvenTree/web/static/frontend-build.zip
|
||||
|
||||
@@ -14,11 +14,10 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # pin@v6.0.1
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # pin@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue seems stale. Please react to show this is still
|
||||
important.'
|
||||
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
|
||||
stale-pr-message: 'This PR seems stale. Please react to show this is still important.'
|
||||
stale-issue-label: 'inactive'
|
||||
stale-pr-label: 'inactive'
|
||||
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0
|
||||
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # pin to v3.5.0
|
||||
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # pin to v3.8.2
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install Dependencies
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
git add "*.po"
|
||||
git commit -m "updated translation base"
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@4dcce6dea3e3c8187237fc86b7dfdc93e5aaae58 # pin@master
|
||||
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: l10
|
||||
|
||||
@@ -9,14 +9,13 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
- name: Setup
|
||||
run: pip install -r requirements-dev.txt
|
||||
- name: Update requirements.txt
|
||||
run: pip-compile --output-file=requirements.txt requirements.in -U
|
||||
- name: Update requirements-dev.txt
|
||||
run: pip-compile --generate-hashes --output-file=requirements-dev.txt
|
||||
requirements-dev.in -U
|
||||
run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U
|
||||
- uses: stefanzweifel/git-auto-commit-action@fd157da78fa13d9383e5580d1fd1184d89554b51 # pin@v4.15.1
|
||||
with:
|
||||
commit_message: "[Bot] Updated dependency"
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 140
|
||||
INVENTREE_API_VERSION = 141
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v141 -> 2023-10-23 : https://github.com/inventree/InvenTree/pull/5774
|
||||
- Changed 'part.responsible' from User to Owner
|
||||
|
||||
v140 -> 2023-10-20 : https://github.com/inventree/InvenTree/pull/5664
|
||||
- Expand API token functionality
|
||||
- Multiple API tokens can be generated per user
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@@ -341,3 +342,58 @@ def get_custom_file(env_ref: str, conf_ref: str, log_ref: str, lookup_media: boo
|
||||
value = False
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def get_frontend_settings(debug=True):
|
||||
"""Return a dictionary of settings for the frontend interface.
|
||||
|
||||
Note that the new config settings use the 'FRONTEND' key,
|
||||
whereas the legacy key was 'PUI' (platform UI) which is now deprecated
|
||||
"""
|
||||
|
||||
# Legacy settings
|
||||
pui_settings = get_setting('INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict)
|
||||
|
||||
if len(pui_settings) > 0:
|
||||
warnings.warn(
|
||||
"The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
|
||||
# New settings
|
||||
frontend_settings = get_setting('INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict)
|
||||
|
||||
# Merge settings
|
||||
settings = {**pui_settings, **frontend_settings}
|
||||
|
||||
# Set the base URL
|
||||
if 'base_url' not in settings:
|
||||
base_url = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', '')
|
||||
|
||||
if base_url:
|
||||
warnings.warn(
|
||||
"The 'INVENTREE_PUI_URL_BASE' key is deprecated. Please use 'INVENTREE_FRONTEND_URL_BASE' instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
else:
|
||||
base_url = get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform')
|
||||
|
||||
settings['base_url'] = base_url
|
||||
|
||||
# Set the server list
|
||||
settings['server_list'] = settings.get('server_list', [])
|
||||
|
||||
# Set the debug flag
|
||||
settings['debug'] = debug
|
||||
|
||||
if 'environment' not in settings:
|
||||
settings['environment'] = 'development' if debug else 'production'
|
||||
|
||||
if debug and 'show_server_selector' not in settings:
|
||||
# In debug mode, show server selector by default
|
||||
settings['show_server_selector'] = True
|
||||
elif len(settings['server_list']) == 0:
|
||||
# If no servers are specified, show server selector
|
||||
settings['show_server_selector'] = True
|
||||
|
||||
return settings
|
||||
|
||||
@@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object):
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif request.path_info.startswith(f'/{settings.PUI_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.PUI_URL_BASE}':
|
||||
elif request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.FRONTEND_URL_BASE}':
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||
|
||||
@@ -1044,9 +1044,9 @@ CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', '
|
||||
|
||||
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
|
||||
|
||||
# Frontend settings
|
||||
PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform')
|
||||
PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {})
|
||||
# Load settings for the frontend interface
|
||||
FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
|
||||
FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
|
||||
|
||||
if DEBUG:
|
||||
logger.info("InvenTree running with DEBUG enabled")
|
||||
@@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS:
|
||||
|
||||
# Magic login django-sesame
|
||||
SESAME_MAX_AGE = 300
|
||||
# LOGIN_REDIRECT_URL = f"/{PUI_URL_BASE}/logged-in/"
|
||||
# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
|
||||
LOGIN_REDIRECT_URL = "/index/"
|
||||
|
||||
@@ -96,7 +96,7 @@ for provider in providers.registry.get_list():
|
||||
social_auth_urlpatterns += provider_urlpatterns
|
||||
|
||||
|
||||
class SocialProvierListView(ListAPIView):
|
||||
class SocialProviderListView(ListAPIView):
|
||||
"""List of available social providers."""
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
|
||||
@@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase):
|
||||
self.assertEqual(resp.url, '/index/')
|
||||
# Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet
|
||||
# TODO: In the future, the URL comparison will need to be reverted
|
||||
# self.assertEqual(resp.url, f'/{settings.PUI_URL_BASE}/logged-in/')
|
||||
# self.assertEqual(resp.url, f'/{settings.FRONTEND_URL_BASE}/logged-in/')
|
||||
# And we should be logged in again
|
||||
self.assertEqual(resp.wsgi_request.user, self.user)
|
||||
|
||||
+25
-25
@@ -16,29 +16,29 @@ from dj_rest_auth.registration.views import (ConfirmEmailView,
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
||||
from sesame.views import LoginView
|
||||
|
||||
from build.api import build_api_urls
|
||||
import build.api
|
||||
import common.api
|
||||
import company.api
|
||||
import label.api
|
||||
import order.api
|
||||
import part.api
|
||||
import plugin.api
|
||||
import report.api
|
||||
import stock.api
|
||||
import users.api
|
||||
from build.urls import build_urls
|
||||
from common.api import admin_api_urls, common_api_urls, settings_api_urls
|
||||
from common.urls import common_urls
|
||||
from company.api import company_api_urls
|
||||
from company.urls import (company_urls, manufacturer_part_urls,
|
||||
supplier_part_urls)
|
||||
from label.api import label_api_urls
|
||||
from order.api import order_api_urls
|
||||
from order.urls import order_urls
|
||||
from part.api import bom_api_urls, part_api_urls
|
||||
from part.urls import part_urls
|
||||
from plugin.api import plugin_api_urls
|
||||
from plugin.urls import get_plugin_urls
|
||||
from report.api import report_api_urls
|
||||
from stock.api import stock_api_urls
|
||||
from stock.urls import stock_urls
|
||||
from users.api import user_urls
|
||||
from web.urls import urlpatterns as platform_urls
|
||||
|
||||
from .api import APISearchView, InfoView, NotFoundView
|
||||
from .magic_login import GetSimpleLoginView
|
||||
from .social_auth_urls import SocialProvierListView, social_auth_urlpatterns
|
||||
from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns
|
||||
from .views import (AboutView, AppearanceSelectView, CustomConnectionsView,
|
||||
CustomEmailView, CustomLoginView,
|
||||
CustomPasswordResetFromKeyView,
|
||||
@@ -55,23 +55,23 @@ apipatterns = [
|
||||
# Global search
|
||||
path('search/', APISearchView.as_view(), name='api-search'),
|
||||
|
||||
re_path(r'^settings/', include(settings_api_urls)),
|
||||
re_path(r'^part/', include(part_api_urls)),
|
||||
re_path(r'^bom/', include(bom_api_urls)),
|
||||
re_path(r'^company/', include(company_api_urls)),
|
||||
re_path(r'^stock/', include(stock_api_urls)),
|
||||
re_path(r'^build/', include(build_api_urls)),
|
||||
re_path(r'^order/', include(order_api_urls)),
|
||||
re_path(r'^label/', include(label_api_urls)),
|
||||
re_path(r'^report/', include(report_api_urls)),
|
||||
re_path(r'^user/', include(user_urls)),
|
||||
re_path(r'^admin/', include(admin_api_urls)),
|
||||
re_path(r'^settings/', include(common.api.settings_api_urls)),
|
||||
re_path(r'^part/', include(part.api.part_api_urls)),
|
||||
re_path(r'^bom/', include(part.api.bom_api_urls)),
|
||||
re_path(r'^company/', include(company.api.company_api_urls)),
|
||||
re_path(r'^stock/', include(stock.api.stock_api_urls)),
|
||||
re_path(r'^build/', include(build.api.build_api_urls)),
|
||||
re_path(r'^order/', include(order.api.order_api_urls)),
|
||||
re_path(r'^label/', include(label.api.label_api_urls)),
|
||||
re_path(r'^report/', include(report.api.report_api_urls)),
|
||||
re_path(r'^user/', include(users.api.user_urls)),
|
||||
re_path(r'^admin/', include(common.api.admin_api_urls)),
|
||||
|
||||
# Plugin endpoints
|
||||
path('', include(plugin_api_urls)),
|
||||
path('', include(plugin.api.plugin_api_urls)),
|
||||
|
||||
# Common endpoints endpoint
|
||||
path('', include(common_api_urls)),
|
||||
path('', include(common.api.common_api_urls)),
|
||||
|
||||
# OpenAPI Schema
|
||||
re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'),
|
||||
@@ -83,7 +83,7 @@ apipatterns = [
|
||||
path('auth/', include([
|
||||
re_path(r'^registration/account-confirm-email/(?P<key>[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'),
|
||||
path('registration/', include('dj_rest_auth.registration.urls')),
|
||||
path('providers/', SocialProvierListView.as_view(), name='social_providers'),
|
||||
path('providers/', SocialProviderListView.as_view(), name='social_providers'),
|
||||
path('social/', include(social_auth_urlpatterns)),
|
||||
path('social/', SocialAccountListView.as_view(), name='social_account_list'),
|
||||
path('social/<int:pk>/disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'),
|
||||
|
||||
@@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER
|
||||
# logo: img/custom_logo.png
|
||||
# splash: img/custom_splash.jpg
|
||||
|
||||
# Platform UI options
|
||||
# pui_settings:
|
||||
# Frontend UI settings
|
||||
# frontend_settings:
|
||||
# base_url: 'frontend'
|
||||
# server_list:
|
||||
# my_server1:
|
||||
# host: https://demo.inventree.org/api/
|
||||
# host: https://demo.inventree.org/
|
||||
# name: InvenTree Demo
|
||||
# default_server: my_server1
|
||||
# show_server_selector: false
|
||||
# sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
|
||||
# environment: development
|
||||
# Base URL for serving Platform UI
|
||||
# pui_url_base: 'platform'
|
||||
|
||||
# Custom flags
|
||||
# InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.22 on 2023-10-23 01:59
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_auto_20231020_2356'),
|
||||
('part', '0114_alter_part_minimum_stock'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='part',
|
||||
name='responsible_owner',
|
||||
field=models.ForeignKey(blank=True, help_text='Owner responsible for this part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parts_responsible', to='users.owner', verbose_name='Responsible'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generated by Django 3.2.22 on 2023-10-23 03:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_part_responsible_owner(apps, schema_editor):
|
||||
"""Copy existing part.responsible field to part.responsible_owner"""
|
||||
|
||||
Owner = apps.get_model('users', 'Owner')
|
||||
Part = apps.get_model('part', 'Part')
|
||||
User = apps.get_model('auth', 'user')
|
||||
ContentType = apps.get_model('contenttypes', 'contenttype')
|
||||
|
||||
user_type = ContentType.objects.get_for_model(User)
|
||||
|
||||
parts = Part.objects.exclude(responsible=None)
|
||||
|
||||
for part in parts:
|
||||
|
||||
# Find a corresponding Owner object, or create one if it does not exist
|
||||
owner, _created = Owner.objects.get_or_create(
|
||||
owner_type=user_type,
|
||||
owner_id=part.responsible.id,
|
||||
)
|
||||
|
||||
part.responsible_owner = owner
|
||||
part.save()
|
||||
|
||||
if parts.count() > 0:
|
||||
print(f"Added 'responsible_owner' for {parts.count()} parts")
|
||||
|
||||
|
||||
def reverse_owner_migration(apps, schema_editor):
|
||||
"""Reverse the owner migration:
|
||||
|
||||
- Set the 'responsible' field to a selected user
|
||||
- Only where 'responsible_owner' is set
|
||||
- Only where 'responsible_owner' is a User object
|
||||
"""
|
||||
|
||||
Part = apps.get_model('part', 'Part')
|
||||
User = apps.get_model('auth', 'user')
|
||||
ContentType = apps.get_model('contenttypes', 'contenttype')
|
||||
|
||||
user_type = ContentType.objects.get_for_model(User)
|
||||
|
||||
parts = Part.objects.exclude(responsible_owner=None)
|
||||
|
||||
for part in parts:
|
||||
|
||||
if part.responsible_owner.owner_type == user_type:
|
||||
|
||||
# Attempt to find matching user
|
||||
try:
|
||||
user = User.objects.get(pk=part.responsible_owner.owner_id)
|
||||
part.responsible = user
|
||||
part.save()
|
||||
except User.DoesNotExist:
|
||||
print("User does not exist:", part.responsible_owner.owner_id)
|
||||
|
||||
if parts.count() > 0:
|
||||
print(f"Added 'responsible' for {parts.count()} parts")
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0115_part_responsible_owner'),
|
||||
('users', '0005_owner_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_part_responsible_owner,
|
||||
reverse_code=reverse_owner_migration,
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.22 on 2023-10-23 05:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0116_auto_20231023_0332'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='part',
|
||||
name='responsible',
|
||||
),
|
||||
]
|
||||
@@ -41,6 +41,7 @@ import InvenTree.fields
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import part.settings as part_settings
|
||||
import users.models
|
||||
from build import models as BuildModels
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import currency_code_default
|
||||
@@ -379,7 +380,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
notes: Additional notes field for this part
|
||||
creation_date: Date that this part was added to the database
|
||||
creation_user: User who added this part to the database
|
||||
responsible: User who is responsible for this part (optional)
|
||||
responsible_owner: Owner (either user or group) which is responsible for this part (optional)
|
||||
last_stocktake: Date at which last stocktake was performed for this Part
|
||||
"""
|
||||
|
||||
@@ -1036,7 +1037,13 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
||||
|
||||
creation_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Creation User'), related_name='parts_created')
|
||||
|
||||
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), help_text=_('User responsible for this part'), related_name='parts_responible')
|
||||
responsible_owner = models.ForeignKey(
|
||||
users.models.Owner, on_delete=models.SET_NULL,
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Responsible'),
|
||||
help_text=_('Owner responsible for this part'),
|
||||
related_name='parts_responsible'
|
||||
)
|
||||
|
||||
last_stocktake = models.DateField(
|
||||
blank=True, null=True,
|
||||
|
||||
@@ -26,6 +26,7 @@ import part.filters
|
||||
import part.stocktake
|
||||
import part.tasks
|
||||
import stock.models
|
||||
import users.models
|
||||
from InvenTree.status_codes import BuildStatusGroups
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
@@ -695,6 +696,12 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
responsible = serializers.PrimaryKeyRelatedField(
|
||||
queryset=users.models.Owner.objects.all(),
|
||||
required=False, allow_null=True,
|
||||
source='responsible_owner',
|
||||
)
|
||||
|
||||
# Annotated fields
|
||||
allocated_to_build_orders = serializers.FloatField(read_only=True)
|
||||
allocated_to_sales_orders = serializers.FloatField(read_only=True)
|
||||
|
||||
@@ -384,11 +384,11 @@
|
||||
<td>{% include 'clip_link.html' with link=part.link new_window=True %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.responsible %}
|
||||
{% if part.responsible_owner %}
|
||||
<tr>
|
||||
<td><span class='fas fa-user'></span></td>
|
||||
<td>{% trans "Responsible" %}</td>
|
||||
<td> <span class='badge badge-right rounded-pill bg-dark'>{{ part.responsible }}</span></td>
|
||||
<td> <span class='badge badge-right rounded-pill bg-dark'>{{ part.responsible_owner }}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
@@ -16,10 +16,10 @@ from django.utils.translation import gettext_lazy as _
|
||||
import common.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import plugin.models
|
||||
from common.settings import currency_code_default
|
||||
from InvenTree import settings, version
|
||||
from plugin import registry
|
||||
from plugin.models import NotificationUserSetting, PluginSetting
|
||||
from plugin.plugin import InvenTreePlugin
|
||||
|
||||
register = template.Library()
|
||||
@@ -346,14 +346,14 @@ def setting_object(key, *args, **kwargs):
|
||||
if 'plugin' in kwargs:
|
||||
# Note, 'plugin' is an instance of an InvenTreePlugin class
|
||||
|
||||
plugin = kwargs['plugin']
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
plg = kwargs['plugin']
|
||||
if issubclass(plg.__class__, InvenTreePlugin):
|
||||
plg = plg.plugin_config()
|
||||
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin, cache=cache)
|
||||
return plugin.models.PluginSetting.get_setting_object(key, plugin=plg, cache=cache)
|
||||
|
||||
elif 'method' in kwargs:
|
||||
return NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'], cache=cache)
|
||||
return plugin.models.NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'], cache=cache)
|
||||
|
||||
elif 'user' in kwargs:
|
||||
return common.models.InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'], cache=cache)
|
||||
|
||||
@@ -649,7 +649,11 @@ class PluginsRegistry:
|
||||
try:
|
||||
logger.debug("Updating plugin registry hash: %s", str(self.registry_hash))
|
||||
InvenTreeSetting.set_setting("_PLUGIN_REGISTRY_HASH", self.registry_hash, change_user=None)
|
||||
except (OperationalError, ProgrammingError):
|
||||
# Exception if the database has not been migrated yet, or is not ready
|
||||
pass
|
||||
except Exception as exc:
|
||||
# Some other exception, we want to know about it
|
||||
logger.exception("Failed to update plugin registry hash: %s", str(exc))
|
||||
|
||||
def calculate_plugin_hash(self):
|
||||
|
||||
@@ -1673,7 +1673,11 @@ function loadPurchaseOrderTable(table, options) {
|
||||
sortable: true,
|
||||
sortName: 'supplier__name',
|
||||
formatter: function(value, row) {
|
||||
if (row.supplier_detail) {
|
||||
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/?display=purchase-orders`);
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1986,7 +1990,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
title: '{% trans "Part" %}',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
if (row.part) {
|
||||
if (row.part_detail) {
|
||||
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
|
||||
} else {
|
||||
return '-';
|
||||
|
||||
@@ -1793,7 +1793,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
title: '{% trans "Part" %}',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
if (row.part) {
|
||||
if (row.part_detail) {
|
||||
return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`);
|
||||
} else {
|
||||
return '-';
|
||||
|
||||
@@ -3062,8 +3062,10 @@ function loadInstalledInTable(table, options) {
|
||||
formatter: function(value, row) {
|
||||
var html = '';
|
||||
|
||||
if (row.part_detail) {
|
||||
html += imageHoverIcon(row.part_detail.thumbnail);
|
||||
html += renderLink(row.part_detail.full_name, `/stock/item/${row.pk}/`);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
+12
-11
@@ -6,7 +6,6 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import ApiToken, Owner, RuleSet
|
||||
@@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
|
||||
users = form.cleaned_data['users']
|
||||
|
||||
# Check for users who are members of multiple groups
|
||||
warning_message = ''
|
||||
multiple_group_users = []
|
||||
|
||||
for user in users:
|
||||
if user.groups.all().count() > 1:
|
||||
warning_message += f'<br>- <b>{user.username}</b> is member of: '
|
||||
for idx, group in enumerate(user.groups.all()):
|
||||
warning_message += f'<b>{group.name}</b>'
|
||||
if idx < len(user.groups.all()) - 1:
|
||||
warning_message += ', '
|
||||
multiple_group_users.append(user.username)
|
||||
|
||||
# If any, display warning message when group is saved
|
||||
if warning_message:
|
||||
warning_message = mark_safe(_(f'The following users are members of multiple groups:'
|
||||
f'{warning_message}'))
|
||||
messages.add_message(request, messages.WARNING, warning_message)
|
||||
if len(multiple_group_users) > 0:
|
||||
|
||||
msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users)
|
||||
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
msg
|
||||
)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
"""Save the inline formset"""
|
||||
|
||||
@@ -422,11 +422,7 @@ class RuleSet(models.Model):
|
||||
"""Construct the correctly formatted permission string, given the app_model name, and the permission type."""
|
||||
model, app = split_model(model)
|
||||
|
||||
return "{app}.{perm}_{model}".format(
|
||||
app=app,
|
||||
perm=permission,
|
||||
model=model
|
||||
)
|
||||
return f"{app}.{permission}_{model}"
|
||||
|
||||
def __str__(self, debug=False): # pragma: no cover
|
||||
"""Ruleset string representation."""
|
||||
@@ -504,12 +500,7 @@ def update_group_roles(group, debug=False):
|
||||
# and create a simplified permission key string
|
||||
for p in group.permissions.all().prefetch_related('content_type'):
|
||||
(permission, app, model) = p.natural_key()
|
||||
|
||||
permission_string = '{app}.{perm}'.format(
|
||||
app=app,
|
||||
perm=permission
|
||||
)
|
||||
|
||||
permission_string = f"{app}.{permission}"
|
||||
group_permissions.add(permission_string)
|
||||
|
||||
# List of permissions which must be added to the group
|
||||
@@ -527,7 +518,7 @@ def update_group_roles(group, debug=False):
|
||||
allowed: Whether or not the action is allowed
|
||||
"""
|
||||
if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover
|
||||
raise ValueError("Action {a} is invalid".format(a=action))
|
||||
raise ValueError(f"Action {action} is invalid")
|
||||
|
||||
permission_string = RuleSet.get_model_permission_string(model, action)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Template tag to render SPA imports."""
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
@@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe
|
||||
logger = getLogger("InvenTree")
|
||||
register = template.Library()
|
||||
|
||||
PUI_DEFAULTS = {
|
||||
'url_base': settings.PUI_URL_BASE,
|
||||
}
|
||||
PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {}))
|
||||
PUI_SETTINGS = json.dumps(PUI_DEFAULTS)
|
||||
FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
@@ -30,11 +27,11 @@ def spa_bundle():
|
||||
index = manifest_data.get("index.html")
|
||||
css_index = manifest_data.get("index.css")
|
||||
|
||||
dynmanic_files = index.get("dynamicImports", [])
|
||||
dynamic_files = index.get("dynamicImports", [])
|
||||
imports_files = "".join(
|
||||
[
|
||||
f'<script type="module" src="{settings.STATIC_URL}web/{manifest_data[file]["file"]}"></script>'
|
||||
for file in dynmanic_files
|
||||
for file in dynamic_files
|
||||
]
|
||||
)
|
||||
|
||||
@@ -47,4 +44,4 @@ def spa_bundle():
|
||||
@register.simple_tag
|
||||
def spa_settings():
|
||||
"""Render settings for spa."""
|
||||
return mark_safe(f"""<script>window.INVENTREE_SETTINGS={PUI_SETTINGS}</script>""")
|
||||
return mark_safe(f"""<script>window.INVENTREE_SETTINGS={FRONTEND_SETTINGS}</script>""")
|
||||
|
||||
@@ -20,12 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(f'{settings.PUI_URL_BASE}/', include([
|
||||
path(f'{settings.FRONTEND_URL_BASE}/', include([
|
||||
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||
re_path(r"^(?P<path>.*)/$", spa_view),
|
||||
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
|
||||
path("", spa_view),]
|
||||
)),
|
||||
path(settings.PUI_URL_BASE, spa_view, name='platform'),
|
||||
path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
|
||||
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||
]
|
||||
|
||||
@@ -25,8 +25,90 @@ The following code types are known to be supported
|
||||
- Data Matrix
|
||||
- Aztec
|
||||
|
||||
## Actions
|
||||
## Barcode Input Methods
|
||||
|
||||
The InvenTree app uses barcodes where possible to provide efficient stock control operations.
|
||||
Barcodes can be scanned using the following methods:
|
||||
|
||||
If there is a new barcode feature you would like to see, [let us know on GitHub](https://github.com/inventree/InvenTree/issues?q=is%3Aopen+is%3Aissue+label%3Aapp)!
|
||||
### Camera Input
|
||||
|
||||
The camera input method allows you to scan barcodes using the device's internal camera. Both the forward and rear-facing cameras are supported.
|
||||
|
||||
### Keyboard Input
|
||||
|
||||
The keyboard wedge input method allows you to scan barcodes using any scanner which presents barcode data as keyboard input. This works with external bluetooth scanners, and also provides support for integrated barcode scanner devices which run Android natively.
|
||||
|
||||
Note that if using keyboard wedge input mode, the scanner must be configured to append an enter (`\n`) character to the end of the barcode data.
|
||||
|
||||
## Barcode Actions
|
||||
|
||||
The InvenTree app uses barcodes where possible to provide efficient stock control operations. Some pages in the app will provide context-sensitive barcode actions. These actions are available from the *Barcode Actions* menu, which is displayed in the bottom right corner of the screen.
|
||||
|
||||
### Global Scan
|
||||
|
||||
Available from the global bottom menu, the *Scan Barcode* provides quick access for scanning a barcode already associated with an InvenTree database item (such as a stock item or location).
|
||||
|
||||
If a match is found, the app will navigate to the relevant page.
|
||||
|
||||
### Stock Location Actions
|
||||
|
||||
From the [Stock Location detail page](./stock.md#stock-location-view), multiple barcode actions may be available:
|
||||
|
||||
{% with id="location-actions", url="app/barcode_stock_location_actions.png", maxheight="240px", description="Stock location barcode actions" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
#### Assign Barcode
|
||||
|
||||
Assign a custom barcode to the selected location. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected location.
|
||||
|
||||
#### Transfer Stock Location
|
||||
|
||||
Transfer the currently selected stock location into another location. Scanning a valid barcode associated with a stock location will result in the current location being *moved* to the scanned location.
|
||||
|
||||
#### Scan Received Parts
|
||||
|
||||
Receive incoming purchase order items into the selected location. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into the selected location.
|
||||
|
||||
#### Scan Items Into Location
|
||||
|
||||
the *Scan Items Into Location* action allows you to scan items into the selected location. Scanning a valid barcode associated with a stock item (already in the database) will result in that item being transferred to the selected location.
|
||||
|
||||
### Stock Item Actions
|
||||
|
||||
From the [Stock Item detail page](./stock.md#stock-item-detail-view), the following barcode actions may be available:
|
||||
|
||||
{% with id="item-actions", url="app/barcode_stock_item_actions.png", maxheight="240px", description="Stock item barcode actions" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
#### Assign Barcode
|
||||
|
||||
Assign a custom barcode to the selected stock item. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected stock item.
|
||||
|
||||
#### Scan Into Location
|
||||
|
||||
Scan the selected stock item into a stock location. Scanning a valid barcode associated with a stock location will result in the selected stock item being transferred to the scanned location.
|
||||
|
||||
### Part Actions
|
||||
|
||||
From the [Part detail page](./part.md#part-detail-view), the following barcode actions are available:
|
||||
|
||||
{% with id="part-actions", url="app/barcode_part_actions.png", maxheight="240px", description="Part barcode actions" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
#### Assign Barcode
|
||||
|
||||
Assign a custom barcode to the selected part. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected part.
|
||||
|
||||
### Purchase Order Actions
|
||||
|
||||
From the [Purchase Order detail page](./po.md#purchase-order-detail) page, the following barcode actions are available:
|
||||
|
||||
{% with id="po-actions", url="app/barcode_po_actions.png", maxheight="240px", description="Purchase order barcode actions" %}
|
||||
{% include 'img.html' %}
|
||||
{% endwith %}
|
||||
|
||||
#### Scan Received Parts
|
||||
|
||||
Receive incoming purchase order items against the selected purchase order. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into stock.
|
||||
|
||||
@@ -48,7 +48,7 @@ The <span class='fas fa-search'></span> action opens the [Search](./search.md) s
|
||||
|
||||
### Scan Barcode
|
||||
|
||||
The <span class='fas fa-qrcode'></span> action opens the [barcode scan](./barcode.md) window, which allows quick access to the barcode scanning functionality.
|
||||
The <span class='fas fa-qrcode'></span> action opens the [barcode scan](./barcode.md#global-scan) window, which allows quick access to the barcode scanning functionality.
|
||||
|
||||
## Context Actions
|
||||
|
||||
|
||||
@@ -10,13 +10,26 @@ The InvenTree mobile app requires some extra permissions for complete functional
|
||||
|
||||
### User Profiles
|
||||
|
||||
The InvenTree app requires the user to enter profile data to connect with an InvenTree server:
|
||||
For each *profile* configured in the app, the following information is stored locally on the device:
|
||||
|
||||
- Server address
|
||||
- Account username
|
||||
- Account password
|
||||
- Server name (e.g. "InvenTree Demo")
|
||||
- Server address (e.g. "https://demo.inventree.org)
|
||||
- *API token*
|
||||
|
||||
Profile data is stored locally on the device, and can be deleted by the user if they wish. Profile data is only used for connection with an InvenTree server.
|
||||
#### User Authentication
|
||||
|
||||
The InvenTree app uses an API token for user authentication. This token is requested once from the server, and then stored locally on the device.
|
||||
|
||||
To initially request the token, the user will be required to enter their username and password.
|
||||
|
||||
!!! info "Password Storage"
|
||||
The user's username and password are not stored locally, or used for any purpose other than requesting an API token
|
||||
|
||||
#### Token Handling
|
||||
|
||||
A separate API token is stored locally for each profile. This token can be deleted at any time from within the app settings - this will force the user to enter their login credentials again to request a new token.
|
||||
|
||||
Additionally, the stored token may be revoked by the server, or expire. Either situation will again require the user to re-enter their username and password.
|
||||
|
||||
### Camera Permissions
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Configure audible app notifications:
|
||||
|
||||
## Barcode Settings
|
||||
|
||||
The *Barcode Settings* view allows you to configure options relating to barcode scanning:
|
||||
The *Barcode Settings* view allows you to configure options relating to [barcode scanning](./barcode.md):
|
||||
|
||||
{% with id="barcode_settings", url="app/barcode_settings.png", maxheight="240px", description="Barcode Settings" %}
|
||||
{% include 'img.html' %}
|
||||
@@ -63,6 +63,7 @@ The *Barcode Settings* view allows you to configure options relating to barcode
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| Scanner Input | Select barcode capture mode |
|
||||
| Barcode Scan Delay | Delay between successive scans |
|
||||
|
||||
## Home Screen
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
@@ -20,8 +20,8 @@ export const doClassicLogin = async (username: string, password: string) => {
|
||||
const token = await axios
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
auth: { username, password },
|
||||
baseURL: host.toString(),
|
||||
timeout: 5000,
|
||||
baseURL: host,
|
||||
timeout: 2000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
@@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout the user (invalidate auth token)
|
||||
*/
|
||||
export const doClassicLogout = async () => {
|
||||
// TODO @matmair - logout from the server session
|
||||
// Set token in context
|
||||
@@ -117,7 +120,7 @@ export function handleReset(navigate: any, values: { email: string }) {
|
||||
export function checkLoginState(navigate: any, redirect?: string) {
|
||||
api
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
timeout: 5000,
|
||||
timeout: 2000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
@@ -137,8 +140,7 @@ export function checkLoginState(navigate: any, redirect?: string) {
|
||||
navigate('/login');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching login information:', error);
|
||||
.catch(() => {
|
||||
navigate('/login');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ declare global {
|
||||
server_list: HostList;
|
||||
default_server: string;
|
||||
show_server_selector: boolean;
|
||||
url_base: string;
|
||||
base_url: string;
|
||||
sentry_dsn?: string;
|
||||
environment?: string;
|
||||
};
|
||||
@@ -25,6 +25,17 @@ export const IS_DEV = import.meta.env.DEV;
|
||||
export const IS_DEMO = import.meta.env.VITE_DEMO === 'true';
|
||||
export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
|
||||
|
||||
// Filter out any settings that are not defined
|
||||
let loaded_vals = (window.INVENTREE_SETTINGS || {}) as any;
|
||||
Object.keys(loaded_vals).forEach((key) => {
|
||||
if (loaded_vals[key] === undefined) {
|
||||
delete loaded_vals[key];
|
||||
// check for empty server list
|
||||
} else if (key === 'server_list' && loaded_vals[key].length === 0) {
|
||||
delete loaded_vals[key];
|
||||
}
|
||||
});
|
||||
|
||||
window.INVENTREE_SETTINGS = {
|
||||
server_list: {
|
||||
'mantine-cqj63coxn': {
|
||||
@@ -40,11 +51,11 @@ window.INVENTREE_SETTINGS = {
|
||||
}
|
||||
: {})
|
||||
},
|
||||
default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', // use demo server for demo mode
|
||||
default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn',
|
||||
show_server_selector: IS_DEV_OR_DEMO,
|
||||
|
||||
// merge in settings that are already set via django's spa_view or for development
|
||||
...((window.INVENTREE_SETTINGS || {}) as any)
|
||||
...loaded_vals
|
||||
};
|
||||
|
||||
if (window.INVENTREE_SETTINGS.sentry_dsn) {
|
||||
@@ -56,7 +67,7 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) {
|
||||
});
|
||||
}
|
||||
|
||||
export const url_base = window.INVENTREE_SETTINGS.url_base || 'platform';
|
||||
export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
@@ -66,5 +77,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
||||
// Redirect to base url if on /
|
||||
if (window.location.pathname === '/') {
|
||||
window.location.replace(`/${url_base}`);
|
||||
window.location.replace(`/${base_url}`);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ export const useServerApiState = create<ServerApiStateProps>((set, get) => ({
|
||||
setServer: (newServer: ServerAPIProps) => set({ server: newServer }),
|
||||
fetchServerApiState: async () => {
|
||||
// Fetch server data
|
||||
await api.get(apiUrl(ApiPaths.api_server_info)).then((response) => {
|
||||
await api
|
||||
.get(apiUrl(ApiPaths.api_server_info))
|
||||
.then((response) => {
|
||||
set({ server: response.data });
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
||||
// Fetch user data
|
||||
await api
|
||||
.get(apiUrl(ApiPaths.user_me), {
|
||||
timeout: 5000
|
||||
timeout: 2000
|
||||
})
|
||||
.then((response) => {
|
||||
const user: UserProps = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import { queryClient, setApiDefaults } from '../App';
|
||||
import { BaseContext } from '../contexts/BaseContext';
|
||||
import { defaultHostList } from '../defaults/defaultHostList';
|
||||
import { url_base } from '../main';
|
||||
import { base_url } from '../main';
|
||||
import { routes } from '../router';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { useSessionState } from '../states/SessionState';
|
||||
@@ -27,10 +27,6 @@ export default function DesktopAppView() {
|
||||
]);
|
||||
|
||||
// Local state initialization
|
||||
if (Object.keys(hostList).length === 0) {
|
||||
console.log('Loading default host list');
|
||||
useLocalState.setState({ hostList: defaultHostList });
|
||||
}
|
||||
setApiDefaults();
|
||||
|
||||
// Server Session
|
||||
@@ -38,6 +34,11 @@ export default function DesktopAppView() {
|
||||
const sessionState = useSessionState.getState();
|
||||
const [token] = sessionState.token ? [sessionState.token] : [null];
|
||||
useEffect(() => {
|
||||
if (Object.keys(hostList).length === 0) {
|
||||
console.log('Loading default host list', defaultHostList);
|
||||
useLocalState.setState({ hostList: defaultHostList });
|
||||
}
|
||||
|
||||
if (token && !fetchedServerSession) {
|
||||
setFetchedServerSession(true);
|
||||
fetchUserState();
|
||||
@@ -49,7 +50,7 @@ export default function DesktopAppView() {
|
||||
return (
|
||||
<BaseContext>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter basename={url_base}>{routes}</BrowserRouter>
|
||||
<BrowserRouter basename={base_url}>{routes}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</BaseContext>
|
||||
);
|
||||
|
||||
@@ -11,9 +11,4 @@ test('Basic Platform UI test', async ({ page }) => {
|
||||
await page.goto('./platform/');
|
||||
|
||||
await expect(page).toHaveTitle('InvenTree Demo Server');
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: 'Welcome to your Dashboard, Ally Access'
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user