Merge branch 'inventree:master' into pr/ChristianSchindler/6305
This commit is contained in:
commit
2d89f068f4
|
|
@ -9,15 +9,13 @@ on:
|
|||
pull_request_target:
|
||||
types: [ "labeled", "closed" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event.pull_request.merged == true
|
||||
&& contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
|
|
@ -27,11 +25,10 @@ jobs:
|
|||
)
|
||||
steps:
|
||||
- name: Backport Action
|
||||
uses: sqren/backport-github-action@f7073a2287aefc1fa12685eb25a712ab5620445c # pin@v9.2.2
|
||||
uses: sqren/backport-github-action@f54e19901f2a57f8b82360f2490d47ee82ec82c6 # pin@v9.2.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
add_original_reviewers: true
|
||||
|
||||
- name: Info log
|
||||
if: ${{ success() }}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ jobs:
|
|||
docker run --rm inventree-test invoke --list
|
||||
docker run --rm inventree-test gunicorn --version
|
||||
docker run --rm inventree-test pg_dump --version
|
||||
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/manage.py
|
||||
- name: Build Docker Image
|
||||
# Build the development docker image (using docker-compose.yml)
|
||||
run: docker compose --project-directory . -f contrib/container/dev-docker-compose.yml build --no-cache
|
||||
|
|
@ -127,7 +128,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # pin@v3.2.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # pin@v3.4.0
|
||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # pin@v3.5.0
|
||||
- name: Check if Dockerhub login is required
|
||||
id: docker_login
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ env:
|
|||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
paths-filter:
|
||||
name: Filter
|
||||
|
|
@ -34,6 +35,7 @@ jobs:
|
|||
migrations: ${{ steps.filter.outputs.migrations }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
api: ${{ steps.filter.outputs.api }}
|
||||
force: ${{ steps.force.outputs.force }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
|
|
@ -46,12 +48,21 @@ jobs:
|
|||
- 'src/backend/requirements.txt'
|
||||
- 'src/backend/requirements-dev.txt'
|
||||
migrations:
|
||||
- '**/test_migrations.py'
|
||||
- '**/migrations/**'
|
||||
- '.github/workflows**'
|
||||
- 'src/backend/requirements.txt'
|
||||
api:
|
||||
- 'InvenTree/InvenTree/api_version.py'
|
||||
- 'src/backend/InvenTree/InvenTree/api_version.py'
|
||||
frontend:
|
||||
- 'src/frontend/**'
|
||||
- name: Is CI being forced?
|
||||
run: echo "force=true" >> $GITHUB_OUTPUT
|
||||
id: force
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'dependency') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'full-run')
|
||||
|
||||
|
||||
javascript:
|
||||
name: Style - Classic UI [JS]
|
||||
|
|
@ -73,13 +84,13 @@ jobs:
|
|||
- name: Lint Javascript Files
|
||||
run: |
|
||||
python src/backend/InvenTree/manage.py prerender
|
||||
npx eslint src/backend/InvenTree/InvenTree/static_i18n/i18n/*.js
|
||||
cd src/backend && npx eslint InvenTree/InvenTree/static_i18n/i18n/*.js
|
||||
|
||||
pre-commit:
|
||||
name: Style [pre-commit]
|
||||
runs-on: ubuntu-20.04
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true'
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
|
||||
|
|
@ -125,7 +136,7 @@ jobs:
|
|||
name: Tests - API Schema Documentation
|
||||
runs-on: ubuntu-20.04
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.server == 'true'
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||
|
|
@ -202,7 +213,7 @@ jobs:
|
|||
echo "Version: $version"
|
||||
mkdir export/${version}
|
||||
mv schema.yml export/${version}/api.yaml
|
||||
- uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5.0.0
|
||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
||||
with:
|
||||
commit_message: "Update API schema for ${version}"
|
||||
|
||||
|
|
@ -210,7 +221,8 @@ jobs:
|
|||
name: Tests - inventree-python
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
needs: pre-commit
|
||||
needs: [ 'pre-commit', 'paths-filter' ]
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
env:
|
||||
wrapper_name: inventree-python
|
||||
|
|
@ -251,7 +263,8 @@ jobs:
|
|||
name: Tests - DB [SQLite] + Coverage
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
needs: [ 'pre-commit' ]
|
||||
needs: [ 'pre-commit', 'paths-filter' ]
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
continue-on-error: true # continue if a step fails so that coverage gets pushed
|
||||
|
||||
env:
|
||||
|
|
@ -276,17 +289,29 @@ jobs:
|
|||
run: python3 .github/scripts/check_migration_files.py
|
||||
- name: Coverage Tests
|
||||
run: invoke test --coverage
|
||||
- name: Upload Coverage Report
|
||||
- name: Upload Coverage Report to Coveralls
|
||||
if: always()
|
||||
uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
flag-name: backend
|
||||
file: coverage.xml
|
||||
git-commit: ${{ github.sha }}
|
||||
git-branch: ${{ github.ref }}
|
||||
parallel: true
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.3.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: inventree/InvenTree
|
||||
flags: backend
|
||||
|
||||
postgres:
|
||||
name: Tests - DB [PostgreSQL]
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ 'pre-commit' ]
|
||||
needs: [ 'pre-commit', 'paths-filter' ]
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||
|
|
@ -330,7 +355,8 @@ jobs:
|
|||
name: Tests - DB [MySQL]
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
needs: [ 'pre-commit' ]
|
||||
needs: [ 'pre-commit', 'paths-filter' ]
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
|
||||
env:
|
||||
# Database backend configuration
|
||||
|
|
@ -373,7 +399,7 @@ jobs:
|
|||
name: Tests - Migrations [PostgreSQL]
|
||||
runs-on: ubuntu-latest
|
||||
needs: paths-filter
|
||||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true'
|
||||
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
|
||||
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||
|
|
@ -404,13 +430,28 @@ jobs:
|
|||
dev-install: true
|
||||
update: true
|
||||
- name: Run Tests
|
||||
run: invoke test --migrations --report
|
||||
run: invoke test --migrations --report --coverage
|
||||
- name: Upload Coverage Report to Coveralls
|
||||
if: always()
|
||||
uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
flag-name: migrations
|
||||
git-commit: ${{ github.sha }}
|
||||
git-branch: ${{ github.ref }}
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.3.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: inventree/InvenTree
|
||||
flags: migrations
|
||||
|
||||
migrations-checks:
|
||||
name: Tests - Full Migration [SQLite]
|
||||
runs-on: ubuntu-latest
|
||||
needs: paths-filter
|
||||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true'
|
||||
if: ${{ (needs.paths-filter.outputs.force == 'true') || (github.ref == 'refs/heads/master' && needs.paths-filter.outputs.migrations == 'true') }}
|
||||
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
|
|
@ -467,7 +508,7 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 60
|
||||
needs: [ 'pre-commit', 'paths-filter' ]
|
||||
if: needs.paths-filter.outputs.frontend == 'true'
|
||||
if: needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true'
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
|
||||
|
|
@ -485,36 +526,40 @@ jobs:
|
|||
update: true
|
||||
- name: Set up test data
|
||||
run: invoke setup-test -i
|
||||
- name: Rebuild thumbnails
|
||||
run: invoke rebuild-thumbnails
|
||||
- name: Install dependencies
|
||||
run: inv frontend-compile
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend && npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
id: tests
|
||||
run: cd src/frontend && npx nyc playwright test
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
|
||||
name: Upload playwright report
|
||||
if: always()
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: src/frontend/playwright-report/
|
||||
retention-days: 30
|
||||
retention-days: 14
|
||||
- name: Report coverage
|
||||
if: always()
|
||||
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
|
||||
name: Upload coverage report
|
||||
if: always()
|
||||
with:
|
||||
name: coverage
|
||||
path: src/frontend/coverage/
|
||||
retention-days: 30
|
||||
- name: Upload Coverage Report
|
||||
- name: Upload Coverage Report to Coveralls
|
||||
if: always()
|
||||
uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
flag-name: pui
|
||||
git-commit: ${{ github.sha }}
|
||||
git-branch: ${{ github.ref }}
|
||||
parallel: true
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.3.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: inventree/InvenTree
|
||||
flags: pui
|
||||
|
||||
platform_ui_build:
|
||||
name: Build - UI Platform
|
||||
|
|
@ -530,11 +575,11 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
run: cd src/frontend && yarn run compile && yarn run build
|
||||
- name: Zip frontend
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static
|
||||
zip -r frontend-build.zip web/
|
||||
zip -r frontend-build.zip web/ web/.vite
|
||||
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # pin@v4.3.1
|
||||
with:
|
||||
name: frontend-build
|
||||
|
|
@ -543,13 +588,15 @@ jobs:
|
|||
finish_coverage:
|
||||
name: Finish Coverage
|
||||
runs-on: ubuntu-20.04
|
||||
needs: ["platform_ui", "coverage", "paths-filter"]
|
||||
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true'
|
||||
needs: ["platform_ui", "coverage", "migration-tests", "paths-filter"]
|
||||
if: (needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true') && (needs.platform_ui.result == 'success' || needs.coverage.result == 'success' || needs.migration-tests.result == 'success')
|
||||
|
||||
steps:
|
||||
- name: Finish coverage reporting
|
||||
uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
carryforward: "pui,backend"
|
||||
carryforward: "pui,backend,migrations"
|
||||
parallel-finished: true
|
||||
git-commit: ${{ github.sha }}
|
||||
git-branch: ${{ github.ref }}
|
||||
|
|
|
|||
|
|
@ -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@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
uses: github/codeql-action/upload-sarif@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ env/
|
|||
|
||||
# Locale stats file
|
||||
src/backend/InvenTree/InvenTree/locale_stats.json
|
||||
src/backend/InvenTree/InvenTree/licenses.txt
|
||||
|
||||
# node.js
|
||||
node_modules/
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ repos:
|
|||
- id: check-yaml
|
||||
- id: mixed-line-ending
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.4
|
||||
rev: v0.3.5
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
args: [--preview]
|
||||
|
|
@ -27,7 +27,7 @@ repos:
|
|||
--preview
|
||||
]
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: v0.1.24
|
||||
rev: 0.1.29
|
||||
hooks:
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements-dev.in
|
||||
|
|
@ -61,7 +61,7 @@ repos:
|
|||
- "prettier@^2.4.1"
|
||||
- "@trivago/prettier-plugin-sort-imports"
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: "v9.0.0-rc.0"
|
||||
rev: "v9.0.0"
|
||||
hooks:
|
||||
- id: eslint
|
||||
additional_dependencies:
|
||||
|
|
|
|||
2
Procfile
2
Procfile
|
|
@ -1,7 +1,7 @@
|
|||
# Web process: gunicorn
|
||||
web: env/bin/gunicorn --chdir $APP_HOME/src/backend/InvenTree -c src/backend/InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
|
||||
# Worker process: qcluster
|
||||
worker: env/bin/python src/backendInvenTree/manage.py qcluster
|
||||
worker: env/bin/python src/backend/InvenTree/manage.py qcluster
|
||||
# Invoke commands
|
||||
invoke: echo "" | echo "" && . env/bin/activate && invoke
|
||||
# CLI: Provided for backwards compatibility
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
[](https://sonarcloud.io/summary/new_code?id=inventree_InvenTree)
|
||||
|
||||
[](https://coveralls.io/github/inventree/InvenTree)
|
||||
[](https://codecov.io/gh/inventree/InvenTree)
|
||||
[](https://crowdin.com/project/inventree)
|
||||

|
||||
[](https://hub.docker.com/r/inventree/inventree)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"repoOwner": "Oliver Walters",
|
||||
"repoName": "InvenTree",
|
||||
"targetBranchChoices": [],
|
||||
"branchLabelMapping": {
|
||||
"^backport-to-(.+)$": "$1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 82%
|
||||
|
||||
github_checks:
|
||||
annotations: true
|
||||
|
||||
flag_management:
|
||||
default_rules:
|
||||
carryforward: true
|
||||
individual_flags:
|
||||
- name: backend
|
||||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 85%
|
||||
- name: migrations
|
||||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 40%
|
||||
- name: pui
|
||||
carryforward: true
|
||||
statuses:
|
||||
- type: project
|
||||
target: 45%
|
||||
|
|
@ -33,6 +33,8 @@ ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
|||
ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
|
||||
ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DATA_DIR}/plugins"
|
||||
|
||||
ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}/src/backend"
|
||||
|
||||
# InvenTree configuration files
|
||||
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
|
||||
|
|
@ -127,19 +129,16 @@ ENV INVENTREE_COMMIT_DATE="${commit_date}"
|
|||
ENV PATH=/root/.local/bin:$PATH
|
||||
COPY --from=prebuild /root/.local /root/.local
|
||||
|
||||
ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}"
|
||||
|
||||
# Copy source code
|
||||
COPY src/backend/InvenTree ./InvenTree
|
||||
COPY --from=frontend ${INVENTREE_HOME}/src/backend/InvenTree/web/static/web ./src/backend/InvenTree/web/static/web
|
||||
COPY src/backend/InvenTree ${INVENTREE_BACKEND_DIR}/InvenTree
|
||||
COPY --from=frontend ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web
|
||||
|
||||
# Launch the production server
|
||||
CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ./InvenTree
|
||||
CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ${INVENTREE_BACKEND_DIR}/InvenTree
|
||||
|
||||
FROM inventree_base AS dev
|
||||
|
||||
ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}/src/backend"
|
||||
|
||||
# Vite server (for local frontend development)
|
||||
EXPOSE 5173
|
||||
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ The following guide provides a streamlined production InvenTree installation, wi
|
|||
|
||||
### Required Files
|
||||
|
||||
The following files required for this setup are provided with the InvenTree source, located in the `./docker/` directory of the [InvenTree source code](https://github.com/inventree/InvenTree/tree/master/docker/):
|
||||
The following files required for this setup are provided with the InvenTree source, located in the `/contrib/container/` directory of the [InvenTree source code](https://github.com/inventree/InvenTree/tree/master/contrib/container/):
|
||||
|
||||
| Filename | Description |
|
||||
| --- | --- |
|
||||
| [docker-compose.yml](https://github.com/inventree/InvenTree/blob/master/docker/docker-compose.yml) | The docker compose script |
|
||||
| [.env](https://github.com/inventree/InvenTree/blob/master/docker/.env) | Environment variables |
|
||||
| [Caddyfile](https://github.com/inventree/InvenTree/blob/master/docker/Caddyfile) | Caddy configuration file |
|
||||
| [docker-compose.yml](https://github.com/inventree/InvenTree/blob/master/contrib/container/docker-compose.yml) | The docker compose script |
|
||||
| [.env](https://github.com/inventree/InvenTree/blob/master/contrib/container/.env) | Environment variables |
|
||||
| [Caddyfile](https://github.com/inventree/InvenTree/blob/master/contrib/container/Caddyfile) | Caddy configuration file |
|
||||
|
||||
Download these files to a directory on your local machine.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# Dummy requirements file to trigger the package pipeline
|
||||
# The new requirements file is located in src/backend/requirements.txt
|
||||
# The backend requirements file is located in src/backend/requirements.txt
|
||||
#
|
||||
|
|
|
|||
|
|
@ -84,25 +84,48 @@ class InvenTreeResource(ModelResource):
|
|||
|
||||
return [f for f in fields if f.column_name not in fields_to_exclude]
|
||||
|
||||
def before_import(self, dataset, using_transactions, dry_run, **kwargs):
|
||||
"""Run custom code before importing data.
|
||||
|
||||
- Determine the list of fields which need to be converted to empty strings
|
||||
"""
|
||||
# Construct a map of field names
|
||||
db_fields = {field.name: field for field in self.Meta.model._meta.fields}
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
# Skip read-only fields (they cannot be imported)
|
||||
if field.readonly:
|
||||
continue
|
||||
|
||||
# Determine the name of the associated column in the dataset
|
||||
column = getattr(field, 'column_name', field_name)
|
||||
|
||||
# Determine the attribute name of the associated database field
|
||||
attribute = getattr(field, 'attribute', field_name)
|
||||
|
||||
# Check if the associated database field is a non-nullable string
|
||||
if db_field := db_fields.get(attribute):
|
||||
if (
|
||||
isinstance(db_field, CharField)
|
||||
and db_field.blank
|
||||
and not db_field.null
|
||||
):
|
||||
if column not in self.CONVERT_NULL_FIELDS:
|
||||
self.CONVERT_NULL_FIELDS.append(column)
|
||||
|
||||
return super().before_import(dataset, using_transactions, dry_run, **kwargs)
|
||||
|
||||
def before_import_row(self, row, row_number=None, **kwargs):
|
||||
"""Run custom code before importing each row.
|
||||
|
||||
- Convert any null fields to empty strings, for fields which do not support null values
|
||||
"""
|
||||
# We can automatically determine which fields might need such a conversion
|
||||
for field in self.Meta.model._meta.fields:
|
||||
if (
|
||||
isinstance(field, CharField)
|
||||
and field.blank
|
||||
and not field.null
|
||||
and field.name not in self.CONVERT_NULL_FIELDS
|
||||
):
|
||||
self.CONVERT_NULL_FIELDS.append(field.name)
|
||||
|
||||
for field in self.CONVERT_NULL_FIELDS:
|
||||
if field in row and row[field] is None:
|
||||
row[field] = ''
|
||||
|
||||
return super().before_import_row(row, row_number, **kwargs)
|
||||
|
||||
|
||||
class CustomRateAdmin(RateAdmin):
|
||||
"""Admin interface for the Rate class."""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
"""Main JSON interface views."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
|
@ -31,6 +34,60 @@ from .status import check_system_health, is_worker_running
|
|||
from .version import inventreeApiText
|
||||
from .views import AjaxView
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class LicenseViewSerializer(serializers.Serializer):
|
||||
"""Serializer for license information."""
|
||||
|
||||
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
|
||||
frontend = serializers.CharField(
|
||||
help_text='Frontend licenses texts', read_only=True
|
||||
)
|
||||
|
||||
|
||||
class LicenseView(APIView):
|
||||
"""Simple JSON endpoint for InvenTree license information."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def read_license_file(self, path: Path) -> list:
|
||||
"""Extract license information from the provided file.
|
||||
|
||||
Arguments:
|
||||
path: Path to the license file
|
||||
|
||||
Returns: A list of items containing the license information
|
||||
"""
|
||||
# Check if the file exists
|
||||
if not path.exists():
|
||||
logger.error("License file not found at '%s'", path)
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except json.JSONDecodeError as e:
|
||||
logger.exception("Failed to parse license file '%s': %s", path, e)
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.exception("Exception while reading license file '%s': %s", path, e)
|
||||
return []
|
||||
|
||||
# Ensure consistent string between backend and frontend licenses
|
||||
return [{key.lower(): value for key, value in entry.items()} for entry in data]
|
||||
|
||||
@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return information about the InvenTree server."""
|
||||
backend = Path(__file__).parent.joinpath('licenses.txt')
|
||||
frontend = Path(__file__).parent.parent.joinpath(
|
||||
'web/static/web/.vite/dependencies.json'
|
||||
)
|
||||
return JsonResponse({
|
||||
'backend': self.read_license_file(backend),
|
||||
'frontend': self.read_license_file(frontend),
|
||||
})
|
||||
|
||||
|
||||
class VersionViewSerializer(serializers.Serializer):
|
||||
"""Serializer for a single version."""
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 185
|
||||
INVENTREE_API_VERSION = 187
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v187 - 2024-03-10 : https://github.com/inventree/InvenTree/pull/6985
|
||||
- Allow Part list endpoint to be sorted by pricing_min and pricing_max values
|
||||
- Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values
|
||||
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
|
||||
- Adds total pricing values to BomItem serializer
|
||||
|
||||
v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855
|
||||
- Adds license information to the API
|
||||
|
||||
v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
|
||||
- Remove /plugin/activate endpoint
|
||||
- Update docstrings and typing for various API endpoints (no functional changes)
|
||||
|
|
|
|||
|
|
@ -268,6 +268,49 @@ MIDDLEWARE = CONFIG.get(
|
|||
],
|
||||
)
|
||||
|
||||
# In DEBUG mode, add support for django-querycount
|
||||
# Ref: https://github.com/bradmontgomery/django-querycount
|
||||
if DEBUG and get_boolean_setting(
|
||||
'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False
|
||||
):
|
||||
MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware')
|
||||
|
||||
QUERYCOUNT = {
|
||||
'THRESHOLDS': {
|
||||
'MEDIUM': 50,
|
||||
'HIGH': 200,
|
||||
'MIN_TIME_TO_LOG': 0,
|
||||
'MIN_QUERY_COUNT_TO_LOG': 0,
|
||||
},
|
||||
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
|
||||
'IGNORE_SQL_PATTERNS': [],
|
||||
'DISPLAY_DUPLICATES': 3,
|
||||
'RESPONSE_HEADER': 'X-Django-Query-Count',
|
||||
}
|
||||
|
||||
ADMIN_SHELL_ENABLE = False
|
||||
ADMIN_SHELL_IMPORT_DJANGO = False
|
||||
ADMIN_SHELL_IMPORT_MODELS = False
|
||||
|
||||
# In DEBUG mode, add support for django-admin-shell
|
||||
# Ref: https://github.com/djk2/django-admin-shell
|
||||
if (
|
||||
DEBUG
|
||||
and INVENTREE_ADMIN_ENABLED
|
||||
and get_boolean_setting('INVENTREE_DEBUG_SHELL', 'debug_shell', False)
|
||||
): # noqa
|
||||
try:
|
||||
import django_admin_shell
|
||||
|
||||
INSTALLED_APPS.append('django_admin_shell')
|
||||
ADMIN_SHELL_ENABLE = True
|
||||
|
||||
logger.warning('Admin shell is enabled')
|
||||
except ModuleNotFoundError:
|
||||
logger.warning(
|
||||
'django-admin-shell is not installed - Admin shell is not enabled'
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = CONFIG.get(
|
||||
'authentication_backends',
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1148,12 +1148,8 @@ class TestSettings(InvenTreeTestCase):
|
|||
|
||||
superuser = True
|
||||
|
||||
def in_env_context(self, envs=None):
|
||||
def in_env_context(self, envs):
|
||||
"""Patch the env to include the given dict."""
|
||||
# Set default - see B006
|
||||
if envs is None:
|
||||
envs = {}
|
||||
|
||||
return mock.patch.dict(os.environ, envs)
|
||||
|
||||
def run_reload(self, envs=None):
|
||||
|
|
@ -1588,15 +1584,15 @@ class ClassValidationMixinTest(TestCase):
|
|||
|
||||
def test(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
def test1(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
def test2(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
required_attributes = ['NAME']
|
||||
required_overrides = [test, [test1, test2]]
|
||||
|
|
@ -1616,11 +1612,11 @@ class ClassValidationMixinTest(TestCase):
|
|||
|
||||
def test(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
def test2(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
TestClass.validate()
|
||||
|
||||
|
|
@ -1643,7 +1639,7 @@ class ClassValidationMixinTest(TestCase):
|
|||
|
||||
def test2(self):
|
||||
"""Test function."""
|
||||
pass
|
||||
...
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
NotImplementedError,
|
||||
|
|
|
|||
|
|
@ -157,12 +157,14 @@ class UserMixin:
|
|||
|
||||
if type(assign_all) is not bool:
|
||||
# Raise exception if common mistake is made!
|
||||
raise TypeError('assignRole: assign_all must be a boolean value')
|
||||
raise TypeError(
|
||||
'assignRole: assign_all must be a boolean value'
|
||||
) # pragma: no cover
|
||||
|
||||
if not role and not assign_all:
|
||||
raise ValueError(
|
||||
'assignRole: either role must be provided, or assign_all must be set'
|
||||
)
|
||||
) # pragma: no cover
|
||||
|
||||
if not assign_all and role:
|
||||
rule, perm = role.split('.')
|
||||
|
|
@ -241,14 +243,18 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||
yield # your test will be run here
|
||||
|
||||
if verbose:
|
||||
msg = '\r\n%s' % json.dumps(context.captured_queries, indent=4)
|
||||
msg = '\r\n%s' % json.dumps(
|
||||
context.captured_queries, indent=4
|
||||
) # pragma: no cover
|
||||
else:
|
||||
msg = None
|
||||
|
||||
n = len(context.captured_queries)
|
||||
|
||||
if debug:
|
||||
print(f'Expected less than {value} queries, got {n} queries')
|
||||
print(
|
||||
f'Expected less than {value} queries, got {n} queries'
|
||||
) # pragma: no cover
|
||||
|
||||
self.assertLess(n, value, msg=msg)
|
||||
|
||||
|
|
@ -258,7 +264,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||
if expected_code is None:
|
||||
return
|
||||
|
||||
if expected_code != response.status_code:
|
||||
if expected_code != response.status_code: # pragma: no cover
|
||||
print(
|
||||
f"Unexpected {method} response at '{url}': status_code = {response.status_code}"
|
||||
)
|
||||
|
|
@ -280,11 +286,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||
response = self.client.options(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
actions = response.data.get('actions', None)
|
||||
|
||||
if not actions:
|
||||
actions = {}
|
||||
|
||||
actions = response.data.get('actions', {})
|
||||
return actions
|
||||
|
||||
def get(self, url, data=None, expected_code=200, format='json', **kwargs):
|
||||
|
|
|
|||
|
|
@ -39,7 +39,14 @@ from stock.urls import stock_urls
|
|||
from web.urls import api_urls as web_api_urls
|
||||
from web.urls import urlpatterns as platform_urls
|
||||
|
||||
from .api import APISearchView, InfoView, NotFoundView, VersionTextView, VersionView
|
||||
from .api import (
|
||||
APISearchView,
|
||||
InfoView,
|
||||
LicenseView,
|
||||
NotFoundView,
|
||||
VersionTextView,
|
||||
VersionView,
|
||||
)
|
||||
from .magic_login import GetSimpleLoginView
|
||||
from .social_auth_urls import (
|
||||
EmailListView,
|
||||
|
|
@ -99,6 +106,7 @@ apipatterns = [
|
|||
name='schema',
|
||||
),
|
||||
# InvenTree information endpoints
|
||||
path('license/', LicenseView.as_view(), name='api-license'), # license info
|
||||
path(
|
||||
'version-text', VersionTextView.as_view(), name='api-version-text'
|
||||
), # version text
|
||||
|
|
@ -377,6 +385,7 @@ if settings.ENABLE_CLASSIC_FRONTEND:
|
|||
|
||||
classic_frontendpatterns = [
|
||||
# Apps
|
||||
#
|
||||
path('build/', include(build_urls)),
|
||||
path('common/', include(common_urls)),
|
||||
path('company/', include(company_urls)),
|
||||
|
|
@ -426,7 +435,11 @@ classic_frontendpatterns = [
|
|||
urlpatterns = []
|
||||
|
||||
if settings.INVENTREE_ADMIN_ENABLED:
|
||||
admin_url = (settings.INVENTREE_ADMIN_URL,)
|
||||
admin_url = settings.INVENTREE_ADMIN_URL
|
||||
|
||||
if settings.ADMIN_SHELL_ENABLE: # noqa
|
||||
urlpatterns += [path(f'{admin_url}/shell/', include('django_admin_shell.urls'))]
|
||||
|
||||
urlpatterns += [
|
||||
path(f'{admin_url}/error_log/', include('error_report.urls')),
|
||||
path(f'{admin_url}/', admin.site.urls, name='inventree-admin'),
|
||||
|
|
|
|||
|
|
@ -258,6 +258,13 @@ class UserSettingsList(SettingsList):
|
|||
common.models.InvenTreeUserSetting.build_default_values(user=request.user)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return prefetched queryset."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.prefetch_related('user')
|
||||
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Only list settings which apply to the current user."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class NewsFeedTests(TestCase):
|
|||
"""Tests that news feed is updated when accessing a valid URL."""
|
||||
try:
|
||||
common_tasks.update_news_feed()
|
||||
except Exception as ex:
|
||||
except Exception as ex: # pragma: no cover
|
||||
self.fail(f'News feed raised exceptions: {ex}')
|
||||
|
||||
self.assertNotEqual(NewsFeedEntry.objects.all().count(), 0)
|
||||
|
|
|
|||
|
|
@ -1048,18 +1048,18 @@ class ColorThemeTest(TestCase):
|
|||
"""Test that default choices are returned."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip
|
||||
# skip due to directories not being set up
|
||||
if not result:
|
||||
return
|
||||
return # pragma: no cover
|
||||
self.assertIn(('default', 'Default'), result)
|
||||
|
||||
def test_valid_choice(self):
|
||||
"""Check that is_valid_choice works correctly."""
|
||||
result = ColorTheme.get_color_themes_choices()
|
||||
|
||||
# skip
|
||||
# skip due to directories not being set up
|
||||
if not result:
|
||||
return
|
||||
return # pragma: no cover
|
||||
|
||||
# check wrong reference
|
||||
self.assertFalse(ColorTheme.is_valid_choice('abcdd'))
|
||||
|
|
@ -1099,10 +1099,12 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
|||
# Exit early
|
||||
return
|
||||
|
||||
# Delay and try again
|
||||
time.sleep(10)
|
||||
# Delay and try again - might have problems with exchange rate endpoint
|
||||
time.sleep(10) # pragma: no cover
|
||||
|
||||
raise TimeoutError('Could not refresh currency exchange data after 5 attempts')
|
||||
raise TimeoutError(
|
||||
'Could not refresh currency exchange data after 5 attempts'
|
||||
) # pragma: no cover
|
||||
|
||||
|
||||
class NotesImageTest(InvenTreeAPITestCase):
|
||||
|
|
|
|||
|
|
@ -468,9 +468,13 @@ class SupplierPriceBreakList(ListCreateAPI):
|
|||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
filter_backends = ORDER_FILTER
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
||||
ordering_fields = ['quantity']
|
||||
ordering_fields = ['quantity', 'supplier', 'SKU', 'price']
|
||||
|
||||
search_fields = ['part__SKU', 'part__supplier__name']
|
||||
|
||||
ordering_field_aliases = {'supplier': 'part__supplier__name', 'SKU': 'part__SKU'}
|
||||
|
||||
ordering = 'quantity'
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ from InvenTree.unit_test import InvenTreeTestCase
|
|||
from .transition import StateTransitionMixin, TransitionMethod, storage
|
||||
|
||||
# Global variables to determine which transition classes raises an exception
|
||||
global raise_storage
|
||||
global raise_function
|
||||
|
||||
raise_storage = False
|
||||
raise_function = False
|
||||
|
||||
|
|
@ -90,7 +87,7 @@ class TransitionTests(InvenTreeTestCase):
|
|||
if raise_function:
|
||||
return 1234
|
||||
else:
|
||||
return False
|
||||
return False # pragma: no cover # Return false to keep other transitions working
|
||||
|
||||
storage.collect()
|
||||
self.assertIn(ValidImplementationNoEffect, storage.list)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class GeneralStatus(StatusCode):
|
|||
|
||||
def GHI(self): # This should be ignored
|
||||
"""A invalid function."""
|
||||
pass
|
||||
...
|
||||
|
||||
|
||||
class GeneralStateTest(InvenTreeTestCase):
|
||||
|
|
|
|||
|
|
@ -64,11 +64,6 @@ class LabelTest(InvenTreeAPITestCase):
|
|||
response = self.get(url, {'enabled': False})
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
# Disable each report
|
||||
for label in labels:
|
||||
label.enabled = False
|
||||
label.save()
|
||||
|
||||
# Filter by "enabled" status
|
||||
response = self.get(url, {'enabled': True})
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -143,7 +143,7 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
|||
for error in errors_msgs:
|
||||
if re.match(pattern, error):
|
||||
break
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
errors_str = '\n'.join([f'- {e}' for e in errors_msgs])
|
||||
self.fail(
|
||||
f"""Error message matching pattern '{pattern}' not found in machine registry errors:\n{errors_str}"""
|
||||
|
|
|
|||
|
|
@ -272,15 +272,21 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase
|
|||
self.print_labels.assert_called_once()
|
||||
self.assertEqual(self.print_labels.call_args.args[0], self.machine.machine)
|
||||
self.assertEqual(self.print_labels.call_args.args[1], label)
|
||||
self.assertQuerySetEqual(
|
||||
self.print_labels.call_args.args[2], parts, transform=lambda x: x
|
||||
)
|
||||
|
||||
# TODO re-activate test
|
||||
# self.assertQuerySetEqual(
|
||||
# self.print_labels.call_args.args[2], parts, transform=lambda x: x
|
||||
# )
|
||||
|
||||
self.assertIn('printing_options', self.print_labels.call_args.kwargs)
|
||||
self.assertEqual(
|
||||
self.print_labels.call_args.kwargs['printing_options'],
|
||||
{'copies': 1, 'test_option': 2},
|
||||
)
|
||||
|
||||
return
|
||||
# TODO re-activate test
|
||||
|
||||
# test the single print label method calls
|
||||
self.assertEqual(self.print_label.call_count, 2)
|
||||
self.assertEqual(self.print_label.call_args.args[0], self.machine.machine)
|
||||
|
|
|
|||
|
|
@ -154,11 +154,11 @@ class LineItemFilter(rest_filters.FilterSet):
|
|||
|
||||
# Filter by order status
|
||||
order_status = rest_filters.NumberFilter(
|
||||
label='order_status', field_name='order__status'
|
||||
label=_('Order Status'), field_name='order__status'
|
||||
)
|
||||
|
||||
has_pricing = rest_filters.BooleanFilter(
|
||||
label='Has Pricing', method='filter_has_pricing'
|
||||
label=_('Has Pricing'), method='filter_has_pricing'
|
||||
)
|
||||
|
||||
def filter_has_pricing(self, queryset, name, value):
|
||||
|
|
@ -425,9 +425,38 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
|||
|
||||
price_field = 'purchase_price'
|
||||
model = models.PurchaseOrderLineItem
|
||||
fields = ['order', 'part']
|
||||
fields = []
|
||||
|
||||
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
|
||||
order = rest_filters.ModelChoiceFilter(
|
||||
queryset=models.PurchaseOrder.objects.all(),
|
||||
field_name='order',
|
||||
label=_('Order'),
|
||||
)
|
||||
|
||||
order_complete = rest_filters.BooleanFilter(
|
||||
label=_('Order Complete'), method='filter_order_complete'
|
||||
)
|
||||
|
||||
def filter_order_complete(self, queryset, name, value):
|
||||
"""Filter by whether the order is 'complete' or not."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(order__status=PurchaseOrderStatus.COMPLETE.value)
|
||||
|
||||
return queryset.exclude(order__status=PurchaseOrderStatus.COMPLETE.value)
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=SupplierPart.objects.all(), field_name='part', label=_('Supplier Part')
|
||||
)
|
||||
|
||||
base_part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.filter(purchaseable=True),
|
||||
field_name='part__part',
|
||||
label=_('Internal Part'),
|
||||
)
|
||||
|
||||
pending = rest_filters.BooleanFilter(
|
||||
method='filter_pending', label=_('Order Pending')
|
||||
)
|
||||
|
||||
def filter_pending(self, queryset, name, value):
|
||||
"""Filter by "pending" status (order status = pending)."""
|
||||
|
|
@ -435,7 +464,9 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
|||
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||
return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||
|
||||
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
||||
received = rest_filters.BooleanFilter(
|
||||
label=_('Items Received'), method='filter_received'
|
||||
)
|
||||
|
||||
def filter_received(self, queryset, name, value):
|
||||
"""Filter by lines which are "received" (or "not" received).
|
||||
|
|
@ -542,25 +573,6 @@ class PurchaseOrderLineItemList(
|
|||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Additional filtering options."""
|
||||
params = self.request.query_params
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
base_part = params.get('base_part', None)
|
||||
|
||||
if base_part:
|
||||
try:
|
||||
base_part = Part.objects.get(pk=base_part)
|
||||
|
||||
queryset = queryset.filter(part__part=base_part)
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the requested queryset as a file."""
|
||||
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
|
||||
|
|
@ -577,6 +589,8 @@ class PurchaseOrderLineItemList(
|
|||
'MPN': 'part__manufacturer_part__MPN',
|
||||
'SKU': 'part__SKU',
|
||||
'part_name': 'part__part__name',
|
||||
'order': 'order__reference',
|
||||
'complete_date': 'order__complete_date',
|
||||
}
|
||||
|
||||
ordering_fields = [
|
||||
|
|
@ -589,6 +603,8 @@ class PurchaseOrderLineItemList(
|
|||
'SKU',
|
||||
'total_price',
|
||||
'target_date',
|
||||
'order',
|
||||
'complete_date',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
|
|
@ -791,7 +807,15 @@ class SalesOrderLineItemFilter(LineItemFilter):
|
|||
|
||||
price_field = 'sale_price'
|
||||
model = models.SalesOrderLineItem
|
||||
fields = ['order', 'part']
|
||||
fields = []
|
||||
|
||||
order = rest_filters.ModelChoiceFilter(
|
||||
queryset=models.SalesOrder.objects.all(), field_name='order', label=_('Order')
|
||||
)
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), field_name='part', label=_('Part')
|
||||
)
|
||||
|
||||
completed = rest_filters.BooleanFilter(label='completed', method='filter_completed')
|
||||
|
||||
|
|
@ -806,6 +830,17 @@ class SalesOrderLineItemFilter(LineItemFilter):
|
|||
return queryset.filter(q)
|
||||
return queryset.exclude(q)
|
||||
|
||||
order_complete = rest_filters.BooleanFilter(
|
||||
label=_('Order Complete'), method='filter_order_complete'
|
||||
)
|
||||
|
||||
def filter_order_complete(self, queryset, name, value):
|
||||
"""Filter by whether the order is 'complete' or not."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(order__status__in=SalesOrderStatusGroups.COMPLETE)
|
||||
|
||||
return queryset.exclude(order__status__in=SalesOrderStatusGroups.COMPLETE)
|
||||
|
||||
|
||||
class SalesOrderLineItemMixin:
|
||||
"""Mixin class for SalesOrderLineItem endpoints."""
|
||||
|
|
@ -862,9 +897,24 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea
|
|||
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
||||
ordering_fields = ['part__name', 'quantity', 'reference', 'target_date']
|
||||
ordering_fields = [
|
||||
'customer',
|
||||
'order',
|
||||
'part',
|
||||
'part__name',
|
||||
'quantity',
|
||||
'reference',
|
||||
'sale_price',
|
||||
'target_date',
|
||||
]
|
||||
|
||||
ordering_field_aliases = {
|
||||
'customer': 'order__customer__name',
|
||||
'part': 'part__name',
|
||||
'order': 'order__reference',
|
||||
}
|
||||
|
||||
search_fields = ['part__name', 'quantity', 'reference']
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ from InvenTree.helpers import (
|
|||
is_ajax,
|
||||
isNull,
|
||||
str2bool,
|
||||
str2int,
|
||||
)
|
||||
from InvenTree.mixins import (
|
||||
CreateAPI,
|
||||
|
|
@ -386,9 +385,10 @@ class PartSalePriceList(ListCreateAPI):
|
|||
queryset = PartSellPriceBreak.objects.all()
|
||||
serializer_class = part_serializers.PartSalePriceSerializer
|
||||
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
filterset_fields = ['part']
|
||||
ordering_fields = ['quantity', 'price']
|
||||
ordering = 'quantity'
|
||||
|
||||
|
||||
class PartInternalPriceDetail(RetrieveUpdateDestroyAPI):
|
||||
|
|
@ -405,9 +405,10 @@ class PartInternalPriceList(ListCreateAPI):
|
|||
serializer_class = part_serializers.PartInternalPriceSerializer
|
||||
permission_required = 'roles.sales_order.show'
|
||||
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
filterset_fields = ['part']
|
||||
ordering_fields = ['quantity', 'price']
|
||||
ordering = 'quantity'
|
||||
|
||||
|
||||
class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
|
||||
|
|
@ -1407,8 +1408,17 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||
'category',
|
||||
'last_stocktake',
|
||||
'units',
|
||||
'pricing_min',
|
||||
'pricing_max',
|
||||
'pricing_updated',
|
||||
]
|
||||
|
||||
ordering_field_aliases = {
|
||||
'pricing_min': 'pricing_data__overall_min',
|
||||
'pricing_max': 'pricing_data__overall_max',
|
||||
'pricing_updated': 'pricing_data__updated',
|
||||
}
|
||||
|
||||
# Default ordering
|
||||
ordering = 'name'
|
||||
|
||||
|
|
@ -1939,9 +1949,19 @@ class BomList(BomMixin, ListCreateDestroyAPIView):
|
|||
'inherited',
|
||||
'optional',
|
||||
'consumable',
|
||||
'pricing_min',
|
||||
'pricing_max',
|
||||
'pricing_min_total',
|
||||
'pricing_max_total',
|
||||
'pricing_updated',
|
||||
]
|
||||
|
||||
ordering_field_aliases = {'sub_part': 'sub_part__name'}
|
||||
ordering_field_aliases = {
|
||||
'sub_part': 'sub_part__name',
|
||||
'pricing_min': 'sub_part__pricing_data__overall_min',
|
||||
'pricing_max': 'sub_part__pricing_data__overall_max',
|
||||
'pricing_updated': 'sub_part__pricing_data__updated',
|
||||
}
|
||||
|
||||
|
||||
class BomDetail(BomMixin, RetrieveUpdateDestroyAPI):
|
||||
|
|
|
|||
|
|
@ -616,6 +616,7 @@ class PartSerializer(
|
|||
'virtual',
|
||||
'pricing_min',
|
||||
'pricing_max',
|
||||
'pricing_updated',
|
||||
'responsible',
|
||||
# Annotated fields
|
||||
'allocated_to_build_orders',
|
||||
|
|
@ -678,6 +679,7 @@ class PartSerializer(
|
|||
if not pricing:
|
||||
self.fields.pop('pricing_min')
|
||||
self.fields.pop('pricing_max')
|
||||
self.fields.pop('pricing_updated')
|
||||
|
||||
def get_api_url(self):
|
||||
"""Return the API url associated with this serializer."""
|
||||
|
|
@ -843,6 +845,9 @@ class PartSerializer(
|
|||
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='pricing_data.overall_max', allow_null=True, read_only=True
|
||||
)
|
||||
pricing_updated = serializers.DateTimeField(
|
||||
source='pricing_data.updated', allow_null=True, read_only=True
|
||||
)
|
||||
|
||||
parameters = PartParameterSerializer(many=True, read_only=True)
|
||||
|
||||
|
|
@ -1413,6 +1418,9 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||
'part_detail',
|
||||
'pricing_min',
|
||||
'pricing_max',
|
||||
'pricing_min_total',
|
||||
'pricing_max_total',
|
||||
'pricing_updated',
|
||||
'quantity',
|
||||
'reference',
|
||||
'sub_part',
|
||||
|
|
@ -1451,6 +1459,9 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||
if not pricing:
|
||||
self.fields.pop('pricing_min')
|
||||
self.fields.pop('pricing_max')
|
||||
self.fields.pop('pricing_min_total')
|
||||
self.fields.pop('pricing_max_total')
|
||||
self.fields.pop('pricing_updated')
|
||||
|
||||
quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
|
||||
|
||||
|
|
@ -1481,10 +1492,22 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||
|
||||
# Cached pricing fields
|
||||
pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing.overall_min', allow_null=True, read_only=True
|
||||
source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True
|
||||
)
|
||||
|
||||
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
source='sub_part.pricing.overall_max', allow_null=True, read_only=True
|
||||
source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True
|
||||
)
|
||||
|
||||
pricing_min_total = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
allow_null=True, read_only=True
|
||||
)
|
||||
pricing_max_total = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||
allow_null=True, read_only=True
|
||||
)
|
||||
|
||||
pricing_updated = serializers.DateTimeField(
|
||||
source='sub_part.pricing_data.updated', allow_null=True, read_only=True
|
||||
)
|
||||
|
||||
# Annotated fields for available stock
|
||||
|
|
@ -1504,6 +1527,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||
|
||||
queryset = queryset.prefetch_related('sub_part')
|
||||
queryset = queryset.prefetch_related('sub_part__category')
|
||||
queryset = queryset.prefetch_related('sub_part__pricing_data')
|
||||
|
||||
queryset = queryset.prefetch_related(
|
||||
'sub_part__stock_items',
|
||||
|
|
@ -1531,6 +1555,18 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|||
available_stock = total_stock - build_order_allocations - sales_order_allocations
|
||||
"""
|
||||
|
||||
# Annotate with the 'total pricing' information based on unit pricing and quantity
|
||||
queryset = queryset.annotate(
|
||||
pricing_min_total=ExpressionWrapper(
|
||||
F('quantity') * F('sub_part__pricing_data__overall_min'),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
pricing_max_total=ExpressionWrapper(
|
||||
F('quantity') * F('sub_part__pricing_data__overall_max'),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
ref = 'sub_part__'
|
||||
|
||||
# Annotate with the total "on order" amount for the sub-part
|
||||
|
|
|
|||
|
|
@ -62,16 +62,19 @@ class SettingsMixin:
|
|||
"""Does this plugin use custom global settings."""
|
||||
return bool(self.settings)
|
||||
|
||||
def get_setting(self, key, cache=False):
|
||||
def get_setting(self, key, cache=False, backup_value=None):
|
||||
"""Return the 'value' of the setting associated with this plugin.
|
||||
|
||||
Arguments:
|
||||
key: The 'name' of the setting value to be retrieved
|
||||
cache: Whether to use RAM cached value (default = False)
|
||||
backup_value: A backup value to return if the setting is not found
|
||||
"""
|
||||
from plugin.models import PluginSetting
|
||||
|
||||
return PluginSetting.get_setting(key, plugin=self.plugin_config(), cache=cache)
|
||||
return PluginSetting.get_setting(
|
||||
key, plugin=self.plugin_config(), cache=cache, backup_value=backup_value
|
||||
)
|
||||
|
||||
def set_setting(self, key, value, user=None):
|
||||
"""Set plugin setting value by key."""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ from stock.models import StockItem, StockLocation
|
|||
class LocatePluginTests(InvenTreeAPITestCase):
|
||||
"""Tests for LocateMixin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case."""
|
||||
super().setUp()
|
||||
|
||||
# Activate plugin
|
||||
config = registry.get_plugin('samplelocate').plugin_config()
|
||||
config.active = True
|
||||
config.save()
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
def test_installed(self):
|
||||
|
|
|
|||
|
|
@ -554,9 +554,6 @@ class StockItemListTest(StockAPITestCase):
|
|||
)
|
||||
self.assertTrue(len(response.data) < StockItem.objects.count())
|
||||
|
||||
for result in response.data:
|
||||
self.assertIsNone(result['location'])
|
||||
|
||||
# Filter with "cascade=True"
|
||||
response = self.get(
|
||||
self.list_url, {'location': 'null', 'cascade': True}, expected_code=200
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class TestSerialNumberMigration(MigratorTestCase):
|
|||
big_ref_item.serial,
|
||||
'9999999999999999999999999999999999999999999999999999999999999',
|
||||
)
|
||||
|
||||
self.assertEqual(big_ref_item.serial_int, 0x7FFFFFFF)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ function renderStockItem(data, parameters={}) {
|
|||
}
|
||||
|
||||
if (data.quantity == 0) {
|
||||
stock_detail = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock"% }</span>`;
|
||||
stock_detail = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||
} else {
|
||||
if (data.serial && data.quantity == 1) {
|
||||
stock_detail = `{% trans "Serial Number" %}: ${data.serial}`;
|
||||
|
|
|
|||
|
|
@ -488,7 +488,7 @@ function receiveReturnOrderItems(order_id, line_items, options={}) {
|
|||
|
||||
if (line_items.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Line Items"% }',
|
||||
'{% trans "Select Line Items" %}',
|
||||
'{% trans "At least one line item must be selected" %}'
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ class TemplateTagTest(InvenTreeTestCase):
|
|||
def test_spa_bundle(self):
|
||||
"""Test the 'spa_bundle' template tag."""
|
||||
resp = spa_helper.spa_bundle()
|
||||
if not resp:
|
||||
# No Vite, no test
|
||||
# TODO: Add a test for the non-Vite case (docker)
|
||||
return # pragma: no cover
|
||||
|
||||
shipped_js = resp.split('<script type="module" src="')[1:]
|
||||
self.assertTrue(len(shipped_js) > 0)
|
||||
self.assertTrue(len(shipped_js) == 3)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "InvenTree",
|
||||
"name": "backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -95,9 +95,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw=="
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
|
||||
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
|
|
@ -472,9 +472,9 @@
|
|||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
|
|
@ -506,11 +506,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
|
||||
"dependencies": {
|
||||
"flatted": "^3.1.0",
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.3",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -518,9 +519,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
|
||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
|
|
@ -672,6 +673,11 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
|
|
@ -682,6 +688,14 @@
|
|||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
-c requirements.txt
|
||||
coverage[toml] # Unit test coverage
|
||||
coveralls==2.1.2 # Coveralls linking (for tracking coverage) # PINNED 2022-06-28 - Old version needed for correct upload
|
||||
django-admin-shell # Remote shell access
|
||||
django-querycount # Display number of URL queries for requests
|
||||
django-slowtests # Show which unit tests are running slowly
|
||||
django-test-migrations # Unit testing for database migrations
|
||||
isort # python import sorting
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt --python-version=3.9 --no-strip-extras
|
||||
asgiref==3.8.0
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
build==1.1.1
|
||||
build==1.2.1
|
||||
# via pip-tools
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
|
|
@ -24,18 +24,22 @@ cryptography==42.0.5
|
|||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
django==4.2.11
|
||||
# via django-slowtests
|
||||
# via
|
||||
# django-admin-shell
|
||||
# django-slowtests
|
||||
django-admin-shell==2.0.1
|
||||
django-querycount==0.8.3
|
||||
django-slowtests==1.1.1
|
||||
django-test-migrations==1.3.0
|
||||
docopt==0.6.2
|
||||
# via coveralls
|
||||
filelock==3.13.1
|
||||
filelock==3.13.3
|
||||
# via virtualenv
|
||||
identify==2.5.35
|
||||
# via pre-commit
|
||||
idna==3.6
|
||||
# via requests
|
||||
importlib-metadata==6.11.0
|
||||
importlib-metadata==7.0.0
|
||||
# via build
|
||||
isort==5.13.2
|
||||
nodeenv==1.8.0
|
||||
|
|
@ -49,7 +53,7 @@ pip-tools==7.4.1
|
|||
platformdirs==4.2.0
|
||||
# via virtualenv
|
||||
pre-commit==3.7.0
|
||||
pycparser==2.21
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pyproject-hooks==1.0.0
|
||||
# via
|
||||
|
|
@ -72,7 +76,7 @@ tomli==2.0.1
|
|||
# build
|
||||
# pip-tools
|
||||
# pyproject-hooks
|
||||
typing-extensions==4.10.0
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# asgiref
|
||||
# django-test-migrations
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ django-maintenance-mode # Shut down application while reloading
|
|||
django-markdownify # Markdown rendering
|
||||
django-mptt # Modified Preorder Tree Traversal
|
||||
django-markdownify # Markdown rendering
|
||||
django-money>=3.0.0,<3.5.0 # Django app for currency management # FIXED 2023-10-31 3.3.0 breaks due to https://github.com/django-money/django-money/issues/731
|
||||
django-money>=3.0.0,<3.3.0 # Django app for currency management # FIXED 2023-10-31 3.3.0 breaks due to https://github.com/django-money/django-money/issues/731
|
||||
django-mptt # Modified Preorder Tree Traversal
|
||||
django-redis>=5.0.0 # Redis integration
|
||||
django-q2 # Background task scheduling
|
||||
|
|
@ -41,6 +41,7 @@ gunicorn # Gunicorn web server
|
|||
pdf2image # PDF to image conversion
|
||||
pillow # Image manipulation
|
||||
pint==0.21 # Unit conversion # FIXED 2023-05-30 breaks tests https://github.com/matmair/InvenTree/actions/runs/5095665936/jobs/9160852560
|
||||
pip-licenses # License information for installed packages
|
||||
python-barcode[images] # Barcode generator
|
||||
python-dotenv # Environment variable management
|
||||
pyyaml>=6.0.1 # YAML parsing
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt --python-version=3.9 --no-strip-extras
|
||||
asgiref==3.8.0
|
||||
asgiref==3.8.1
|
||||
# via
|
||||
# django
|
||||
# django-cors-headers
|
||||
|
|
@ -87,7 +87,7 @@ django-cors-headers==4.3.1
|
|||
django-crispy-forms==1.14.0
|
||||
django-dbbackup==4.1.0
|
||||
django-error-report-2==0.4.2
|
||||
django-filter==24.1
|
||||
django-filter==24.2
|
||||
django-flags==5.0.13
|
||||
django-formtools==2.5.1
|
||||
django-ical==1.9.2
|
||||
|
|
@ -96,11 +96,11 @@ django-js-asset==2.2.0
|
|||
# via django-mptt
|
||||
django-maintenance-mode==0.21.1
|
||||
django-markdownify==0.9.3
|
||||
django-money==3.4.1
|
||||
django-money==3.2.0
|
||||
django-mptt==0.16.0
|
||||
django-otp==1.3.0
|
||||
# via django-allauth-2fa
|
||||
django-picklefield==3.1
|
||||
django-picklefield==3.2
|
||||
# via django-q2
|
||||
django-q-sentry==0.1.6
|
||||
django-q2==1.6.2
|
||||
|
|
@ -121,12 +121,12 @@ djangorestframework==3.14.0
|
|||
# djangorestframework-simplejwt
|
||||
# drf-spectacular
|
||||
djangorestframework-simplejwt[crypto]==5.3.1
|
||||
drf-spectacular==0.27.1
|
||||
drf-spectacular==0.27.2
|
||||
dulwich==0.21.7
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
feedparser==6.0.11
|
||||
fonttools[woff]==4.50.0
|
||||
fonttools[woff]==4.51.0
|
||||
# via weasyprint
|
||||
googleapis-common-protos==1.63.0
|
||||
# via
|
||||
|
|
@ -141,7 +141,7 @@ icalendar==5.0.12
|
|||
# via django-ical
|
||||
idna==3.6
|
||||
# via requests
|
||||
importlib-metadata==6.11.0
|
||||
importlib-metadata==7.0.0
|
||||
# via
|
||||
# django-q2
|
||||
# markdown
|
||||
|
|
@ -168,7 +168,7 @@ odfpy==1.4.1
|
|||
# via tablib
|
||||
openpyxl==3.1.2
|
||||
# via tablib
|
||||
opentelemetry-api==1.23.0
|
||||
opentelemetry-api==1.24.0
|
||||
# via
|
||||
# opentelemetry-exporter-otlp-proto-grpc
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
|
|
@ -178,43 +178,43 @@ opentelemetry-api==1.23.0
|
|||
# opentelemetry-instrumentation-requests
|
||||
# opentelemetry-instrumentation-wsgi
|
||||
# opentelemetry-sdk
|
||||
opentelemetry-exporter-otlp==1.23.0
|
||||
opentelemetry-exporter-otlp-proto-common==1.23.0
|
||||
opentelemetry-exporter-otlp==1.24.0
|
||||
opentelemetry-exporter-otlp-proto-common==1.24.0
|
||||
# via
|
||||
# opentelemetry-exporter-otlp-proto-grpc
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.23.0
|
||||
opentelemetry-exporter-otlp-proto-grpc==1.24.0
|
||||
# via opentelemetry-exporter-otlp
|
||||
opentelemetry-exporter-otlp-proto-http==1.23.0
|
||||
opentelemetry-exporter-otlp-proto-http==1.24.0
|
||||
# via opentelemetry-exporter-otlp
|
||||
opentelemetry-instrumentation==0.44b0
|
||||
opentelemetry-instrumentation==0.45b0
|
||||
# via
|
||||
# opentelemetry-instrumentation-django
|
||||
# opentelemetry-instrumentation-redis
|
||||
# opentelemetry-instrumentation-requests
|
||||
# opentelemetry-instrumentation-wsgi
|
||||
opentelemetry-instrumentation-django==0.44b0
|
||||
opentelemetry-instrumentation-redis==0.44b0
|
||||
opentelemetry-instrumentation-requests==0.44b0
|
||||
opentelemetry-instrumentation-wsgi==0.44b0
|
||||
opentelemetry-instrumentation-django==0.45b0
|
||||
opentelemetry-instrumentation-redis==0.45b0
|
||||
opentelemetry-instrumentation-requests==0.45b0
|
||||
opentelemetry-instrumentation-wsgi==0.45b0
|
||||
# via opentelemetry-instrumentation-django
|
||||
opentelemetry-proto==1.23.0
|
||||
opentelemetry-proto==1.24.0
|
||||
# via
|
||||
# opentelemetry-exporter-otlp-proto-common
|
||||
# opentelemetry-exporter-otlp-proto-grpc
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
opentelemetry-sdk==1.23.0
|
||||
opentelemetry-sdk==1.24.0
|
||||
# via
|
||||
# opentelemetry-exporter-otlp-proto-grpc
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
opentelemetry-semantic-conventions==0.44b0
|
||||
opentelemetry-semantic-conventions==0.45b0
|
||||
# via
|
||||
# opentelemetry-instrumentation-django
|
||||
# opentelemetry-instrumentation-redis
|
||||
# opentelemetry-instrumentation-requests
|
||||
# opentelemetry-instrumentation-wsgi
|
||||
# opentelemetry-sdk
|
||||
opentelemetry-util-http==0.44b0
|
||||
opentelemetry-util-http==0.45b0
|
||||
# via
|
||||
# opentelemetry-instrumentation-django
|
||||
# opentelemetry-instrumentation-requests
|
||||
|
|
@ -222,7 +222,7 @@ opentelemetry-util-http==0.44b0
|
|||
packaging==24.0
|
||||
# via gunicorn
|
||||
pdf2image==1.17.0
|
||||
pillow==10.2.0
|
||||
pillow==10.3.0
|
||||
# via
|
||||
# django-stdimage
|
||||
# pdf2image
|
||||
|
|
@ -230,13 +230,16 @@ pillow==10.2.0
|
|||
# qrcode
|
||||
# weasyprint
|
||||
pint==0.21
|
||||
pip-licenses==4.4.0
|
||||
prettytable==3.10.0
|
||||
# via pip-licenses
|
||||
protobuf==4.25.3
|
||||
# via
|
||||
# googleapis-common-protos
|
||||
# opentelemetry-proto
|
||||
py-moneyed==3.0
|
||||
# via django-money
|
||||
pycparser==2.21
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydyf==0.9.0
|
||||
# via weasyprint
|
||||
|
|
@ -269,7 +272,7 @@ pyyaml==6.0.1
|
|||
# tablib
|
||||
qrcode[pil]==7.4.2
|
||||
# via django-allauth-2fa
|
||||
rapidfuzz==3.7.0
|
||||
rapidfuzz==3.8.1
|
||||
redis==5.0.3
|
||||
# via django-redis
|
||||
referencing==0.34.0
|
||||
|
|
@ -283,13 +286,13 @@ requests==2.31.0
|
|||
# django-allauth
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
# requests-oauthlib
|
||||
requests-oauthlib==1.4.0
|
||||
requests-oauthlib==2.0.0
|
||||
# via django-allauth
|
||||
rpds-py==0.18.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
sentry-sdk==1.43.0
|
||||
sentry-sdk==1.44.1
|
||||
# via django-q-sentry
|
||||
setuptools==69.2.0
|
||||
# via
|
||||
|
|
@ -313,7 +316,7 @@ tinycss2==1.2.1
|
|||
# bleach
|
||||
# cssselect2
|
||||
# weasyprint
|
||||
typing-extensions==4.10.0
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# asgiref
|
||||
# drf-spectacular
|
||||
|
|
@ -329,6 +332,8 @@ urllib3==2.2.1
|
|||
# dulwich
|
||||
# requests
|
||||
# sentry-sdk
|
||||
wcwidth==0.2.13
|
||||
# via prettytable
|
||||
weasyprint==61.2
|
||||
# via django-weasyprint
|
||||
webencodings==0.5.1
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@lingui/core": "^4.7.1",
|
||||
"@lingui/react": "^4.7.2",
|
||||
"@lingui/react": "^4.10.0",
|
||||
"@mantine/carousel": "<7",
|
||||
"@mantine/core": "<7",
|
||||
"@mantine/dates": "<7",
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"@mantine/hooks": "<7",
|
||||
"@mantine/modals": "<7",
|
||||
"@mantine/notifications": "<7",
|
||||
"@mantine/spotlight": "<7",
|
||||
"@naisutech/react-tree": "^3.1.0",
|
||||
"@sentry/react": "^7.109.0",
|
||||
"@tabler/icons-react": "^3.1.0",
|
||||
|
|
@ -37,17 +38,18 @@
|
|||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"easymde": "^2.18.0",
|
||||
"embla-carousel-react": "^8.0.0",
|
||||
"embla-carousel-react": "^8.0.2",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"mantine-datatable": "<7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-is": "^18.2.0",
|
||||
"react-router-dom": "^6.22.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"recharts": "^2.12.4",
|
||||
"styled-components": "^5.3.6",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
|
|
@ -56,8 +58,8 @@
|
|||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@lingui/cli": "^4.7.2",
|
||||
"@lingui/macro": "^4.7.2",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@lingui/macro": "^4.10.0",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@types/node": "^20.12.3",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
|
|
@ -66,6 +68,7 @@
|
|||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"nyc": "^15.1.0",
|
||||
"rollup-plugin-license": "^3.3.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.7",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default defineConfig({
|
|||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? 2 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { spotlight } from '@mantine/spotlight';
|
||||
import { IconCommand } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* A button which opens the quick command modal
|
||||
*/
|
||||
export function SpotlightButton() {
|
||||
return (
|
||||
<ActionIcon onClick={() => spotlight.open()} title={t`Open spotlight`}>
|
||||
<IconCommand />
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export const CHART_COLORS: string[] = [
|
||||
'#ffa8a8',
|
||||
'#8ce99a',
|
||||
'#74c0fc',
|
||||
'#ffe066',
|
||||
'#63e6be',
|
||||
'#ffc078',
|
||||
'#d8f5a2',
|
||||
'#66d9e8',
|
||||
'#e599f7',
|
||||
'#dee2e6'
|
||||
];
|
||||
|
|
@ -23,9 +23,9 @@ import { getDetailUrl } from '../../functions/urls';
|
|||
import { base_url } from '../../main';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { YesNoButton } from '../buttons/YesNoButton';
|
||||
import { ProgressBar } from '../items/ProgressBar';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { YesNoButton } from '../items/YesNoButton';
|
||||
import { getModelInfo } from '../render/ModelType';
|
||||
import { StatusRenderer } from '../render/StatusRenderer';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { Badge } from '@mantine/core';
|
||||
|
||||
export type DetailsBadgeProps = {
|
||||
color: string;
|
||||
label: string;
|
||||
size?: string;
|
||||
visible?: boolean;
|
||||
key?: any;
|
||||
};
|
||||
|
||||
export default function DetailsBadge(props: DetailsBadgeProps) {
|
||||
if (props.visible == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={props.key}
|
||||
color={props.color}
|
||||
variant="filled"
|
||||
size={props.size ?? 'lg'}
|
||||
>
|
||||
{props.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ export interface ApiFormProps {
|
|||
pathParams?: PathParams;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
fields?: ApiFormFieldSet;
|
||||
focus?: string;
|
||||
initialData?: FieldValues;
|
||||
submitText?: string;
|
||||
submitColor?: string;
|
||||
|
|
@ -292,7 +293,48 @@ export function ApiForm({
|
|||
});
|
||||
initialDataQuery.refetch();
|
||||
}
|
||||
}, []);
|
||||
}, [props.fetchInitialData]);
|
||||
|
||||
const isLoading = useMemo(
|
||||
() =>
|
||||
isFormLoading ||
|
||||
initialDataQuery.isFetching ||
|
||||
optionsLoading ||
|
||||
isSubmitting ||
|
||||
!props.fields,
|
||||
[
|
||||
isFormLoading,
|
||||
initialDataQuery.isFetching,
|
||||
isSubmitting,
|
||||
props.fields,
|
||||
optionsLoading
|
||||
]
|
||||
);
|
||||
|
||||
const [initialFocus, setInitialFocus] = useState<string>('');
|
||||
|
||||
// Update field focus when the form is loaded
|
||||
useEffect(() => {
|
||||
let focusField = props.focus ?? '';
|
||||
|
||||
if (!focusField) {
|
||||
// If a focus field is not specified, then focus on the first available field
|
||||
Object.entries(props.fields ?? {}).forEach(([fieldName, field]) => {
|
||||
if (focusField || field.read_only || field.disabled || field.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusField = fieldName;
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading || initialFocus == focusField) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFocus(focusField);
|
||||
setInitialFocus(focusField);
|
||||
}, [props.focus, props.fields, form.setFocus, isLoading, initialFocus]);
|
||||
|
||||
const submitForm: SubmitHandler<FieldValues> = async (data) => {
|
||||
setNonFieldErrors([]);
|
||||
|
|
@ -392,22 +434,6 @@ export function ApiForm({
|
|||
});
|
||||
};
|
||||
|
||||
const isLoading = useMemo(
|
||||
() =>
|
||||
isFormLoading ||
|
||||
initialDataQuery.isFetching ||
|
||||
optionsLoading ||
|
||||
isSubmitting ||
|
||||
!props.fields,
|
||||
[
|
||||
isFormLoading,
|
||||
initialDataQuery.isFetching,
|
||||
isSubmitting,
|
||||
props.fields,
|
||||
optionsLoading
|
||||
]
|
||||
);
|
||||
|
||||
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
|
||||
props.onFormError?.();
|
||||
}, [props.onFormError]);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { IconCheck } from '@tabler/icons-react';
|
|||
import { useServerApiState } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { HostList } from '../../states/states';
|
||||
import { EditButton } from '../items/EditButton';
|
||||
import { EditButton } from '../buttons/EditButton';
|
||||
import { HostOptionsForm } from './HostOptionsForm';
|
||||
|
||||
export function InstanceOptions({
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export function ApiFormField({
|
|||
return (
|
||||
<TextInput
|
||||
{...reducedDefinition}
|
||||
ref={ref}
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
type={definition.field_type}
|
||||
value={value || ''}
|
||||
|
|
@ -226,21 +226,14 @@ export function ApiFormField({
|
|||
<NumberInput
|
||||
{...reducedDefinition}
|
||||
radius="sm"
|
||||
ref={ref}
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
value={numericalValue}
|
||||
error={error?.message}
|
||||
formatter={(value) => {
|
||||
let v: any = parseFloat(value);
|
||||
|
||||
if (Number.isNaN(v) || !Number.isFinite(v)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${1 * v.toFixed()}`;
|
||||
}}
|
||||
precision={definition.field_type == 'integer' ? 0 : 10}
|
||||
onChange={(value: number) => onChange(value)}
|
||||
removeTrailingZeros
|
||||
step={1}
|
||||
/>
|
||||
);
|
||||
case 'choice':
|
||||
|
|
@ -256,7 +249,7 @@ export function ApiFormField({
|
|||
<FileInput
|
||||
{...reducedDefinition}
|
||||
id={fieldId}
|
||||
ref={ref}
|
||||
ref={field.ref}
|
||||
radius="sm"
|
||||
value={value}
|
||||
error={error?.message}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export default function DateField({
|
|||
<DateInput
|
||||
id={fieldId}
|
||||
radius="sm"
|
||||
ref={field.ref}
|
||||
type={undefined}
|
||||
error={error?.message}
|
||||
value={dateValue}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ export function RelatedModelField({
|
|||
<Select
|
||||
id={fieldId}
|
||||
value={currentValue}
|
||||
ref={field.ref}
|
||||
options={data}
|
||||
filterOption={null}
|
||||
onInputChange={(value: any) => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export function Thumbnail({
|
|||
src,
|
||||
alt = t`Thumbnail`,
|
||||
size = 20,
|
||||
link,
|
||||
text,
|
||||
align
|
||||
}: {
|
||||
|
|
@ -21,9 +22,22 @@ export function Thumbnail({
|
|||
size?: number;
|
||||
text?: ReactNode;
|
||||
align?: string;
|
||||
link?: string;
|
||||
}) {
|
||||
const backup_image = '/static/img/blank_image.png';
|
||||
|
||||
const inner = useMemo(() => {
|
||||
if (link) {
|
||||
return (
|
||||
<Anchor href={link} target="_blank">
|
||||
{text}
|
||||
</Anchor>
|
||||
);
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}, [link, text]);
|
||||
|
||||
return (
|
||||
<Group align={align ?? 'left'} spacing="xs" noWrap={true}>
|
||||
<ApiImage
|
||||
|
|
@ -39,7 +53,7 @@ export function Thumbnail({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
{text}
|
||||
{inner}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro';
|
|||
import { Code, Flex, Group, Text } from '@mantine/core';
|
||||
import { Link, To } from 'react-router-dom';
|
||||
|
||||
import { YesNoButton } from '../buttons/YesNoButton';
|
||||
import { DetailDrawerLink } from '../nav/DetailDrawer';
|
||||
import { YesNoButton } from './YesNoButton';
|
||||
|
||||
export function InfoItem({
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ export interface MenuLinkItem {
|
|||
docchildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
export type menuItemsCollection = {
|
||||
[key: string]: MenuLinkItem;
|
||||
};
|
||||
|
||||
function ConditionalDocTooltip({
|
||||
item,
|
||||
children
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
|||
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { CopyButton } from '../items/CopyButton';
|
||||
import { CopyButton } from '../buttons/CopyButton';
|
||||
|
||||
type AboutLookupRef = {
|
||||
ref: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Divider,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
|
||||
export function LicenceView(entries: Readonly<any[]>) {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Divider />
|
||||
{entries?.length > 0 ? (
|
||||
<Accordion variant="contained" defaultValue="-">
|
||||
{entries?.map((entry: any, index: number) => (
|
||||
<Accordion.Item key={entry.name} value={`entry-${index}`}>
|
||||
<Accordion.Control>
|
||||
<Group position="apart" grow>
|
||||
<Text>{entry.name}</Text>
|
||||
<Text>{entry.license}</Text>
|
||||
<Space />
|
||||
<Text>{entry.version}</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel style={{ whiteSpace: 'pre-line' }}>
|
||||
{entry.licensetext || t`No license text available`}
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>No Information provided - this is likely a server issue</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function LicenseModal() {
|
||||
const { data, isFetching, isError } = useQuery({
|
||||
queryKey: ['license'],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(apiUrl(ApiEndpoints.license))
|
||||
.then((res) => res.data ?? {})
|
||||
.catch(() => {})
|
||||
});
|
||||
|
||||
const rspdata = !data ? [] : Object.keys(data ?? {});
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Divider />
|
||||
<LoadingOverlay visible={isFetching} />
|
||||
{isFetching && (
|
||||
<Text>
|
||||
<Trans>Loading license information</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{isError ? (
|
||||
<Alert color="red" title={t`Error`}>
|
||||
<Text>
|
||||
<Trans>Failed to fetch license information</Trans>
|
||||
</Text>
|
||||
</Alert>
|
||||
) : (
|
||||
<Tabs defaultValue={rspdata[0] ?? ''}>
|
||||
<Tabs.List>
|
||||
{rspdata.map((key) => (
|
||||
<Tabs.Tab key={key} value={key}>
|
||||
<Trans>{key} Packages</Trans>
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
{rspdata.map((key) => (
|
||||
<Tabs.Panel key={key} value={key}>
|
||||
{LicenceView(data[key] ?? [])}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { ActionIcon, Container, Group, Indicator, Tabs } from '@mantine/core';
|
|||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconBell, IconSearch } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMatch, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
|
|
@ -10,7 +10,9 @@ import { navTabs as mainNavTabs } from '../../defaults/links';
|
|||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { ScanButton } from '../items/ScanButton';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { ScanButton } from '../buttons/ScanButton';
|
||||
import { SpotlightButton } from '../buttons/SpotlightButton';
|
||||
import { MainMenu } from './MainMenu';
|
||||
import { NavHoverMenu } from './NavHoverMenu';
|
||||
import { NavigationDrawer } from './NavigationDrawer';
|
||||
|
|
@ -19,8 +21,12 @@ import { SearchDrawer } from './SearchDrawer';
|
|||
|
||||
export function Header() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const [setNavigationOpen, navigationOpen] = useLocalState((state) => [
|
||||
state.setNavigationOpen,
|
||||
state.navigationOpen
|
||||
]);
|
||||
const [navDrawerOpened, { open: openNavDrawer, close: closeNavDrawer }] =
|
||||
useDisclosure(false);
|
||||
useDisclosure(navigationOpen);
|
||||
const [
|
||||
searchDrawerOpened,
|
||||
{ open: openSearchDrawer, close: closeSearchDrawer }
|
||||
|
|
@ -59,6 +65,18 @@ export function Header() {
|
|||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
// Sync Navigation Drawer state with zustand
|
||||
useEffect(() => {
|
||||
if (navigationOpen === navDrawerOpened) return;
|
||||
setNavigationOpen(navDrawerOpened);
|
||||
}, [navDrawerOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
if (navigationOpen === navDrawerOpened) return;
|
||||
if (navigationOpen) openNavDrawer();
|
||||
else closeNavDrawer();
|
||||
}, [navigationOpen]);
|
||||
|
||||
return (
|
||||
<div className={classes.layoutHeader}>
|
||||
<SearchDrawer opened={searchDrawerOpened} onClose={closeSearchDrawer} />
|
||||
|
|
@ -80,6 +98,7 @@ export function Header() {
|
|||
<ActionIcon onClick={openSearchDrawer}>
|
||||
<IconSearch />
|
||||
</ActionIcon>
|
||||
<SpotlightButton />
|
||||
<ScanButton />
|
||||
<ActionIcon onClick={openNotificationDrawer}>
|
||||
<Indicator
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue