mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
c0f3189041
@ -1,4 +1,5 @@
|
|||||||
# InvenTree environment variables for a development setup
|
# InvenTree environment variables for a development setup
|
||||||
|
# These variables will be used by the docker-compose.yml file
|
||||||
|
|
||||||
# Set DEBUG to True for a development setup
|
# Set DEBUG to True for a development setup
|
||||||
INVENTREE_DEBUG=True
|
INVENTREE_DEBUG=True
|
6
.github/CODEOWNERS
vendored
Normal file
6
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# General owner is the maintainers team
|
||||||
|
* @SchrodingersGat
|
||||||
|
|
||||||
|
# plugins are co-owned
|
||||||
|
/InvenTree/plugin/ @SchrodingersGat @matmair
|
||||||
|
/InvenTree/plugins/ @SchrodingersGat @matmair
|
17
.github/actions/migration/action.yaml
vendored
Normal file
17
.github/actions/migration/action.yaml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: 'Migration test'
|
||||||
|
description: 'Run migration test sequenze'
|
||||||
|
author: 'inventree'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Data Import Export
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
invoke migrate
|
||||||
|
invoke import-fixtures
|
||||||
|
invoke export-records -f data.json
|
||||||
|
python3 ./InvenTree/manage.py flush --noinput
|
||||||
|
invoke migrate
|
||||||
|
invoke import-records -f data.json
|
||||||
|
invoke import-records -f data.json
|
82
.github/actions/setup/action.yaml
vendored
Normal file
82
.github/actions/setup/action.yaml
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
name: 'Setup Enviroment'
|
||||||
|
description: 'Setup the enviroment for general InvenTree tests'
|
||||||
|
author: 'inventree'
|
||||||
|
inputs:
|
||||||
|
python:
|
||||||
|
required: false
|
||||||
|
description: 'Install python.'
|
||||||
|
default: 'true'
|
||||||
|
npm:
|
||||||
|
required: false
|
||||||
|
description: 'Install npm.'
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
install:
|
||||||
|
required: false
|
||||||
|
description: 'Install the InvenTree requirements?'
|
||||||
|
default: 'false'
|
||||||
|
update:
|
||||||
|
required: false
|
||||||
|
description: 'Should a full update cycle be run?'
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
apt-dependency:
|
||||||
|
required: false
|
||||||
|
description: 'Extra APT package for install.'
|
||||||
|
pip-dependency:
|
||||||
|
required: false
|
||||||
|
description: 'Extra python package for install.'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Python installs
|
||||||
|
- name: Set up Python ${{ env.python_version }}
|
||||||
|
if: ${{ inputs.python == 'true' }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.python_version }}
|
||||||
|
cache: pip
|
||||||
|
- name: Install Base Python Dependencies
|
||||||
|
if: ${{ inputs.python == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -U pip
|
||||||
|
pip3 install invoke wheel
|
||||||
|
- name: Install Specific Python Dependencies
|
||||||
|
if: ${{ inputs.pip-dependency }}
|
||||||
|
shell: bash
|
||||||
|
run: pip3 install ${{ inputs.pip-dependency }}
|
||||||
|
|
||||||
|
# NPM installs
|
||||||
|
- name: Install node.js ${{ env.node_version }}
|
||||||
|
if: ${{ inputs.npm == 'true' }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.node_version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- name: Intall npm packages
|
||||||
|
if: ${{ inputs.npm == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# OS installs
|
||||||
|
- name: Install OS Dependencies
|
||||||
|
if: ${{ inputs.apt-dependency }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install ${{ inputs.apt-dependency }}
|
||||||
|
|
||||||
|
# Invoke commands
|
||||||
|
- name: Run invoke install
|
||||||
|
if: ${{ inputs.install == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: invoke install
|
||||||
|
- name: Run invoke update
|
||||||
|
if: ${{ inputs.update == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: invoke update
|
69
.github/workflows/docker.yaml
vendored
Normal file
69
.github/workflows/docker.yaml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Build, test and push InvenTree docker image
|
||||||
|
# This workflow runs under any of the following conditions:
|
||||||
|
#
|
||||||
|
# - Push to the master branch
|
||||||
|
# - Push to the stable branch
|
||||||
|
# - Publish release
|
||||||
|
#
|
||||||
|
# The following actions are performed:
|
||||||
|
#
|
||||||
|
# - Check that the version number matches the current branch or tag
|
||||||
|
# - Build the InvenTree docker image
|
||||||
|
# - Run suite of unit tests against the build image
|
||||||
|
# - Push the compiled, tested image to dockerhub
|
||||||
|
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
- 'stable'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
# Build the docker image
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Version Check
|
||||||
|
run: |
|
||||||
|
python3 ci/check_version_number.py
|
||||||
|
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: |
|
||||||
|
docker-compose build
|
||||||
|
docker-compose run inventree-dev-server invoke update
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose run inventree-dev-server invoke wait
|
||||||
|
docker-compose run inventree-dev-server invoke test
|
||||||
|
docker-compose down
|
||||||
|
- name: Set up QEMU
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to Dockerhub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and Push
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
target: production
|
||||||
|
tags: inventree/inventree:${{ env.docker_tag }}
|
||||||
|
build-args: commit_hash=${{ env.git_commit_hash }},commit_date=${{ env.git_commit_date }},commit_tag=${{ env.docker_tag }}
|
51
.github/workflows/docker_latest.yaml
vendored
51
.github/workflows/docker_latest.yaml
vendored
@ -1,51 +0,0 @@
|
|||||||
# Build and push latest docker image on push to master branch
|
|
||||||
|
|
||||||
name: Docker Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Check version number
|
|
||||||
run: |
|
|
||||||
python3 ci/check_version_number.py --dev
|
|
||||||
- name: Build Docker Image
|
|
||||||
run: |
|
|
||||||
cd docker
|
|
||||||
docker-compose build
|
|
||||||
docker-compose run inventree-dev-server invoke update
|
|
||||||
- name: Run unit tests
|
|
||||||
run: |
|
|
||||||
cd docker
|
|
||||||
docker-compose up -d
|
|
||||||
docker-compose run inventree-dev-server invoke wait
|
|
||||||
docker-compose run inventree-dev-server invoke test
|
|
||||||
docker-compose down
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Login to Dockerhub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build and Push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./docker
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
push: true
|
|
||||||
target: production
|
|
||||||
tags: inventree/inventree:latest
|
|
||||||
- name: Image Digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
42
.github/workflows/docker_stable.yaml
vendored
42
.github/workflows/docker_stable.yaml
vendored
@ -1,42 +0,0 @@
|
|||||||
# Build and push docker image on push to 'stable' branch
|
|
||||||
# Docker build will be uploaded to dockerhub with the 'inventree:stable' tag
|
|
||||||
|
|
||||||
name: Docker Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'stable'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Check version number
|
|
||||||
run: |
|
|
||||||
python3 ci/check_version_number.py --release
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Login to Dockerhub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build and Push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./docker
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
push: true
|
|
||||||
target: production
|
|
||||||
build-args:
|
|
||||||
branch=stable
|
|
||||||
tags: inventree/inventree:stable
|
|
||||||
- name: Image Digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
38
.github/workflows/docker_tag.yaml
vendored
38
.github/workflows/docker_tag.yaml
vendored
@ -1,38 +0,0 @@
|
|||||||
# Publish docker images to dockerhub on a tagged release
|
|
||||||
# Docker build will be uploaded to dockerhub with the 'invetree:<tag>' tag
|
|
||||||
|
|
||||||
name: Docker Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish_image:
|
|
||||||
name: Push InvenTree web server image to dockerhub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Check Release tag
|
|
||||||
run: |
|
|
||||||
python3 ci/check_version_number.py --release --tag ${{ github.event.release.tag_name }}
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Login to Dockerhub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build and Push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./docker
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
push: true
|
|
||||||
target: production
|
|
||||||
build-args:
|
|
||||||
tag=${{ github.event.release.tag_name }}
|
|
||||||
tags: inventree/inventree:${{ github.event.release.tag_name }}
|
|
270
.github/workflows/qc_checks.yaml
vendored
270
.github/workflows/qc_checks.yaml
vendored
@ -14,120 +14,97 @@ on:
|
|||||||
env:
|
env:
|
||||||
python_version: 3.9
|
python_version: 3.9
|
||||||
node_version: 16
|
node_version: 16
|
||||||
|
# The OS version must be set per job
|
||||||
|
|
||||||
server_start_sleep: 60
|
server_start_sleep: 60
|
||||||
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
INVENTREE_DB_ENGINE: sqlite3
|
INVENTREE_DB_ENGINE: sqlite3
|
||||||
INVENTREE_DB_NAME: inventree
|
INVENTREE_DB_NAME: inventree
|
||||||
INVENTREE_MEDIA_ROOT: ./media
|
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
||||||
INVENTREE_STATIC_ROOT: ./static
|
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pep_style:
|
pep_style:
|
||||||
name: PEP style (python)
|
name: Style [Python]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Set up Python ${{ env.python_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.python_version }}
|
install: true
|
||||||
cache: 'pip'
|
- name: Run flake8
|
||||||
- name: Install deps
|
run: flake8 InvenTree --extend-ignore=D
|
||||||
run: |
|
|
||||||
pip install flake8==3.8.3
|
|
||||||
pip install pep8-naming==0.11.1
|
|
||||||
- name: flake8
|
|
||||||
run: |
|
|
||||||
flake8 InvenTree
|
|
||||||
|
|
||||||
javascript:
|
javascript:
|
||||||
name: javascript template files
|
name: Style [JS]
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
needs: pep_style
|
needs: pep_style
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Install node.js ${{ env.node_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.node_version }}
|
npm: true
|
||||||
cache: 'npm'
|
install: true
|
||||||
- run: npm install
|
- name: Check Templated JS Files
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.python_version }}
|
|
||||||
cache: 'pip'
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install gettext
|
|
||||||
pip3 install invoke
|
|
||||||
invoke install
|
|
||||||
invoke static
|
|
||||||
- name: Check Templated Files
|
|
||||||
run: |
|
run: |
|
||||||
cd ci
|
cd ci
|
||||||
python check_js_templates.py
|
python3 check_js_templates.py
|
||||||
- name: Lint Javascript Files
|
- name: Lint Javascript Files
|
||||||
run: |
|
run: |
|
||||||
invoke render-js-files
|
invoke render-js-files
|
||||||
npx eslint js_tmp/*.js
|
npx eslint js_tmp/*.js
|
||||||
|
|
||||||
html:
|
html:
|
||||||
name: html template files
|
name: Style [HTML]
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
needs: pep_style
|
needs: pep_style
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Install node.js ${{ env.node_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.node_version }}
|
npm: true
|
||||||
cache: 'npm'
|
install: true
|
||||||
- run: npm install
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.python_version }}
|
|
||||||
cache: 'pip'
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install gettext
|
|
||||||
pip3 install invoke
|
|
||||||
invoke install
|
|
||||||
invoke static
|
|
||||||
- name: Check HTML Files
|
- name: Check HTML Files
|
||||||
run: |
|
run: npx markuplint **/templates/*.html
|
||||||
npx markuplint InvenTree/build/templates/build/*.html
|
|
||||||
npx markuplint InvenTree/company/templates/company/*.html
|
pre-commit:
|
||||||
npx markuplint InvenTree/order/templates/order/*.html
|
name: Style [pre-commit]
|
||||||
npx markuplint InvenTree/part/templates/part/*.html
|
runs-on: ubuntu-20.04
|
||||||
npx markuplint InvenTree/stock/templates/stock/*.html
|
|
||||||
npx markuplint InvenTree/templates/*.html
|
needs: pep_style
|
||||||
npx markuplint InvenTree/templates/InvenTree/*.html
|
|
||||||
npx markuplint InvenTree/templates/InvenTree/settings/*.html
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.python_version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.python_version }}
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Run pre-commit Checks
|
||||||
|
uses: pre-commit/action@v2.0.3
|
||||||
|
- name: Check version number
|
||||||
|
run: |
|
||||||
|
python3 ci/check_version_number.py
|
||||||
|
|
||||||
python:
|
python:
|
||||||
name: python bindings
|
name: Tests - inventree-python
|
||||||
needs: pep_style
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
needs: pre-commit
|
||||||
|
|
||||||
env:
|
env:
|
||||||
wrapper_name: inventree-python
|
wrapper_name: inventree-python
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||||
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
|
||||||
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
|
||||||
INVENTREE_ADMIN_USER: testuser
|
INVENTREE_ADMIN_USER: testuser
|
||||||
INVENTREE_ADMIN_PASSWORD: testpassword
|
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||||
INVENTREE_ADMIN_EMAIL: test@test.com
|
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||||
@ -136,34 +113,32 @@ jobs:
|
|||||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Install InvenTree
|
uses: ./.github/actions/setup
|
||||||
run: |
|
with:
|
||||||
sudo apt-get update
|
apt-dependency: gettext poppler-utils
|
||||||
sudo apt-get install python3-dev python3-pip python3-venv
|
update: true
|
||||||
pip3 install invoke
|
- name: Download Python Code For `${{ env.wrapper_name }}`
|
||||||
invoke install
|
run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
||||||
invoke migrate
|
- name: Start InvenTree Server
|
||||||
- name: Download Python Code
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
|
||||||
- name: Start Server
|
|
||||||
run: |
|
run: |
|
||||||
invoke delete-data -f
|
invoke delete-data -f
|
||||||
invoke import-fixtures
|
invoke import-fixtures
|
||||||
invoke server -a 127.0.0.1:12345 &
|
invoke server -a 127.0.0.1:12345 &
|
||||||
invoke wait
|
invoke wait
|
||||||
- name: Run Tests
|
- name: Run Tests For `${{ env.wrapper_name }}`
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.wrapper_name }}
|
cd ${{ env.wrapper_name }}
|
||||||
invoke check-server
|
invoke check-server
|
||||||
coverage run -m unittest discover -s test/
|
coverage run -m unittest discover -s test/
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
name: Sqlite / coverage
|
name: Tests - DB [SQLite] + Coverage
|
||||||
needs: ['javascript', 'html']
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
needs: ['javascript', 'html', 'pre-commit']
|
||||||
|
continue-on-error: true # continue if a step fails so that coverage gets pushed
|
||||||
|
|
||||||
env:
|
env:
|
||||||
INVENTREE_DB_NAME: ./inventree.sqlite
|
INVENTREE_DB_NAME: ./inventree.sqlite
|
||||||
@ -171,32 +146,16 @@ jobs:
|
|||||||
INVENTREE_PLUGINS_ENABLED: true
|
INVENTREE_PLUGINS_ENABLED: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Setup Python ${{ env.python_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.python_version }}
|
apt-dependency: gettext poppler-utils
|
||||||
cache: 'pip'
|
update: true
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install gettext
|
|
||||||
python -m pip install -U pip
|
|
||||||
pip3 install invoke
|
|
||||||
invoke update
|
|
||||||
- name: Coverage Tests
|
- name: Coverage Tests
|
||||||
run: |
|
run: invoke coverage
|
||||||
invoke coverage
|
- name: Data Export Test
|
||||||
- name: Data Import Export
|
uses: ./.github/actions/migration
|
||||||
run: |
|
|
||||||
invoke migrate
|
|
||||||
invoke import-fixtures
|
|
||||||
invoke export-records -f data.json
|
|
||||||
rm inventree.sqlite
|
|
||||||
invoke migrate
|
|
||||||
invoke import-records -f data.json
|
|
||||||
invoke import-records -f data.json
|
|
||||||
- name: Test Translations
|
- name: Test Translations
|
||||||
run: invoke translate
|
run: invoke translate
|
||||||
- name: Check Migration Files
|
- name: Check Migration Files
|
||||||
@ -205,9 +164,10 @@ jobs:
|
|||||||
run: coveralls
|
run: coveralls
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
name: Postgres
|
name: Tests - DB [PostgreSQL]
|
||||||
needs: ['javascript', 'html']
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
needs: ['javascript', 'html', 'pre-commit']
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@ -235,38 +195,23 @@ jobs:
|
|||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Setup Python ${{ env.python_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.python_version }}
|
apt-dependency: gettext poppler-utils libpq-dev
|
||||||
cache: 'pip'
|
pip-dependency: psycopg2 django-redis>=5.0.0
|
||||||
- name: Install Dependencies
|
update: true
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install libpq-dev gettext
|
|
||||||
python -m pip install -U pip
|
|
||||||
pip3 install invoke
|
|
||||||
pip3 install psycopg2
|
|
||||||
pip3 install django-redis>=5.0.0
|
|
||||||
invoke update
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: invoke test
|
run: invoke test
|
||||||
- name: Data Import Export
|
- name: Data Export Test
|
||||||
run: |
|
uses: ./.github/actions/migration
|
||||||
invoke migrate
|
|
||||||
python3 ./InvenTree/manage.py flush --noinput
|
|
||||||
invoke import-fixtures
|
|
||||||
invoke export-records -f data.json
|
|
||||||
python3 ./InvenTree/manage.py flush --noinput
|
|
||||||
invoke import-records -f data.json
|
|
||||||
invoke import-records -f data.json
|
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
name: MySql
|
name: Tests - DB [MySQL]
|
||||||
needs: ['javascript', 'html']
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
needs: ['javascript', 'html', 'pre-commit']
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@ -293,29 +238,14 @@ jobs:
|
|||||||
- 3306:3306
|
- 3306:3306
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
- name: Enviroment Setup
|
||||||
- name: Setup Python ${{ env.python_version }}
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.python_version }}
|
apt-dependency: gettext poppler-utils libmysqlclient-dev
|
||||||
cache: 'pip'
|
pip-dependency: mysqlclient
|
||||||
- name: Install Dependencies
|
update: true
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install libmysqlclient-dev gettext
|
|
||||||
python -m pip install -U pip
|
|
||||||
pip3 install invoke
|
|
||||||
pip3 install mysqlclient
|
|
||||||
invoke update
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: invoke test
|
run: invoke test
|
||||||
- name: Data Import Export
|
- name: Data Export Test
|
||||||
run: |
|
uses: ./.github/actions/migration
|
||||||
invoke migrate
|
|
||||||
python3 ./InvenTree/manage.py flush --noinput
|
|
||||||
invoke import-fixtures
|
|
||||||
invoke export-records -f data.json
|
|
||||||
python3 ./InvenTree/manage.py flush --noinput
|
|
||||||
invoke import-records -f data.json
|
|
||||||
invoke import-records -f data.json
|
|
||||||
|
21
.github/workflows/version.yml
vendored
21
.github/workflows/version.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
# Checks version number
|
|
||||||
name: version number
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches-ignore:
|
|
||||||
- l10*
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
check_version:
|
|
||||||
name: version number
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Check version number
|
|
||||||
run: |
|
|
||||||
python3 ci/check_version_number.py --branch ${{ github.base_ref }}
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -8,6 +8,7 @@ __pycache__/
|
|||||||
env/
|
env/
|
||||||
inventree-env/
|
inventree-env/
|
||||||
./build/
|
./build/
|
||||||
|
.cache/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
bin/
|
bin/
|
||||||
@ -26,7 +27,6 @@ var/
|
|||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
|
|
||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
local_settings.py
|
local_settings.py
|
||||||
@ -38,6 +38,8 @@ local_settings.py
|
|||||||
# Files used for testing
|
# Files used for testing
|
||||||
dummy_image.*
|
dummy_image.*
|
||||||
_tmp.csv
|
_tmp.csv
|
||||||
|
inventree/label.pdf
|
||||||
|
inventree/label.png
|
||||||
|
|
||||||
# Sphinx files
|
# Sphinx files
|
||||||
docs/_build
|
docs/_build
|
||||||
@ -63,6 +65,7 @@ secret_key.txt
|
|||||||
.idea/
|
.idea/
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.bash_history
|
||||||
|
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
.coverage
|
.coverage
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
InvenTree/InvenTree/static/.*|
|
||||||
|
InvenTree/locale/.*
|
||||||
|
)$
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.2.0
|
rev: v4.2.0
|
||||||
|
@ -2,7 +2,7 @@ Please read the contribution guidelines below, before submitting your first pull
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Please run `invoke setup_dev` in the root directory of your InvenTree code base to set up your development setup before starting to contribute. This will install and set up pre-commit to run some checks before each commit and help reduce the style errors.
|
Please run `invoke setup-dev` in the root directory of your InvenTree code base to set up your development setup before starting to contribute. This will install and set up pre-commit to run some checks before each commit and help reduce the style errors.
|
||||||
|
|
||||||
## Branches and Versioning
|
## Branches and Versioning
|
||||||
|
|
||||||
|
@ -1,37 +1,39 @@
|
|||||||
FROM alpine:3.14 as base
|
# The InvenTree dockerfile provides two build targets:
|
||||||
|
#
|
||||||
|
# production:
|
||||||
|
# - Required files are copied into the image
|
||||||
|
# - Runs InvenTree web server under gunicorn
|
||||||
|
#
|
||||||
|
# dev:
|
||||||
|
# - Expects source directories to be loaded as a run-time volume
|
||||||
|
# - Runs InvenTree web server under django development server
|
||||||
|
# - Monitors source files for any changes, and live-reloads server
|
||||||
|
|
||||||
# GitHub source
|
|
||||||
ARG repository="https://github.com/inventree/InvenTree.git"
|
|
||||||
ARG branch="master"
|
|
||||||
|
|
||||||
# Optionally specify a particular tag to checkout
|
FROM python:3.9-slim as base
|
||||||
ARG tag=""
|
|
||||||
|
# Build arguments for this image
|
||||||
|
ARG commit_hash=""
|
||||||
|
ARG commit_date=""
|
||||||
|
ARG commit_tag=""
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
# Ref: https://github.com/pyca/cryptography/issues/5776
|
# Ref: https://github.com/pyca/cryptography/issues/5776
|
||||||
ENV CRYPTOGRAPHY_DONT_BUILD_RUST 1
|
ENV CRYPTOGRAPHY_DONT_BUILD_RUST 1
|
||||||
|
|
||||||
# InvenTree key settings
|
|
||||||
|
|
||||||
# The INVENTREE_HOME directory is where the InvenTree source repository will be located
|
|
||||||
ENV INVENTREE_HOME="/home/inventree"
|
|
||||||
|
|
||||||
# GitHub settings
|
|
||||||
ENV INVENTREE_GIT_REPO="${repository}"
|
|
||||||
ENV INVENTREE_GIT_BRANCH="${branch}"
|
|
||||||
ENV INVENTREE_GIT_TAG="${tag}"
|
|
||||||
|
|
||||||
ENV INVENTREE_LOG_LEVEL="INFO"
|
ENV INVENTREE_LOG_LEVEL="INFO"
|
||||||
ENV INVENTREE_DOCKER="true"
|
ENV INVENTREE_DOCKER="true"
|
||||||
|
|
||||||
# InvenTree paths
|
# InvenTree paths
|
||||||
|
ENV INVENTREE_HOME="/home/inventree"
|
||||||
ENV INVENTREE_MNG_DIR="${INVENTREE_HOME}/InvenTree"
|
ENV INVENTREE_MNG_DIR="${INVENTREE_HOME}/InvenTree"
|
||||||
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
||||||
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
|
||||||
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
||||||
ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DATA_DIR}/plugins"
|
ENV INVENTREE_PLUGIN_DIR="${INVENTREE_DATA_DIR}/plugins"
|
||||||
|
|
||||||
|
# InvenTree configuration files
|
||||||
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||||
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
|
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
|
||||||
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt"
|
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt"
|
||||||
@ -49,82 +51,83 @@ LABEL org.label-schema.schema-version="1.0" \
|
|||||||
org.label-schema.vendor="inventree" \
|
org.label-schema.vendor="inventree" \
|
||||||
org.label-schema.name="inventree/inventree" \
|
org.label-schema.name="inventree/inventree" \
|
||||||
org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
|
org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
|
||||||
org.label-schema.vcs-url=${INVENTREE_GIT_REPO} \
|
org.label-schema.vcs-url="https://github.com/inventree/InvenTree.git" \
|
||||||
org.label-schema.vcs-branch=${INVENTREE_GIT_BRANCH} \
|
org.label-schema.vcs-ref=${commit_tag}
|
||||||
org.label-schema.vcs-ref=${INVENTREE_GIT_TAG}
|
|
||||||
|
|
||||||
# Create user account
|
# RUN apt-get upgrade && apt-get update
|
||||||
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
|
RUN apt-get update
|
||||||
|
|
||||||
RUN apk -U upgrade
|
|
||||||
|
|
||||||
# Install required system packages
|
# Install required system packages
|
||||||
RUN apk add --no-cache git make bash \
|
RUN apt-get install -y --no-install-recommends \
|
||||||
gcc libgcc g++ libstdc++ \
|
git gcc g++ gettext gnupg libffi-dev \
|
||||||
gnupg \
|
# Weasyprint requirements : https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#debian-11
|
||||||
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev libwebp-dev \
|
poppler-utils libpango-1.0-0 libpangoft2-1.0-0 \
|
||||||
libffi libffi-dev \
|
# Image format support
|
||||||
zlib zlib-dev \
|
libjpeg-dev webp \
|
||||||
# Special deps for WeasyPrint (these will be deprecated once WeasyPrint drops cairo requirement)
|
|
||||||
cairo cairo-dev pango pango-dev gdk-pixbuf \
|
|
||||||
# Fonts
|
|
||||||
fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans font-croscore font-noto \
|
|
||||||
# Core python
|
|
||||||
python3 python3-dev py3-pip \
|
|
||||||
# SQLite support
|
# SQLite support
|
||||||
sqlite \
|
sqlite3 \
|
||||||
# PostgreSQL support
|
# PostgreSQL support
|
||||||
postgresql postgresql-contrib postgresql-dev libpq \
|
libpq-dev \
|
||||||
# MySQL/MariaDB support
|
# MySQL / MariaDB support
|
||||||
mariadb-connector-c mariadb-dev mariadb-client \
|
default-libmysqlclient-dev mariadb-client && \
|
||||||
# Required for python cryptography support
|
apt-get autoclean && apt-get autoremove
|
||||||
openssl-dev musl-dev libffi-dev rust cargo
|
|
||||||
|
|
||||||
# Update pip
|
# Update pip
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
# Install required base-level python packages
|
# Install required base-level python packages
|
||||||
COPY requirements.txt requirements.txt
|
COPY ./docker/requirements.txt base_requirements.txt
|
||||||
RUN pip install --no-cache-dir -U -r requirements.txt
|
RUN pip install --disable-pip-version-check -U -r base_requirements.txt
|
||||||
|
|
||||||
|
# InvenTree production image:
|
||||||
|
# - Copies required files from local directory
|
||||||
|
# - Installs required python packages from requirements.txt
|
||||||
|
# - Starts a gunicorn webserver
|
||||||
|
|
||||||
# Production code (pulled from tagged github release)
|
|
||||||
FROM base as production
|
FROM base as production
|
||||||
|
|
||||||
# Clone source code
|
ENV INVENTREE_DEBUG=False
|
||||||
RUN echo "Downloading InvenTree from ${INVENTREE_GIT_REPO}"
|
|
||||||
|
|
||||||
RUN git clone --branch ${INVENTREE_GIT_BRANCH} --depth 1 ${INVENTREE_GIT_REPO} ${INVENTREE_HOME}
|
# As .git directory is not available in production image, we pass the commit information via ENV
|
||||||
|
ENV INVENTREE_COMMIT_HASH="${commit_hash}"
|
||||||
|
ENV INVENTREE_COMMIT_DATE="${commit_date}"
|
||||||
|
|
||||||
# Ref: https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
# Copy source code
|
||||||
RUN git config --global --add safe.directory ${INVENTREE_HOME}
|
COPY InvenTree ${INVENTREE_HOME}/InvenTree
|
||||||
|
|
||||||
# Checkout against a particular git tag
|
# Copy other key files
|
||||||
RUN if [ -n "${INVENTREE_GIT_TAG}" ] ; then cd ${INVENTREE_HOME} && git fetch --all --tags && git checkout tags/${INVENTREE_GIT_TAG} -b v${INVENTREE_GIT_TAG}-branch ; fi
|
COPY requirements.txt ${INVENTREE_HOME}/requirements.txt
|
||||||
|
COPY tasks.py ${INVENTREE_HOME}/tasks.py
|
||||||
RUN chown -R inventree:inventreegroup ${INVENTREE_HOME}/*
|
COPY docker/gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
|
||||||
|
COPY docker/init.sh ${INVENTREE_MNG_DIR}/init.sh
|
||||||
# Drop to the inventree user
|
|
||||||
USER inventree
|
|
||||||
|
|
||||||
# Install InvenTree packages
|
|
||||||
RUN pip3 install --user --no-cache-dir --disable-pip-version-check -r ${INVENTREE_HOME}/requirements.txt
|
|
||||||
|
|
||||||
# Need to be running from within this directory
|
# Need to be running from within this directory
|
||||||
WORKDIR ${INVENTREE_MNG_DIR}
|
WORKDIR ${INVENTREE_MNG_DIR}
|
||||||
|
|
||||||
|
# Drop to the inventree user for the production image
|
||||||
|
RUN adduser inventree
|
||||||
|
RUN chown -R inventree:inventree ${INVENTREE_HOME}
|
||||||
|
|
||||||
|
USER inventree
|
||||||
|
|
||||||
|
# Install InvenTree packages
|
||||||
|
RUN pip3 install --user --disable-pip-version-check -r ${INVENTREE_HOME}/requirements.txt
|
||||||
|
|
||||||
# Server init entrypoint
|
# Server init entrypoint
|
||||||
ENTRYPOINT ["/bin/bash", "../docker/init.sh"]
|
ENTRYPOINT ["/bin/bash", "./init.sh"]
|
||||||
|
|
||||||
# Launch the production server
|
# Launch the production server
|
||||||
# TODO: Work out why environment variables cannot be interpolated in this command
|
# TODO: Work out why environment variables cannot be interpolated in this command
|
||||||
# TODO: e.g. -b ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT} fails here
|
# TODO: e.g. -b ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT} fails here
|
||||||
CMD gunicorn -c ./docker/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
|
||||||
|
|
||||||
FROM base as dev
|
FROM base as dev
|
||||||
|
|
||||||
# The development image requires the source code to be mounted to /home/inventree/
|
# The development image requires the source code to be mounted to /home/inventree/
|
||||||
# So from here, we don't actually "do" anything, apart from some file management
|
# So from here, we don't actually "do" anything, apart from some file management
|
||||||
|
|
||||||
|
ENV INVENTREE_DEBUG=True
|
||||||
|
|
||||||
ENV INVENTREE_DEV_DIR="${INVENTREE_HOME}/dev"
|
ENV INVENTREE_DEV_DIR="${INVENTREE_HOME}/dev"
|
||||||
|
|
||||||
# Location for python virtual environment
|
# Location for python virtual environment
|
@ -117,6 +117,11 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
|||||||
response = self.client.get(url, data, format='json')
|
response = self.client.get(url, data, format='json')
|
||||||
|
|
||||||
if expected_code is not None:
|
if expected_code is not None:
|
||||||
|
|
||||||
|
if response.status_code != expected_code:
|
||||||
|
print(f"Unexpected response at '{url}':")
|
||||||
|
print(response.data)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, expected_code)
|
self.assertEqual(response.status_code, expected_code)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -4,11 +4,14 @@ InvenTree API version information
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 50
|
INVENTREE_API_VERSION = 51
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v51 -> 2022-05-24 : https://github.com/inventree/InvenTree/pull/3058
|
||||||
|
- Adds new fields to the SalesOrderShipment model
|
||||||
|
|
||||||
v50 -> 2022-05-18 : https://github.com/inventree/InvenTree/pull/2912
|
v50 -> 2022-05-18 : https://github.com/inventree/InvenTree/pull/2912
|
||||||
- Implement Attachments for manufacturer parts
|
- Implement Attachments for manufacturer parts
|
||||||
|
|
||||||
|
@ -40,7 +40,11 @@ def exception_handler(exc, context):
|
|||||||
if response is None:
|
if response is None:
|
||||||
# DRF handler did not provide a default response for this exception
|
# DRF handler did not provide a default response for this exception
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.TESTING:
|
||||||
|
# If in TESTING mode, re-throw the exception for traceback
|
||||||
|
raise exc
|
||||||
|
elif settings.DEBUG:
|
||||||
|
# If in DEBUG mode, provide error information in the response
|
||||||
error_detail = str(exc)
|
error_detail = str(exc)
|
||||||
else:
|
else:
|
||||||
error_detail = _("Error details can be found in the admin panel")
|
error_detail = _("Error details can be found in the admin panel")
|
||||||
|
@ -129,7 +129,7 @@ def TestIfImageURL(url):
|
|||||||
Simply tests the extension against a set of allowed values
|
Simply tests the extension against a set of allowed values
|
||||||
"""
|
"""
|
||||||
return os.path.splitext(os.path.basename(url))[-1].lower() in [
|
return os.path.splitext(os.path.basename(url))[-1].lower() in [
|
||||||
'.jpg', '.jpeg',
|
'.jpg', '.jpeg', '.j2k',
|
||||||
'.png', '.bmp',
|
'.png', '.bmp',
|
||||||
'.tif', '.tiff',
|
'.tif', '.tiff',
|
||||||
'.webp', '.gif',
|
'.webp', '.gif',
|
||||||
|
@ -173,12 +173,9 @@ class StockStatus(StatusCode):
|
|||||||
DESTROYED = 60 # Item is destroyed
|
DESTROYED = 60 # Item is destroyed
|
||||||
REJECTED = 65 # Item is rejected
|
REJECTED = 65 # Item is rejected
|
||||||
LOST = 70 # Item has been lost
|
LOST = 70 # Item has been lost
|
||||||
|
QUARANTINED = 75 # Item has been quarantined and is unavailable
|
||||||
RETURNED = 85 # Item has been returned from a customer
|
RETURNED = 85 # Item has been returned from a customer
|
||||||
|
|
||||||
# Any stock code above 100 means that the stock item is not "in stock"
|
|
||||||
# This can be used as a quick check for filtering
|
|
||||||
NOT_IN_STOCK = 100
|
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
OK: _("OK"),
|
OK: _("OK"),
|
||||||
ATTENTION: _("Attention needed"),
|
ATTENTION: _("Attention needed"),
|
||||||
@ -186,6 +183,7 @@ class StockStatus(StatusCode):
|
|||||||
DESTROYED: _("Destroyed"),
|
DESTROYED: _("Destroyed"),
|
||||||
LOST: _("Lost"),
|
LOST: _("Lost"),
|
||||||
REJECTED: _("Rejected"),
|
REJECTED: _("Rejected"),
|
||||||
|
QUARANTINED: _("Quarantined"),
|
||||||
RETURNED: _("Returned"),
|
RETURNED: _("Returned"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +194,7 @@ class StockStatus(StatusCode):
|
|||||||
DESTROYED: 'danger',
|
DESTROYED: 'danger',
|
||||||
LOST: 'dark',
|
LOST: 'dark',
|
||||||
REJECTED: 'danger',
|
REJECTED: 'danger',
|
||||||
|
QUARANTINED: 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
# The following codes correspond to parts that are 'available' or 'in stock'
|
# The following codes correspond to parts that are 'available' or 'in stock'
|
||||||
@ -206,22 +205,6 @@ class StockStatus(StatusCode):
|
|||||||
RETURNED,
|
RETURNED,
|
||||||
]
|
]
|
||||||
|
|
||||||
# The following codes correspond to parts that are 'unavailable'
|
|
||||||
UNAVAILABLE_CODES = [
|
|
||||||
DESTROYED,
|
|
||||||
LOST,
|
|
||||||
REJECTED,
|
|
||||||
]
|
|
||||||
|
|
||||||
# The following codes are available for receiving goods
|
|
||||||
RECEIVING_CODES = [
|
|
||||||
OK,
|
|
||||||
ATTENTION,
|
|
||||||
DAMAGED,
|
|
||||||
DESTROYED,
|
|
||||||
REJECTED
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class StockHistoryCode(StatusCode):
|
class StockHistoryCode(StatusCode):
|
||||||
|
|
||||||
|
@ -380,6 +380,30 @@ class TestVersionNumber(TestCase):
|
|||||||
self.assertTrue(v_d > v_c)
|
self.assertTrue(v_d > v_c)
|
||||||
self.assertTrue(v_d > v_a)
|
self.assertTrue(v_d > v_a)
|
||||||
|
|
||||||
|
def test_commit_info(self):
|
||||||
|
"""Test that the git commit information is extracted successfully"""
|
||||||
|
|
||||||
|
envs = {
|
||||||
|
'INVENTREE_COMMIT_HASH': 'abcdef',
|
||||||
|
'INVENTREE_COMMIT_DATE': '2022-12-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that the environment variables take priority
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, envs):
|
||||||
|
self.assertEqual(version.inventreeCommitHash(), 'abcdef')
|
||||||
|
self.assertEqual(version.inventreeCommitDate(), '2022-12-31')
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Check that the current .git values work too
|
||||||
|
|
||||||
|
hash = str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||||
|
self.assertEqual(hash, version.inventreeCommitHash())
|
||||||
|
|
||||||
|
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip().split(' ')[0]
|
||||||
|
self.assertEqual(d, version.inventreeCommitDate())
|
||||||
|
|
||||||
|
|
||||||
class CurrencyTests(TestCase):
|
class CurrencyTests(TestCase):
|
||||||
"""
|
"""
|
||||||
@ -522,7 +546,7 @@ class TestSettings(helpers.InvenTreeTestCase):
|
|||||||
|
|
||||||
# Set dynamic setting to True and rerun to launch install
|
# Set dynamic setting to True and rerun to launch install
|
||||||
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user)
|
||||||
registry.reload_plugins()
|
registry.reload_plugins(full_reload=True)
|
||||||
|
|
||||||
# Check that there was anotehr run
|
# Check that there was anotehr run
|
||||||
response = registry.install_plugin_file()
|
response = registry.install_plugin_file()
|
||||||
|
@ -3,6 +3,7 @@ Version information for InvenTree.
|
|||||||
Provides information on the current InvenTree version
|
Provides information on the current InvenTree version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ import common.models
|
|||||||
from InvenTree.api_version import INVENTREE_API_VERSION
|
from InvenTree.api_version import INVENTREE_API_VERSION
|
||||||
|
|
||||||
# InvenTree software version
|
# InvenTree software version
|
||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.8.0 dev"
|
||||||
|
|
||||||
|
|
||||||
def inventreeInstanceName():
|
def inventreeInstanceName():
|
||||||
@ -99,6 +100,12 @@ def inventreeDjangoVersion():
|
|||||||
def inventreeCommitHash():
|
def inventreeCommitHash():
|
||||||
""" Returns the git commit hash for the running codebase """
|
""" Returns the git commit hash for the running codebase """
|
||||||
|
|
||||||
|
# First look in the environment variables, i.e. if running in docker
|
||||||
|
commit_hash = os.environ.get('INVENTREE_COMMIT_HASH', '')
|
||||||
|
|
||||||
|
if commit_hash:
|
||||||
|
return commit_hash
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
return str(subprocess.check_output('git rev-parse --short HEAD'.split()), 'utf-8').strip()
|
||||||
except: # pragma: no cover
|
except: # pragma: no cover
|
||||||
@ -108,6 +115,12 @@ def inventreeCommitHash():
|
|||||||
def inventreeCommitDate():
|
def inventreeCommitDate():
|
||||||
""" Returns the git commit date for the running codebase """
|
""" Returns the git commit date for the running codebase """
|
||||||
|
|
||||||
|
# First look in the environment variables, e.g. if running in docker
|
||||||
|
commit_date = os.environ.get('INVENTREE_COMMIT_DATE', '')
|
||||||
|
|
||||||
|
if commit_date:
|
||||||
|
return commit_date.split(' ')[0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
|
d = str(subprocess.check_output('git show -s --format=%ci'.split()), 'utf-8').strip()
|
||||||
return d.split(' ')[0]
|
return d.split(' ')[0]
|
||||||
|
@ -355,8 +355,9 @@ onPanelLoad('completed', function() {
|
|||||||
|
|
||||||
onPanelLoad('children', function() {
|
onPanelLoad('children', function() {
|
||||||
loadBuildTable($('#sub-build-table'), {
|
loadBuildTable($('#sub-build-table'), {
|
||||||
url: '{% url "api-build-list" %}',
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
filterTarget: "#filter-list-sub-build",
|
filterTarget: "#filter-list-sub-build",
|
||||||
|
parentBuild: {{ build.pk }},
|
||||||
params: {
|
params: {
|
||||||
ancestor: {{ build.pk }},
|
ancestor: {{ build.pk }},
|
||||||
}
|
}
|
||||||
@ -527,11 +528,7 @@ $('#btn-unallocate').on('click', function() {
|
|||||||
|
|
||||||
$('#allocate-selected-items').click(function() {
|
$('#allocate-selected-items').click(function() {
|
||||||
|
|
||||||
var bom_items = $("#allocation-table-untracked").bootstrapTable("getSelections");
|
var bom_items = getTableData('#allocation-table-untracked');
|
||||||
|
|
||||||
if (bom_items.length == 0) {
|
|
||||||
bom_items = $("#allocation-table-untracked").bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
allocateStockToBuild(
|
allocateStockToBuild(
|
||||||
{{ build.pk }},
|
{{ build.pk }},
|
||||||
|
@ -40,13 +40,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Buttons to switch between list and calendar views -->
|
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
|
||||||
<span class='fas fa-calendar-alt'></span>
|
|
||||||
</button>
|
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-list' title='{% trans "Display list view" %}'>
|
|
||||||
<span class='fas fa-th-list'></span>
|
|
||||||
</button>
|
|
||||||
{% include "filter_list.html" with id="build" %}
|
{% include "filter_list.html" with id="build" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,139 +47,24 @@
|
|||||||
|
|
||||||
<table class='table table-striped table-condensed' id='build-table' data-toolbar='#button-toolbar'>
|
<table class='table table-striped table-condensed' id='build-table' data-toolbar='#button-toolbar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div id='build-order-calendar'></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_load %}
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
<script type='text/javascript'>
|
|
||||||
function loadOrderEvents(calendar) {
|
|
||||||
|
|
||||||
var start = startDate(calendar);
|
|
||||||
var end = endDate(calendar);
|
|
||||||
|
|
||||||
clearEvents(calendar);
|
|
||||||
|
|
||||||
// Request build orders from the server within specified date range
|
|
||||||
inventreeGet(
|
|
||||||
'{% url "api-build-list" %}',
|
|
||||||
{
|
|
||||||
min_date: start,
|
|
||||||
max_date: end,
|
|
||||||
part_detail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
|
|
||||||
|
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
|
||||||
|
|
||||||
var order = response[idx];
|
|
||||||
|
|
||||||
var date = order.creation_date;
|
|
||||||
|
|
||||||
if (order.completion_date) {
|
|
||||||
date = order.completion_date;
|
|
||||||
} else if (order.target_date) {
|
|
||||||
date = order.target_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = `${prefix}${order.reference}`; //- ${order.quantity} x ${order.part_detail.name}`;
|
|
||||||
|
|
||||||
var color = '#4c68f5';
|
|
||||||
|
|
||||||
if (order.completed) {
|
|
||||||
color = '#25c234';
|
|
||||||
} else if (order.overdue) {
|
|
||||||
color = '#c22525';
|
|
||||||
}
|
|
||||||
|
|
||||||
var event = {
|
|
||||||
title: title,
|
|
||||||
start: date,
|
|
||||||
end: date,
|
|
||||||
url: `/build/${order.pk}/`,
|
|
||||||
backgroundColor: color,
|
|
||||||
};
|
|
||||||
|
|
||||||
calendar.addEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var calendar = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var el = document.getElementById('build-order-calendar');
|
|
||||||
|
|
||||||
calendar = new FullCalendar.Calendar(el, {
|
|
||||||
initialView: 'dayGridMonth',
|
|
||||||
nowIndicator: true,
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
locale: '{{request.LANGUAGE_CODE}}',
|
|
||||||
datesSet: function() {
|
|
||||||
loadOrderEvents(calendar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#build-order-calendar').hide();
|
|
||||||
$('#view-list').hide();
|
|
||||||
|
|
||||||
$('#view-calendar').click(function() {
|
|
||||||
// Hide the list view, show the calendar view
|
|
||||||
$("#build-table").hide();
|
|
||||||
$("#view-calendar").hide();
|
|
||||||
$(".fixed-table-pagination").hide();
|
|
||||||
$(".columns-right").hide();
|
|
||||||
$(".search").hide();
|
|
||||||
|
|
||||||
$("#build-order-calendar").show();
|
|
||||||
$("#view-list").show();
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
|
||||||
// Hide the calendar view, show the list view
|
|
||||||
$("#build-order-calendar").hide();
|
|
||||||
$("#view-list").hide();
|
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
|
||||||
$(".columns-right").show();
|
|
||||||
$(".search").show();
|
|
||||||
$("#build-table").show();
|
|
||||||
$("#view-calendar").show();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#collapse-item-active").collapse().show();
|
|
||||||
|
|
||||||
$("#new-build").click(function() {
|
$("#new-build").click(function() {
|
||||||
newBuildOrder();
|
newBuildOrder();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBuildTable($("#build-table"), {
|
loadBuildTable($("#build-table"), {
|
||||||
url: "{% url 'api-build-list' %}",
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if report_enabled %}
|
{% if report_enabled %}
|
||||||
$('#multi-build-print').click(function() {
|
$('#multi-build-print').click(function() {
|
||||||
var rows = $("#build-table").bootstrapTable('getSelections');
|
var rows = getTableData("#build-table");
|
||||||
|
|
||||||
var build_ids = [];
|
var build_ids = [];
|
||||||
|
|
||||||
rows.forEach(function(row) {
|
rows.forEach(function(row) {
|
||||||
|
@ -203,7 +203,7 @@ class UIMessageNotification(SingleNotificationMethod):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def trigger_notifaction(obj, category=None, obj_ref='pk', **kwargs):
|
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||||
"""
|
"""
|
||||||
Send out a notification
|
Send out a notification
|
||||||
"""
|
"""
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
<div class='button-toolbar container-fluid'>
|
<div class='button-toolbar container-fluid'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class="btn btn-primary dropdown-toggle" id='supplier-table-options' type="button" data-bs-toggle="dropdown">{% trans "Options" %}
|
<button class="btn btn-primary dropdown-toggle" id='supplier-table-options' type="button" data-bs-toggle="dropdown">
|
||||||
<span class="caret"></span>
|
<span class='fas fa-tools'></span> <span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if roles.purchase_order.add %}
|
{% if roles.purchase_order.add %}
|
||||||
@ -75,7 +75,8 @@
|
|||||||
<div class='button-toolbar container-fluid'>
|
<div class='button-toolbar container-fluid'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button class="btn btn-primary dropdown-toggle" id='manufacturer-table-options' type="button" data-bs-toggle="dropdown">{% trans "Options" %}
|
<button class="btn btn-primary dropdown-toggle" id='manufacturer-table-options' type="button" data-bs-toggle="dropdown">
|
||||||
|
<span class='fas fa-tools'></span>
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
@ -307,20 +308,18 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-table-options']);
|
|
||||||
|
|
||||||
$("#multi-manufacturer-part-delete").click(function() {
|
$("#multi-manufacturer-part-delete").click(function() {
|
||||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
var selections = getTableData('#manufacturer-part-table');
|
||||||
|
|
||||||
deleteManufacturerParts(selections, {
|
deleteManufacturerParts(selections, {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
$("#manufacturer-part-table").bootstrapTable("refresh");
|
$("#manufacturer-part-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#multi-manufacturer-part-order").click(function() {
|
$("#multi-manufacturer-part-order").click(function() {
|
||||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
var selections = getTableData('#manufacturer-part-table');
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
@ -364,34 +363,20 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-table-options']);
|
|
||||||
|
|
||||||
$("#multi-supplier-part-delete").click(function() {
|
$("#multi-supplier-part-delete").click(function() {
|
||||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
|
||||||
|
|
||||||
var requests = [];
|
var selections = getTableData("#supplier-part-table");
|
||||||
|
|
||||||
showQuestionDialog(
|
deleteSupplierParts(selections, {
|
||||||
'{% trans "Delete Supplier Parts?" %}',
|
success: function() {
|
||||||
'{% trans "All selected supplier parts will be deleted" %}',
|
$('#supplier-part-table').bootstrapTable('refresh');
|
||||||
{
|
|
||||||
accept: function() {
|
|
||||||
selections.forEach(function(part) {
|
|
||||||
var url = `/api/company/part/${part.pk}/`;
|
|
||||||
|
|
||||||
requests.push(inventreeDelete(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
$('#supplier-part-table').bootstrapTable('refresh');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#multi-supplier-part-order").click(function() {
|
$("#multi-supplier-part-order").click(function() {
|
||||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
|
||||||
|
var selections = getTableData('#supplier-part-table');
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
|
@ -129,7 +129,9 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<div id='supplier-button-toolbar'>
|
<div id='supplier-button-toolbar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<div id='opt-dropdown' class="btn-group">
|
<div id='opt-dropdown' class="btn-group">
|
||||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<span class='fas fa-tools'></span> <span class="caret"></span>
|
||||||
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -174,11 +176,14 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<div id='parameter-toolbar'>
|
<div id='parameter-toolbar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<div id='opt-dropdown' class="btn-group">
|
<div id='opt-dropdown' class="btn-group">
|
||||||
<button id='parameter-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
<button id='parameter-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||||
|
<span class='fas fa-tools'></span> <span class="caret"></span>
|
||||||
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class='dropdown-item' href='#' id='multi-parameter-delete' title='{% trans "Delete parameters" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='multi-parameter-delete' title='{% trans "Delete parameters" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "filter_list.html" with id="manufacturer-part-parameters" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -256,67 +261,22 @@ $('#supplier-create').click(function () {
|
|||||||
|
|
||||||
$("#supplier-part-delete").click(function() {
|
$("#supplier-part-delete").click(function() {
|
||||||
|
|
||||||
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
var selections = getTableData('#supplier-table');
|
||||||
|
|
||||||
var requests = [];
|
deleteSupplierParts(selections, {
|
||||||
|
success: reloadSupplierPartTable,
|
||||||
showQuestionDialog(
|
});
|
||||||
'{% trans "Delete Supplier Parts?" %}',
|
|
||||||
'{% trans "All selected supplier parts will be deleted" %}',
|
|
||||||
{
|
|
||||||
accept: function() {
|
|
||||||
selections.forEach(function(part) {
|
|
||||||
var url = `/api/company/part/${part.pk}/`;
|
|
||||||
|
|
||||||
requests.push(inventreeDelete(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
reloadSupplierPartTable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#multi-parameter-delete").click(function() {
|
$("#multi-parameter-delete").click(function() {
|
||||||
|
|
||||||
var selections = $("#parameter-table").bootstrapTable("getSelections");
|
var selections = getTableData('#parameter-table');
|
||||||
|
|
||||||
var text = `
|
deleteManufacturerPartParameters(selections, {
|
||||||
<div class ='alert alert-block alert-danger'>
|
success: function() {
|
||||||
<p>{% trans "Selected parameters will be deleted" %}:</p>
|
$('#parameter-table').bootstrapTable('refresh');
|
||||||
<ul>`;
|
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
|
||||||
text += `<li>${item.name} - <em>${item.value}</em></li>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
text += `
|
|
||||||
</ul>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
showQuestionDialog(
|
|
||||||
'{% trans "Delete Parameters" %}',
|
|
||||||
text,
|
|
||||||
{
|
|
||||||
accept_text: '{% trans "Delete" %}',
|
|
||||||
accept: function() {
|
|
||||||
// Delete each parameter via the API
|
|
||||||
var requests = [];
|
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
|
||||||
var url = `/api/company/part/manufacturer/parameter/${item.pk}/`;
|
|
||||||
|
|
||||||
requests.push(inventreeDelete(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
$('#parameter-table').bootstrapTable('refresh');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSupplierPartTable(
|
loadSupplierPartTable(
|
||||||
@ -326,9 +286,9 @@ loadSupplierPartTable(
|
|||||||
params: {
|
params: {
|
||||||
part: {{ part.part.id }},
|
part: {{ part.part.id }},
|
||||||
manufacturer_part: {{ part.id }},
|
manufacturer_part: {{ part.id }},
|
||||||
part_detail: false,
|
part_detail: true,
|
||||||
supplier_detail: true,
|
supplier_detail: true,
|
||||||
manufacturer_detail: false,
|
manufacturer_detail: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -343,10 +303,6 @@ loadManufacturerPartParameterTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']);
|
|
||||||
|
|
||||||
linkButtonsToSelection($("#parameter-table"), ['#parameter-options']);
|
|
||||||
|
|
||||||
$('#order-part, #order-part2').click(function() {
|
$('#order-part, #order-part2').click(function() {
|
||||||
|
|
||||||
inventreeGet(
|
inventreeGet(
|
||||||
@ -376,15 +332,26 @@ $('#edit-part').click(function () {
|
|||||||
|
|
||||||
$('#delete-part').click(function() {
|
$('#delete-part').click(function() {
|
||||||
|
|
||||||
deleteManufacturerPart({{ part.pk }}, {
|
inventreeGet(
|
||||||
onSuccess: function() {
|
'{% url "api-manufacturer-part-detail" part.pk %}',
|
||||||
{% if part.manufacturer %}
|
{},
|
||||||
window.location.href = "{% url 'company-detail' part.manufacturer.id %}";
|
{
|
||||||
{% else%}
|
success: function(data) {
|
||||||
window.location.href = "{% url 'index' %}";
|
deleteManufacturerParts(
|
||||||
{% endif %}
|
[data],
|
||||||
|
{
|
||||||
|
success: function() {
|
||||||
|
{% if part.manufacturer %}
|
||||||
|
window.location.href = "{% url 'company-detail' part.manufacturer.id %}";
|
||||||
|
{% else%}
|
||||||
|
window.location.href = "{% url 'index' %}";
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
enableSidebar('manufacturerpart');
|
enableSidebar('manufacturerpart');
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% trans "Parameters" as text %}
|
{% trans "Parameters" as text %}
|
||||||
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
|
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
|
||||||
{% trans "Attachments" as text %}
|
|
||||||
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
|
||||||
{% trans "Supplier Parts" as text %}
|
{% trans "Supplier Parts" as text %}
|
||||||
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.urls import include, re_path
|
from django.urls import include, re_path
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from PIL import Image
|
|
||||||
from rest_framework import filters, generics
|
from rest_framework import filters, generics
|
||||||
from rest_framework.exceptions import NotFound
|
from rest_framework.exceptions import NotFound
|
||||||
|
|
||||||
@ -137,25 +134,21 @@ class LabelPrintMixin:
|
|||||||
# Label instance
|
# Label instance
|
||||||
label_instance = self.get_object()
|
label_instance = self.get_object()
|
||||||
|
|
||||||
for output in outputs:
|
for idx, output in enumerate(outputs):
|
||||||
"""
|
"""
|
||||||
For each output, we generate a temporary image file,
|
For each output, we generate a temporary image file,
|
||||||
which will then get sent to the printer
|
which will then get sent to the printer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Generate a png image at 300dpi
|
# Generate PDF data for the label
|
||||||
(img_data, w, h) = output.get_document().write_png(resolution=300)
|
pdf = output.get_document().write_pdf()
|
||||||
|
|
||||||
# Construct a BytesIO object, which can be read by pillow
|
|
||||||
img_bytes = BytesIO(img_data)
|
|
||||||
|
|
||||||
image = Image.open(img_bytes)
|
|
||||||
|
|
||||||
# Offload a background task to print the provided label
|
# Offload a background task to print the provided label
|
||||||
offload_task(
|
offload_task(
|
||||||
plugin_label.print_label,
|
plugin_label.print_label,
|
||||||
plugin.plugin_slug(),
|
plugin.plugin_slug(),
|
||||||
image,
|
pdf,
|
||||||
|
filename=label_names[idx],
|
||||||
label_instance=label_instance,
|
label_instance=label_instance,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
)
|
)
|
||||||
|
23
InvenTree/order/migrations/0069_auto_20220524_0508.py
Normal file
23
InvenTree/order/migrations/0069_auto_20220524_0508.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.13 on 2022-05-24 05:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('order', '0068_alter_salesorderallocation_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesordershipment',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, help_text='Reference number for associated invoice', max_length=100, verbose_name='Invoice Number'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='salesordershipment',
|
||||||
|
name='link',
|
||||||
|
field=models.URLField(blank=True, help_text='Link to external page', verbose_name='Link'),
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,10 @@ Order model definitions
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@ -19,8 +22,10 @@ from django.dispatch.dispatcher import receiver
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
|
from error_report.models import Error
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
from mptt.models import TreeForeignKey
|
from mptt.models import TreeForeignKey
|
||||||
|
|
||||||
@ -38,6 +43,8 @@ from plugin.models import MetadataMixin
|
|||||||
from stock import models as stock_models
|
from stock import models as stock_models
|
||||||
from users import models as UserModels
|
from users import models as UserModels
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def get_next_po_number():
|
def get_next_po_number():
|
||||||
"""
|
"""
|
||||||
@ -151,23 +158,74 @@ class Order(MetadataMixin, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
notes = MarkdownxField(blank=True, verbose_name=_('Notes'), help_text=_('Order notes'))
|
||||||
|
|
||||||
def get_total_price(self):
|
def get_total_price(self, target_currency=currency_code_default()):
|
||||||
"""
|
"""
|
||||||
Calculates the total price of all order lines
|
Calculates the total price of all order lines, and converts to the specified target currency.
|
||||||
|
|
||||||
|
If not specified, the default system currency is used.
|
||||||
|
|
||||||
|
If currency conversion fails (e.g. there are no valid conversion rates),
|
||||||
|
then we simply return zero, rather than attempting some other calculation.
|
||||||
"""
|
"""
|
||||||
target_currency = currency_code_default()
|
|
||||||
total = Money(0, target_currency)
|
total = Money(0, target_currency)
|
||||||
|
|
||||||
# gather name reference
|
# gather name reference
|
||||||
price_ref = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
|
price_ref_tag = 'sale_price' if isinstance(self, SalesOrder) else 'purchase_price'
|
||||||
# order items
|
|
||||||
total += sum(a.quantity * convert_money(getattr(a, price_ref), target_currency) for a in self.lines.all() if getattr(a, price_ref))
|
|
||||||
|
|
||||||
# extra lines
|
# order items
|
||||||
total += sum(a.quantity * convert_money(a.price, target_currency) for a in self.extra_lines.all() if a.price)
|
for line in self.lines.all():
|
||||||
|
|
||||||
|
price_ref = getattr(line, price_ref_tag)
|
||||||
|
|
||||||
|
if not price_ref:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
total += line.quantity * convert_money(price_ref, target_currency)
|
||||||
|
except MissingRate:
|
||||||
|
# Record the error, try to press on
|
||||||
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
|
Error.objects.create(
|
||||||
|
kind=kind.__name__,
|
||||||
|
info=info,
|
||||||
|
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||||
|
path='order.get_total_price',
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(f"Missing exchange rate for '{target_currency}'")
|
||||||
|
|
||||||
|
# Return None to indicate the calculated price is invalid
|
||||||
|
return None
|
||||||
|
|
||||||
|
# extra items
|
||||||
|
for line in self.extra_lines.all():
|
||||||
|
|
||||||
|
if not line.price:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
total += line.quantity * convert_money(line.price, target_currency)
|
||||||
|
except MissingRate:
|
||||||
|
# Record the error, try to press on
|
||||||
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
|
Error.objects.create(
|
||||||
|
kind=kind.__name__,
|
||||||
|
info=info,
|
||||||
|
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||||
|
path='order.get_total_price',
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(f"Missing exchange rate for '{target_currency}'")
|
||||||
|
|
||||||
|
# Return None to indicate the calculated price is invalid
|
||||||
|
return None
|
||||||
|
|
||||||
# set decimal-places
|
# set decimal-places
|
||||||
total.decimal_places = 4
|
total.decimal_places = 4
|
||||||
|
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
@ -1202,6 +1260,20 @@ class SalesOrderShipment(models.Model):
|
|||||||
help_text=_('Shipment tracking information'),
|
help_text=_('Shipment tracking information'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
invoice_number = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
unique=False,
|
||||||
|
verbose_name=_('Invoice Number'),
|
||||||
|
help_text=_('Reference number for associated invoice'),
|
||||||
|
)
|
||||||
|
|
||||||
|
link = models.URLField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('Link'),
|
||||||
|
help_text=_('Link to external page')
|
||||||
|
)
|
||||||
|
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
return self.shipment_date is not None
|
return self.shipment_date is not None
|
||||||
|
|
||||||
@ -1253,6 +1325,18 @@ class SalesOrderShipment(models.Model):
|
|||||||
if tracking_number is not None:
|
if tracking_number is not None:
|
||||||
self.tracking_number = tracking_number
|
self.tracking_number = tracking_number
|
||||||
|
|
||||||
|
# Was an invoice number provided?
|
||||||
|
invoice_number = kwargs.get('invoice_number', None)
|
||||||
|
|
||||||
|
if invoice_number is not None:
|
||||||
|
self.invoice_number = invoice_number
|
||||||
|
|
||||||
|
# Was a link provided?
|
||||||
|
link = kwargs.get('link', None)
|
||||||
|
|
||||||
|
if link is not None:
|
||||||
|
self.link = link
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
trigger_event('salesordershipment.completed', id=self.pk)
|
trigger_event('salesordershipment.completed', id=self.pk)
|
||||||
|
@ -886,6 +886,8 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
|
|||||||
'checked_by',
|
'checked_by',
|
||||||
'reference',
|
'reference',
|
||||||
'tracking_number',
|
'tracking_number',
|
||||||
|
'invoice_number',
|
||||||
|
'link',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -899,8 +901,10 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
|||||||
model = order.models.SalesOrderShipment
|
model = order.models.SalesOrderShipment
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'tracking_number',
|
|
||||||
'shipment_date',
|
'shipment_date',
|
||||||
|
'tracking_number',
|
||||||
|
'invoice_number',
|
||||||
|
'link',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -928,15 +932,14 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
|||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
# Extract provided tracking number (optional)
|
|
||||||
tracking_number = data.get('tracking_number', shipment.tracking_number)
|
|
||||||
|
|
||||||
# Extract shipping date (defaults to today's date)
|
# Extract shipping date (defaults to today's date)
|
||||||
shipment_date = data.get('shipment_date', datetime.now())
|
shipment_date = data.get('shipment_date', datetime.now())
|
||||||
|
|
||||||
shipment.complete_shipment(
|
shipment.complete_shipment(
|
||||||
user,
|
user,
|
||||||
tracking_number=tracking_number,
|
tracking_number=data.get('tracking_number', shipment.tracking_number),
|
||||||
|
invoice_number=data.get('invoice_number', shipment.invoice_number),
|
||||||
|
link=data.get('link', shipment.link),
|
||||||
shipment_date=shipment_date,
|
shipment_date=shipment_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -181,7 +181,15 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-dollar-sign'></span></td>
|
<td><span class='fas fa-dollar-sign'></span></td>
|
||||||
<td>{% trans "Total cost" %}</td>
|
<td>{% trans "Total cost" %}</td>
|
||||||
<td id="poTotalPrice">{{ order.get_total_price }}</td>
|
<td id="poTotalPrice">
|
||||||
|
{% with order.get_total_price as tp %}
|
||||||
|
{% if tp == None %}
|
||||||
|
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ tp }}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -191,11 +191,7 @@ $('#new-po-line').click(function() {
|
|||||||
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
{% elif order.status == PurchaseOrderStatus.PLACED %}
|
||||||
|
|
||||||
$('#receive-selected-items').click(function() {
|
$('#receive-selected-items').click(function() {
|
||||||
var items = $("#po-line-table").bootstrapTable('getSelections');
|
var items = getTableData('#po-line-table');
|
||||||
|
|
||||||
if (items.length == 0) {
|
|
||||||
items = $("#po-line-table").bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
receivePurchaseOrderItems(
|
receivePurchaseOrderItems(
|
||||||
{{ order.id }},
|
{{ order.id }},
|
||||||
|
@ -31,12 +31,6 @@
|
|||||||
<span class='fas fa-print'></span>
|
<span class='fas fa-print'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
|
||||||
<span class='fas fa-calendar-alt'></span>
|
|
||||||
</button>
|
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-list' title='{% trans "Display list view" %}'>
|
|
||||||
<span class='fas fa-th-list'></span>
|
|
||||||
</button>
|
|
||||||
{% include "filter_list.html" with id="purchaseorder" %}
|
{% include "filter_list.html" with id="purchaseorder" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,125 +48,14 @@
|
|||||||
{% block js_load %}
|
{% block js_load %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
<script type='text/javascript'>
|
|
||||||
|
|
||||||
function loadOrderEvents(calendar) {
|
|
||||||
|
|
||||||
var start = startDate(calendar);
|
|
||||||
var end = endDate(calendar);
|
|
||||||
|
|
||||||
clearEvents(calendar);
|
|
||||||
|
|
||||||
// Request purchase orders from the server within specified date range
|
|
||||||
inventreeGet(
|
|
||||||
'{% url "api-po-list" %}',
|
|
||||||
{
|
|
||||||
supplier_detail: true,
|
|
||||||
min_date: start,
|
|
||||||
max_date: end,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
|
|
||||||
|
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
|
||||||
|
|
||||||
var order = response[idx];
|
|
||||||
|
|
||||||
var date = order.creation_date;
|
|
||||||
|
|
||||||
if (order.complete_date) {
|
|
||||||
date = order.complete_date;
|
|
||||||
} else if (order.target_date) {
|
|
||||||
date = order.target_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
|
||||||
|
|
||||||
var color = '#4c68f5';
|
|
||||||
|
|
||||||
if (order.complete_date) {
|
|
||||||
color = '#25c235';
|
|
||||||
} else if (order.overdue) {
|
|
||||||
color = '#c22525';
|
|
||||||
} else {
|
|
||||||
color = '#4c68f5';
|
|
||||||
}
|
|
||||||
|
|
||||||
var event = {
|
|
||||||
title: title,
|
|
||||||
start: date,
|
|
||||||
end: date,
|
|
||||||
url: `/order/purchase-order/${order.pk}/`,
|
|
||||||
backgroundColor: color,
|
|
||||||
};
|
|
||||||
|
|
||||||
calendar.addEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var calendar = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var el = document.getElementById('purchase-order-calendar');
|
|
||||||
|
|
||||||
calendar = new FullCalendar.Calendar(el, {
|
|
||||||
initialView: 'dayGridMonth',
|
|
||||||
nowIndicator: true,
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
locale: '{{request.LANGUAGE_CODE}}',
|
|
||||||
datesSet: function() {
|
|
||||||
loadOrderEvents(calendar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#purchase-order-calendar').hide();
|
|
||||||
$('#view-list').hide();
|
|
||||||
|
|
||||||
$('#view-calendar').click(function() {
|
|
||||||
// Hide the list view, show the calendar view
|
|
||||||
$("#purchase-order-table").hide();
|
|
||||||
$("#view-calendar").hide();
|
|
||||||
$(".fixed-table-pagination").hide();
|
|
||||||
$(".columns-right").hide();
|
|
||||||
$(".search").hide();
|
|
||||||
$('#filter-list-salesorder').hide();
|
|
||||||
|
|
||||||
$("#purchase-order-calendar").show();
|
|
||||||
$("#view-list").show();
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
|
||||||
// Hide the calendar view, show the list view
|
|
||||||
$("#purchase-order-calendar").hide();
|
|
||||||
$("#view-list").hide();
|
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
|
||||||
$(".columns-right").show();
|
|
||||||
$(".search").show();
|
|
||||||
$("#purchase-order-table").show();
|
|
||||||
$('#filter-list-salesorder').show();
|
|
||||||
$("#view-calendar").show();
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if report_enabled %}
|
{% if report_enabled %}
|
||||||
$("#order-print").click(function() {
|
$("#order-print").click(function() {
|
||||||
var rows = $("#purchase-order-table").bootstrapTable('getSelections');
|
var rows = getTableData('#purchase-order-table');
|
||||||
|
|
||||||
var orders = [];
|
var orders = [];
|
||||||
|
|
||||||
|
@ -188,7 +188,15 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-dollar-sign'></span></td>
|
<td><span class='fas fa-dollar-sign'></span></td>
|
||||||
<td>{% trans "Total cost" %}</td>
|
<td>{% trans "Total cost" %}</td>
|
||||||
<td id="soTotalPrice">{{ order.get_total_price }}</td>
|
<td id="soTotalPrice">
|
||||||
|
{% with order.get_total_price as tp %}
|
||||||
|
{% if tp == None %}
|
||||||
|
<span class='badge bg-warning'>{% trans "Total cost could not be calculated" %}</span>
|
||||||
|
{% else %}
|
||||||
|
{{ tp }}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -231,7 +231,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
loadBuildTable($("#builds-table"), {
|
loadBuildTable($("#builds-table"), {
|
||||||
url: "{% url 'api-build-list' %}",
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
params: {
|
params: {
|
||||||
sales_order: {{ order.id }},
|
sales_order: {{ order.id }},
|
||||||
},
|
},
|
||||||
|
@ -34,12 +34,6 @@
|
|||||||
<span class='fas fa-print'></span>
|
<span class='fas fa-print'></span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-calendar' title='{% trans "Display calendar view" %}'>
|
|
||||||
<span class='fas fa-calendar-alt'></span>
|
|
||||||
</button>
|
|
||||||
<button class='btn btn-outline-secondary' type='button' id='view-list' title='{% trans "Display list view" %}'>
|
|
||||||
<span class='fas fa-th-list'></span>
|
|
||||||
</button>
|
|
||||||
{% include "filter_list.html" with id="salesorder" %}
|
{% include "filter_list.html" with id="salesorder" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,130 +47,16 @@
|
|||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_load %}
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
<script type='text/javascript'>
|
|
||||||
|
|
||||||
function loadOrderEvents(calendar) {
|
|
||||||
|
|
||||||
var start = startDate(calendar);
|
|
||||||
var end = endDate(calendar);
|
|
||||||
|
|
||||||
clearEvents(calendar);
|
|
||||||
|
|
||||||
// Request orders from the server within specified date range
|
|
||||||
inventreeGet(
|
|
||||||
'{% url "api-so-list" %}',
|
|
||||||
{
|
|
||||||
customer_detail: true,
|
|
||||||
min_date: start,
|
|
||||||
max_date: end,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
success: function(response) {
|
|
||||||
|
|
||||||
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
|
||||||
|
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
|
||||||
var order = response[idx];
|
|
||||||
|
|
||||||
var date = order.creation_date;
|
|
||||||
|
|
||||||
if (order.shipment_date) {
|
|
||||||
date = order.shipment_date;
|
|
||||||
} else if (order.target_date) {
|
|
||||||
date = order.target_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = `${prefix}${order.reference} - ${order.customer_detail.name}`;
|
|
||||||
|
|
||||||
// Default color is blue
|
|
||||||
var color = '#4c68f5';
|
|
||||||
|
|
||||||
// Overdue orders are red
|
|
||||||
if (order.overdue) {
|
|
||||||
color = '#c22525';
|
|
||||||
} else if (order.status == {{ SalesOrderStatus.SHIPPED }}) {
|
|
||||||
color = '#25c235';
|
|
||||||
}
|
|
||||||
|
|
||||||
var event = {
|
|
||||||
title: title,
|
|
||||||
start: date,
|
|
||||||
end: date,
|
|
||||||
url: `/order/sales-order/${order.pk}/`,
|
|
||||||
backgroundColor: color,
|
|
||||||
};
|
|
||||||
|
|
||||||
calendar.addEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var calendar = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
var calendarEl = document.getElementById('sales-order-calendar');
|
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
|
||||||
initialView: 'dayGridMonth',
|
|
||||||
nowIndicator: true,
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
locale: '{{request.LANGUAGE_CODE}}',
|
|
||||||
datesSet: function() {
|
|
||||||
loadOrderEvents(calendar);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$("#sales-order-calendar").hide();
|
|
||||||
$("#view-list").hide();
|
|
||||||
|
|
||||||
$('#view-calendar').click(function() {
|
|
||||||
// Hide the list view, show the calendar view
|
|
||||||
$("#sales-order-table").hide();
|
|
||||||
$("#view-calendar").hide();
|
|
||||||
$(".fixed-table-pagination").hide();
|
|
||||||
$(".columns-right").hide();
|
|
||||||
$(".search").hide();
|
|
||||||
$('#filter-list-salesorder').hide();
|
|
||||||
|
|
||||||
$("#sales-order-calendar").show();
|
|
||||||
$("#view-list").show();
|
|
||||||
|
|
||||||
calendar.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#view-list").click(function() {
|
|
||||||
// Hide the calendar view, show the list view
|
|
||||||
$("#sales-order-calendar").hide();
|
|
||||||
$("#view-list").hide();
|
|
||||||
|
|
||||||
$(".fixed-table-pagination").show();
|
|
||||||
$(".columns-right").show();
|
|
||||||
$(".search").show();
|
|
||||||
$("#sales-order-table").show();
|
|
||||||
$('#filter-list-salesorder').show();
|
|
||||||
$("#view-calendar").show();
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSalesOrderTable("#sales-order-table", {
|
loadSalesOrderTable("#sales-order-table", {
|
||||||
url: "{% url 'api-so-list' %}",
|
url: "{% url 'api-so-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if report_enabled %}
|
{% if report_enabled %}
|
||||||
$("#order-print").click(function() {
|
$("#order-print").click(function() {
|
||||||
var rows = $("#sales-order-table").bootstrapTable('getSelections');
|
var rows = getTableData('#sales-order-table');
|
||||||
|
|
||||||
var orders = [];
|
var orders = [];
|
||||||
|
|
||||||
|
@ -1324,6 +1324,8 @@ class SalesOrderAllocateTest(OrderTest):
|
|||||||
response = self.post(
|
response = self.post(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
|
'invoice_number': 'INV01234',
|
||||||
|
'link': 'http://test.com/link.html',
|
||||||
'tracking_number': 'TRK12345',
|
'tracking_number': 'TRK12345',
|
||||||
'shipment_date': '2020-12-05',
|
'shipment_date': '2020-12-05',
|
||||||
},
|
},
|
||||||
@ -1334,6 +1336,8 @@ class SalesOrderAllocateTest(OrderTest):
|
|||||||
|
|
||||||
self.assertTrue(self.shipment.is_complete())
|
self.assertTrue(self.shipment.is_complete())
|
||||||
self.assertEqual(self.shipment.tracking_number, 'TRK12345')
|
self.assertEqual(self.shipment.tracking_number, 'TRK12345')
|
||||||
|
self.assertEqual(self.shipment.invoice_number, 'INV01234')
|
||||||
|
self.assertEqual(self.shipment.link, 'http://test.com/link.html')
|
||||||
|
|
||||||
def test_sales_order_shipment_list(self):
|
def test_sales_order_shipment_list(self):
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ def notify_low_stock(part: part.models.Part):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
common.notifications.trigger_notifaction(
|
common.notifications.trigger_notification(
|
||||||
part,
|
part,
|
||||||
'part.notify_low_stock',
|
'part.notify_low_stock',
|
||||||
target_fnc=part.get_subscribers,
|
target_fnc=part.get_subscribers,
|
||||||
|
@ -360,7 +360,9 @@
|
|||||||
<div id='opt-dropdown' class="btn-group">
|
<div id='opt-dropdown' class="btn-group">
|
||||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>
|
||||||
|
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}
|
||||||
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% include "filter_list.html" with id="supplier-part" %}
|
{% include "filter_list.html" with id="supplier-part" %}
|
||||||
@ -443,27 +445,11 @@
|
|||||||
|
|
||||||
$("#supplier-part-delete").click(function() {
|
$("#supplier-part-delete").click(function() {
|
||||||
|
|
||||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
var selections = getTableData('#supplier-part-table');
|
||||||
|
|
||||||
var requests = [];
|
deleteSupplierParts(selections, {
|
||||||
|
success: reloadSupplierPartTable,
|
||||||
showQuestionDialog(
|
});
|
||||||
'{% trans "Delete Supplier Parts?" %}',
|
|
||||||
'{% trans "All selected supplier parts will be deleted" %}',
|
|
||||||
{
|
|
||||||
accept: function() {
|
|
||||||
selections.forEach(function(part) {
|
|
||||||
var url = `/api/company/part/${part.pk}/`;
|
|
||||||
|
|
||||||
requests.push(inventreeDelete(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
reloadSupplierPartTable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSupplierPartTable(
|
loadSupplierPartTable(
|
||||||
@ -472,7 +458,7 @@
|
|||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
part: {{ part.id }},
|
part: {{ part.id }},
|
||||||
part_detail: false,
|
part_detail: true,
|
||||||
supplier_detail: true,
|
supplier_detail: true,
|
||||||
manufacturer_detail: true,
|
manufacturer_detail: true,
|
||||||
},
|
},
|
||||||
@ -497,10 +483,10 @@
|
|||||||
|
|
||||||
$("#manufacturer-part-delete").click(function() {
|
$("#manufacturer-part-delete").click(function() {
|
||||||
|
|
||||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
var selectionss = getTableData('#manufacturer-part-table');
|
||||||
|
|
||||||
deleteManufacturerParts(selections, {
|
deleteManufacturerParts(selections, {
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
$("#manufacturer-part-table").bootstrapTable("refresh");
|
$("#manufacturer-part-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -528,7 +514,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
loadBuildTable($("#build-table"), {
|
loadBuildTable($("#build-table"), {
|
||||||
url: "{% url 'api-build-list' %}",
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
params: {
|
params: {
|
||||||
part: {{ part.id }},
|
part: {{ part.id }},
|
||||||
}
|
}
|
||||||
@ -587,11 +573,7 @@
|
|||||||
$('#bom-item-delete').click(function() {
|
$('#bom-item-delete').click(function() {
|
||||||
|
|
||||||
// Get a list of the selected BOM items
|
// Get a list of the selected BOM items
|
||||||
var rows = $("#bom-table").bootstrapTable('getSelections');
|
var rows = getTableData('#bom-table');
|
||||||
|
|
||||||
if (rows.length == 0) {
|
|
||||||
rows = $('#bom-table').bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBomItems(rows, {
|
deleteBomItems(rows, {
|
||||||
success: function() {
|
success: function() {
|
||||||
|
@ -1098,7 +1098,7 @@ class PartDetailTests(InvenTreeAPITestCase):
|
|||||||
self.assertIn('Upload a valid image', str(response.data))
|
self.assertIn('Upload a valid image', str(response.data))
|
||||||
|
|
||||||
# Now try to upload a valid image file, in multiple formats
|
# Now try to upload a valid image file, in multiple formats
|
||||||
for fmt in ['jpg', 'png', 'bmp', 'webp']:
|
for fmt in ['jpg', 'j2k', 'png', 'bmp', 'webp']:
|
||||||
fn = f'dummy_image.{fmt}'
|
fn = f'dummy_image.{fmt}'
|
||||||
|
|
||||||
img = PIL.Image.new('RGB', (128, 128), color='red')
|
img = PIL.Image.new('RGB', (128, 128), color='red')
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
"""Functions to print a label to a mixin printer"""
|
"""Functions to print a label to a mixin printer"""
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.debug import ExceptionReporter
|
||||||
|
|
||||||
|
import pdf2image
|
||||||
|
from error_report.models import Error
|
||||||
|
|
||||||
import common.notifications
|
import common.notifications
|
||||||
from plugin.registry import registry
|
from plugin.registry import registry
|
||||||
@ -9,7 +16,7 @@ from plugin.registry import registry
|
|||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
def print_label(plugin_slug, pdf_data, filename=None, label_instance=None, user=None):
|
||||||
"""
|
"""
|
||||||
Print label with the provided plugin.
|
Print label with the provided plugin.
|
||||||
|
|
||||||
@ -19,10 +26,11 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
|||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
plugin_slug: The unique slug (key) of the plugin
|
plugin_slug: The unique slug (key) of the plugin
|
||||||
label_image: A PIL.Image image object to be printed
|
pdf_data: Binary PDF data
|
||||||
|
filename: The intended name of the printed label
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(f"Plugin '{plugin_slug}' is printing a label")
|
logger.info(f"Plugin '{plugin_slug}' is printing a label '{filename}'")
|
||||||
|
|
||||||
plugin = registry.plugins.get(plugin_slug, None)
|
plugin = registry.plugins.get(plugin_slug, None)
|
||||||
|
|
||||||
@ -30,8 +38,22 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
|||||||
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
logger.error(f"Could not find matching plugin for '{plugin_slug}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# In addition to providing a .pdf image, we'll also provide a .png file
|
||||||
|
png_file = pdf2image.convert_from_bytes(
|
||||||
|
pdf_data,
|
||||||
|
dpi=300,
|
||||||
|
)[0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plugin.print_label(label_image, width=label_instance.width, height=label_instance.height)
|
plugin.print_label(
|
||||||
|
pdf_data=pdf_data,
|
||||||
|
png_file=png_file,
|
||||||
|
filename=filename,
|
||||||
|
label_instance=label_instance,
|
||||||
|
width=label_instance.width,
|
||||||
|
height=label_instance.height,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
# Plugin threw an error - notify the user who attempted to print
|
# Plugin threw an error - notify the user who attempted to print
|
||||||
|
|
||||||
@ -40,13 +62,28 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None):
|
|||||||
'message': str(e),
|
'message': str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(f"Label printing failed: Sending notification to user '{user}'")
|
# Log an error message to the database
|
||||||
|
kind, info, data = sys.exc_info()
|
||||||
|
|
||||||
|
Error.objects.create(
|
||||||
|
kind=kind.__name__,
|
||||||
|
info=info,
|
||||||
|
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||||
|
path='print_label',
|
||||||
|
html=ExceptionReporter(None, kind, info, data).get_traceback_html(),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(f"Label printing failed: Sending notification to user '{user}'") # pragma: no cover
|
||||||
|
|
||||||
# Throw an error against the plugin instance
|
# Throw an error against the plugin instance
|
||||||
common.notifications.trigger_notifaction(
|
common.notifications.trigger_notification(
|
||||||
plugin.plugin_config(),
|
plugin.plugin_config(),
|
||||||
'label.printing_failed',
|
'label.printing_failed',
|
||||||
targets=[user],
|
targets=[user],
|
||||||
context=ctx,
|
context=ctx,
|
||||||
delivery_methods=[common.notifications.UIMessageNotification]
|
delivery_methods=set([common.notifications.UIMessageNotification])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.TESTING:
|
||||||
|
# If we are in testing mode, we want to know about this exception
|
||||||
|
raise e
|
||||||
|
@ -22,17 +22,18 @@ class LabelPrintingMixin:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('labels', True, __class__)
|
self.add_mixin('labels', True, __class__)
|
||||||
|
|
||||||
def print_label(self, label, **kwargs):
|
def print_label(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Callback to print a single label
|
Callback to print a single label
|
||||||
|
|
||||||
Arguments:
|
|
||||||
label: A black-and-white pillow Image object
|
|
||||||
|
|
||||||
kwargs:
|
kwargs:
|
||||||
length: The length of the label (in mm)
|
pdf_data: Raw PDF data of the rendered label
|
||||||
width: The width of the label (in mm)
|
png_file: An in-memory PIL image file, rendered at 300dpi
|
||||||
|
label_instance: The instance of the label model which triggered the print_label() method
|
||||||
|
width: The expected width of the label (in mm)
|
||||||
|
height: The expected height of the label (in mm)
|
||||||
|
filename: The filename of this PDF label
|
||||||
|
user: The user who printed this label
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Unimplemented (to be implemented by the particular plugin class)
|
# Unimplemented (to be implemented by the particular plugin class)
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
"""Unit tests for the label printing mixin"""
|
"""Unit tests for the label printing mixin"""
|
||||||
|
import os
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
from InvenTree.api_tester import InvenTreeAPITestCase
|
||||||
from label.models import PartLabel, StockItemLabel, StockLocationLabel
|
from label.models import PartLabel, StockItemLabel, StockLocationLabel
|
||||||
@ -68,7 +71,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
with self.assertRaises(MixinNotImplementedError):
|
with self.assertRaises(MixinNotImplementedError):
|
||||||
plugin = WrongPlugin()
|
plugin = WrongPlugin()
|
||||||
plugin.print_label('test')
|
plugin.print_label(filename='test')
|
||||||
|
|
||||||
def test_installed(self):
|
def test_installed(self):
|
||||||
"""Test that the sample printing plugin is installed"""
|
"""Test that the sample printing plugin is installed"""
|
||||||
@ -167,6 +170,21 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
# Print no part
|
# Print no part
|
||||||
self.get(self.do_url(None, plugin_ref, label), expected_code=400)
|
self.get(self.do_url(None, plugin_ref, label), expected_code=400)
|
||||||
|
|
||||||
|
# Test that the labels have been printed
|
||||||
|
# The sample labelling plugin simply prints to file
|
||||||
|
self.assertTrue(os.path.exists('label.pdf'))
|
||||||
|
|
||||||
|
# Read the raw .pdf data - ensure it contains some sensible information
|
||||||
|
with open('label.pdf', 'rb') as f:
|
||||||
|
pdf_data = str(f.read())
|
||||||
|
self.assertIn('WeasyPrint', pdf_data)
|
||||||
|
|
||||||
|
# Check that the .png file has already been created
|
||||||
|
self.assertTrue(os.path.exists('label.png'))
|
||||||
|
|
||||||
|
# And that it is a valid image file
|
||||||
|
Image.open('label.png')
|
||||||
|
|
||||||
def test_printing_endpoints(self):
|
def test_printing_endpoints(self):
|
||||||
"""Cover the endpoints not covered by `test_printing_process`"""
|
"""Cover the endpoints not covered by `test_printing_process`"""
|
||||||
plugin_ref = 'samplelabel'
|
plugin_ref = 'samplelabel'
|
||||||
|
@ -86,9 +86,11 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
# region public functions
|
# region public functions
|
||||||
# region loading / unloading
|
# region loading / unloading
|
||||||
def load_plugins(self):
|
def load_plugins(self, full_reload: bool = False):
|
||||||
"""
|
"""Load and activate all IntegrationPlugins
|
||||||
Load and activate all IntegrationPlugins
|
|
||||||
|
Args:
|
||||||
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
"""
|
"""
|
||||||
if not settings.PLUGINS_ENABLED:
|
if not settings.PLUGINS_ENABLED:
|
||||||
# Plugins not enabled, do nothing
|
# Plugins not enabled, do nothing
|
||||||
@ -109,7 +111,7 @@ class PluginsRegistry:
|
|||||||
try:
|
try:
|
||||||
# We are using the db so for migrations etc we need to try this block
|
# We are using the db so for migrations etc we need to try this block
|
||||||
self._init_plugins(blocked_plugin)
|
self._init_plugins(blocked_plugin)
|
||||||
self._activate_plugins()
|
self._activate_plugins(full_reload=full_reload)
|
||||||
registered_successful = True
|
registered_successful = True
|
||||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||||
# Exception if the database has not been migrated yet
|
# Exception if the database has not been migrated yet
|
||||||
@ -123,7 +125,7 @@ class PluginsRegistry:
|
|||||||
# Initialize apps without any plugins
|
# Initialize apps without any plugins
|
||||||
self._clean_registry()
|
self._clean_registry()
|
||||||
self._clean_installed_apps()
|
self._clean_installed_apps()
|
||||||
self._activate_plugins(force_reload=True)
|
self._activate_plugins(force_reload=True, full_reload=full_reload)
|
||||||
|
|
||||||
# We do not want to end in an endless loop
|
# We do not want to end in an endless loop
|
||||||
retry_counter -= 1
|
retry_counter -= 1
|
||||||
@ -137,6 +139,10 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
# now the loading will re-start up with init
|
# now the loading will re-start up with init
|
||||||
|
|
||||||
|
# disable full reload after the first round
|
||||||
|
if full_reload:
|
||||||
|
full_reload = False
|
||||||
|
|
||||||
# Remove maintenance mode
|
# Remove maintenance mode
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
set_maintenance_mode(False)
|
set_maintenance_mode(False)
|
||||||
@ -170,9 +176,11 @@ class PluginsRegistry:
|
|||||||
set_maintenance_mode(False) # pragma: no cover
|
set_maintenance_mode(False) # pragma: no cover
|
||||||
logger.info('Finished unloading plugins')
|
logger.info('Finished unloading plugins')
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self, full_reload: bool = False):
|
||||||
"""
|
"""Safely reload IntegrationPlugins
|
||||||
Safely reload IntegrationPlugins
|
|
||||||
|
Args:
|
||||||
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Do not reload whe currently loading
|
# Do not reload whe currently loading
|
||||||
@ -183,7 +191,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
with maintenance_mode_on():
|
with maintenance_mode_on():
|
||||||
self.unload_plugins()
|
self.unload_plugins()
|
||||||
self.load_plugins()
|
self.load_plugins(full_reload)
|
||||||
|
|
||||||
logger.info('Finished reloading plugins')
|
logger.info('Finished reloading plugins')
|
||||||
|
|
||||||
@ -335,12 +343,12 @@ class PluginsRegistry:
|
|||||||
# save for later reference
|
# save for later reference
|
||||||
self.plugins_inactive[plug_key] = plugin_db_setting # pragma: no cover
|
self.plugins_inactive[plug_key] = plugin_db_setting # pragma: no cover
|
||||||
|
|
||||||
def _activate_plugins(self, force_reload=False):
|
def _activate_plugins(self, force_reload=False, full_reload: bool = False):
|
||||||
"""
|
"""Run activation functions for all plugins
|
||||||
Run activation functions for all plugins
|
|
||||||
|
|
||||||
:param force_reload: force reload base apps, defaults to False
|
Args:
|
||||||
:type force_reload: bool, optional
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
"""
|
"""
|
||||||
# activate integrations
|
# activate integrations
|
||||||
plugins = self.plugins.items()
|
plugins = self.plugins.items()
|
||||||
@ -348,7 +356,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
self.activate_plugin_settings(plugins)
|
self.activate_plugin_settings(plugins)
|
||||||
self.activate_plugin_schedule(plugins)
|
self.activate_plugin_schedule(plugins)
|
||||||
self.activate_plugin_app(plugins, force_reload=force_reload)
|
self.activate_plugin_app(plugins, force_reload=force_reload, full_reload=full_reload)
|
||||||
|
|
||||||
def _deactivate_plugins(self):
|
def _deactivate_plugins(self):
|
||||||
"""Run deactivation functions for all plugins"""
|
"""Run deactivation functions for all plugins"""
|
||||||
@ -432,15 +440,15 @@ class PluginsRegistry:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def activate_plugin_app(self, plugins, force_reload=False):
|
def activate_plugin_app(self, plugins, force_reload=False, full_reload: bool = False):
|
||||||
"""
|
"""Activate AppMixin plugins - add custom apps and reload
|
||||||
Activate AppMixin plugins - add custom apps and reload
|
|
||||||
|
|
||||||
:param plugins: list of IntegrationPlugins that should be installed
|
Args:
|
||||||
:type plugins: dict
|
plugins (dict): List of IntegrationPlugins that should be installed
|
||||||
:param force_reload: only reload base apps, defaults to False
|
force_reload (bool, optional): Only reload base apps. Defaults to False.
|
||||||
:type force_reload: bool, optional
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_APP'):
|
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_APP'):
|
||||||
@ -461,9 +469,9 @@ class PluginsRegistry:
|
|||||||
# first startup or force loading of base apps -> registry is prob false
|
# first startup or force loading of base apps -> registry is prob false
|
||||||
if self.apps_loading or force_reload:
|
if self.apps_loading or force_reload:
|
||||||
self.apps_loading = False
|
self.apps_loading = False
|
||||||
self._reload_apps(force_reload=True)
|
self._reload_apps(force_reload=True, full_reload=full_reload)
|
||||||
else:
|
else:
|
||||||
self._reload_apps()
|
self._reload_apps(full_reload=full_reload)
|
||||||
|
|
||||||
# rediscover models/ admin sites
|
# rediscover models/ admin sites
|
||||||
self._reregister_contrib_apps()
|
self._reregister_contrib_apps()
|
||||||
@ -589,8 +597,17 @@ class PluginsRegistry:
|
|||||||
global_pattern[0] = re_path('', include(urlpatterns))
|
global_pattern[0] = re_path('', include(urlpatterns))
|
||||||
clear_url_caches()
|
clear_url_caches()
|
||||||
|
|
||||||
def _reload_apps(self, force_reload: bool = False):
|
def _reload_apps(self, force_reload: bool = False, full_reload: bool = False):
|
||||||
self.is_loading = True # set flag to disable loop reloading
|
"""Internal: reload apps using django internal functions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If full_reloading is set to true we do not want to set the flag
|
||||||
|
if not full_reload:
|
||||||
|
self.is_loading = True # set flag to disable loop reloading
|
||||||
if force_reload:
|
if force_reload:
|
||||||
# we can not use the built in functions as we need to brute force the registry
|
# we can not use the built in functions as we need to brute force the registry
|
||||||
apps.app_configs = OrderedDict()
|
apps.app_configs = OrderedDict()
|
||||||
|
@ -12,7 +12,22 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
|
|||||||
SLUG = "samplelabel"
|
SLUG = "samplelabel"
|
||||||
TITLE = "Sample Label Printer"
|
TITLE = "Sample Label Printer"
|
||||||
DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
|
DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
|
||||||
VERSION = "0.1"
|
VERSION = "0.2"
|
||||||
|
|
||||||
def print_label(self, label, **kwargs):
|
def print_label(self, **kwargs):
|
||||||
print("OK PRINTING")
|
|
||||||
|
# Test that the expected kwargs are present
|
||||||
|
print(f"Printing Label: {kwargs['filename']} (User: {kwargs['user']})")
|
||||||
|
print(f"Width: {kwargs['width']} x Height: {kwargs['height']}")
|
||||||
|
|
||||||
|
pdf_data = kwargs['pdf_data']
|
||||||
|
png_file = kwargs['png_file']
|
||||||
|
|
||||||
|
filename = kwargs['filename']
|
||||||
|
|
||||||
|
# Dump the PDF to a local file
|
||||||
|
with open(filename, 'wb') as pdf_out:
|
||||||
|
pdf_out.write(pdf_data)
|
||||||
|
|
||||||
|
# Save the PNG to disk
|
||||||
|
png_file.save(filename.replace('.pdf', '.png'))
|
||||||
|
19
InvenTree/stock/migrations/0076_alter_stockitem_status.py
Normal file
19
InvenTree/stock/migrations/0076_alter_stockitem_status.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.13 on 2022-05-27 04:40
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0075_auto_20220515_1440'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (65, 'Rejected'), (75, 'Quarantined'), (85, 'Returned')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||||
|
),
|
||||||
|
]
|
@ -156,7 +156,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% if item.uid %}
|
{% if item.uid %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-barcode'></span></td>
|
<td><span class='fas fa-barcode'></span></td>
|
||||||
@ -452,7 +452,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
{% endblock details_right %}
|
{% endblock details_right %}
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@
|
|||||||
|
|
||||||
$('#multi-location-print-label').click(function() {
|
$('#multi-location-print-label').click(function() {
|
||||||
|
|
||||||
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
var selections = getTableData('#sublocation-table');
|
||||||
|
|
||||||
var locations = [];
|
var locations = [];
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ addHeaderTitle('{% trans "Build Orders" %}');
|
|||||||
{% if setting_build_pending %}
|
{% if setting_build_pending %}
|
||||||
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
addHeaderAction('build-pending', '{% trans "Build Orders In Progress" %}', 'fa-cogs');
|
||||||
loadBuildTable("#table-build-pending", {
|
loadBuildTable("#table-build-pending", {
|
||||||
url: "{% url 'api-build-list' %}",
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
params: {
|
params: {
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
@ -234,7 +234,7 @@ loadBuildTable("#table-build-pending", {
|
|||||||
{% if setting_build_overdue %}
|
{% if setting_build_overdue %}
|
||||||
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
addHeaderAction('build-overdue', '{% trans "Overdue Build Orders" %}', 'fa-calendar-times');
|
||||||
loadBuildTable("#table-build-overdue", {
|
loadBuildTable("#table-build-overdue", {
|
||||||
url: "{% url 'api-build-list' %}",
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
params: {
|
params: {
|
||||||
overdue: true,
|
overdue: true,
|
||||||
},
|
},
|
||||||
|
@ -139,6 +139,7 @@
|
|||||||
addItem('build-order', '{% trans "Build Orders" %}', 'fa-tools');
|
addItem('build-order', '{% trans "Build Orders" %}', 'fa-tools');
|
||||||
|
|
||||||
loadBuildTable('#table-build-order', {
|
loadBuildTable('#table-build-order', {
|
||||||
|
locale: '{{ request.LANGUAGE_CODE }}',
|
||||||
params: {
|
params: {
|
||||||
original_search: '{{ query }}',
|
original_search: '{{ query }}',
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
/* exported
|
/* exported
|
||||||
inventreeGet,
|
inventreeGet,
|
||||||
inventreeDelete,
|
inventreeDelete,
|
||||||
|
inventreeMultiDelete,
|
||||||
inventreeFormDataUpload,
|
inventreeFormDataUpload,
|
||||||
showApiError,
|
showApiError,
|
||||||
*/
|
*/
|
||||||
@ -171,6 +172,50 @@ function inventreeDelete(url, options={}) {
|
|||||||
return inventreePut(url, {}, options);
|
return inventreePut(url, {}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Perform a 'multi delete' operation:
|
||||||
|
*
|
||||||
|
* - Items are deleted sequentially from the database, rather than simultaneous requests
|
||||||
|
* - This prevents potential overload / transaction issues in the DB backend
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Assumes that each item in the 'items' list has a parameter 'pk'
|
||||||
|
*/
|
||||||
|
function inventreeMultiDelete(url, items, options={}) {
|
||||||
|
|
||||||
|
if (!url.endsWith('/')) {
|
||||||
|
url += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function doNextDelete() {
|
||||||
|
if (items.length > 0) {
|
||||||
|
var item = items.shift();
|
||||||
|
|
||||||
|
inventreeDelete(`${url}${item.pk}/`, {
|
||||||
|
complete: doNextDelete
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (options.modal) {
|
||||||
|
$(options.modal).modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.success) {
|
||||||
|
options.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.modal) {
|
||||||
|
showModalSpinner(options.modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate the process
|
||||||
|
doNextDelete();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Display a notification with error information
|
* Display a notification with error information
|
||||||
*/
|
*/
|
||||||
|
@ -693,41 +693,20 @@ function deleteBomItems(items, options={}) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
constructFormBody({}, {
|
constructFormBody({}, {
|
||||||
|
method: 'DELETE',
|
||||||
title: '{% trans "Delete selected BOM items?" %}',
|
title: '{% trans "Delete selected BOM items?" %}',
|
||||||
fields: {},
|
fields: {},
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
submitText: '{% trans "Delete" %}',
|
|
||||||
submitClass: 'danger',
|
|
||||||
confirm: true,
|
|
||||||
onSubmit: function(fields, opts) {
|
onSubmit: function(fields, opts) {
|
||||||
// Individually send DELETE requests for each BOM item
|
|
||||||
// We do *not* send these all at once, to prevent overloading the server
|
|
||||||
|
|
||||||
// Show the progress spinner
|
inventreeMultiDelete(
|
||||||
$(opts.modal).find('#modal-progress-spinner').show();
|
'{% url "api-bom-list" %}',
|
||||||
|
items,
|
||||||
function deleteNextBomItem() {
|
{
|
||||||
|
modal: opts.modal,
|
||||||
if (items.length > 0) {
|
success: options.success,
|
||||||
|
|
||||||
var item = items.shift();
|
|
||||||
|
|
||||||
inventreeDelete(`/api/bom/${item.pk}/`,
|
|
||||||
{
|
|
||||||
complete: deleteNextBomItem,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Destroy this modal once all items are deleted
|
|
||||||
$(opts.modal).modal('hide');
|
|
||||||
|
|
||||||
if (options.success) {
|
|
||||||
options.success();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
deleteNextBomItem();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -834,12 +834,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
var subtable = $(`#output-sub-table-${pk}`);
|
var subtable = $(`#output-sub-table-${pk}`);
|
||||||
|
|
||||||
if (subtable.exists()) {
|
if (subtable.exists()) {
|
||||||
var rows = subtable.bootstrapTable('getSelections');
|
var rows = getTableData(`#output-sub-table-${pk}`);
|
||||||
|
|
||||||
// None selected? Use all!
|
|
||||||
if (rows.length == 0) {
|
|
||||||
rows = subtable.bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
allocateStockToBuild(
|
allocateStockToBuild(
|
||||||
build_info.pk,
|
build_info.pk,
|
||||||
@ -1291,11 +1286,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
|
|
||||||
// Complete multiple outputs
|
// Complete multiple outputs
|
||||||
$('#multi-output-complete').click(function() {
|
$('#multi-output-complete').click(function() {
|
||||||
var outputs = $(table).bootstrapTable('getSelections');
|
var outputs = getTableData(table);
|
||||||
|
|
||||||
if (outputs.length == 0) {
|
|
||||||
outputs = $(table).bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
completeBuildOutputs(
|
completeBuildOutputs(
|
||||||
build_info.pk,
|
build_info.pk,
|
||||||
@ -1314,11 +1305,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
|
|
||||||
// Delete multiple build outputs
|
// Delete multiple build outputs
|
||||||
$('#multi-output-delete').click(function() {
|
$('#multi-output-delete').click(function() {
|
||||||
var outputs = $(table).bootstrapTable('getSelections');
|
var outputs = getTableData(table);
|
||||||
|
|
||||||
if (outputs.length == 0) {
|
|
||||||
outputs = $(table).bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBuildOutputs(
|
deleteBuildOutputs(
|
||||||
build_info.pk,
|
build_info.pk,
|
||||||
@ -1337,11 +1324,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
|||||||
|
|
||||||
// Print stock item labels
|
// Print stock item labels
|
||||||
$('#incomplete-output-print-label').click(function() {
|
$('#incomplete-output-print-label').click(function() {
|
||||||
var outputs = $(table).bootstrapTable('getSelections');
|
var outputs = getTableData(table);
|
||||||
|
|
||||||
if (outputs.length == 0) {
|
|
||||||
outputs = $(table).bootstrapTable('getData');
|
|
||||||
}
|
|
||||||
|
|
||||||
var stock_id_values = [];
|
var stock_id_values = [];
|
||||||
|
|
||||||
@ -2337,6 +2320,9 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
|
|||||||
*/
|
*/
|
||||||
function loadBuildTable(table, options) {
|
function loadBuildTable(table, options) {
|
||||||
|
|
||||||
|
// Ensure the table starts in a known state
|
||||||
|
$(table).bootstrapTable('destroy');
|
||||||
|
|
||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
|
|
||||||
var filters = {};
|
var filters = {};
|
||||||
@ -2351,23 +2337,105 @@ function loadBuildTable(table, options) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
options.url = options.url || '{% url "api-build-list" %}';
|
|
||||||
|
|
||||||
var filterTarget = options.filterTarget || null;
|
var filterTarget = options.filterTarget || null;
|
||||||
|
|
||||||
setupFilterList('build', table, filterTarget, {download: true});
|
setupFilterList('build', table, filterTarget, {download: true});
|
||||||
|
|
||||||
|
// Which display mode to use for the build table?
|
||||||
|
var display_mode = inventreeLoad('build-table-display-mode', 'list');
|
||||||
|
var tree_enable = display_mode == 'tree';
|
||||||
|
|
||||||
|
var loaded_calendar = false;
|
||||||
|
|
||||||
|
// Function for rendering BuildOrder calendar display
|
||||||
|
function buildEvents(calendar) {
|
||||||
|
var start = startDate(calendar);
|
||||||
|
var end = endDate(calendar);
|
||||||
|
|
||||||
|
clearEvents(calendar);
|
||||||
|
|
||||||
|
// Extract current filters from table
|
||||||
|
var table_options = $(table).bootstrapTable('getOptions');
|
||||||
|
var filters = table_options.query_params || {};
|
||||||
|
|
||||||
|
filters.min_date = start;
|
||||||
|
filters.max_date = end;
|
||||||
|
filters.part_detail = true;
|
||||||
|
|
||||||
|
// Request build orders from the server within specified date range
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-build-list" %}',
|
||||||
|
filters,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
|
||||||
|
var order = response[idx];
|
||||||
|
|
||||||
|
var date = order.creation_date;
|
||||||
|
|
||||||
|
if (order.completion_date) {
|
||||||
|
date = order.completion_date;
|
||||||
|
} else if (order.target_date) {
|
||||||
|
date = order.target_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = `${prefix}${order.reference}`;
|
||||||
|
|
||||||
|
var color = '#4c68f5';
|
||||||
|
|
||||||
|
if (order.completed) {
|
||||||
|
color = '#25c234';
|
||||||
|
} else if (order.overdue) {
|
||||||
|
color = '#c22525';
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = {
|
||||||
|
title: title,
|
||||||
|
start: date,
|
||||||
|
end: date,
|
||||||
|
url: `/build/${order.pk}/`,
|
||||||
|
backgroundColor: color,
|
||||||
|
};
|
||||||
|
|
||||||
|
calendar.addEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No builds matching query" %}';
|
return '{% trans "No builds matching query" %}';
|
||||||
},
|
},
|
||||||
url: options.url,
|
url: '{% url "api-build-list" %}',
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
groupBy: false,
|
groupBy: false,
|
||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
name: 'builds',
|
name: 'builds',
|
||||||
original: params,
|
original: params,
|
||||||
|
treeEnable: tree_enable,
|
||||||
|
uniqueId: 'pk',
|
||||||
|
rootParentId: options.parentBuild || null,
|
||||||
|
idField: 'pk',
|
||||||
|
parentIdField: 'parent',
|
||||||
|
treeShowField: tree_enable ? 'reference' : null,
|
||||||
|
showColumns: display_mode == 'list' || display_mode == 'tree',
|
||||||
|
showCustomView: display_mode == 'calendar',
|
||||||
|
showCustomViewButton: false,
|
||||||
|
disablePagination: display_mode == 'calendar',
|
||||||
|
search: display_mode != 'calendar',
|
||||||
|
buttons: constructOrderTableButtons({
|
||||||
|
prefix: 'build',
|
||||||
|
callback: function() {
|
||||||
|
// Force complete reload of the table
|
||||||
|
loadBuildTable(table, options);
|
||||||
|
}
|
||||||
|
}),
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
@ -2494,6 +2562,43 @@ function loadBuildTable(table, options) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
customView: function(data) {
|
||||||
|
return `<div id='build-order-calendar'></div>`;
|
||||||
|
},
|
||||||
|
onRefresh: function() {
|
||||||
|
loadBuildTable(table, options);
|
||||||
|
},
|
||||||
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
|
if (tree_enable) {
|
||||||
|
$(table).treegrid({
|
||||||
|
treeColumn: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
table.treegrid('expandAll');
|
||||||
|
} else if (display_mode == 'calendar') {
|
||||||
|
|
||||||
|
if (!loaded_calendar) {
|
||||||
|
loaded_calendar = true;
|
||||||
|
|
||||||
|
var el = document.getElementById('build-order-calendar');
|
||||||
|
|
||||||
|
calendar = new FullCalendar.Calendar(el, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
nowIndicator: true,
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
locale: options.locale,
|
||||||
|
datesSet: function() {
|
||||||
|
buildEvents(calendar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
} else {
|
||||||
|
calendar.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
linkButtonsToSelection(
|
linkButtonsToSelection(
|
||||||
|
@ -3,12 +3,11 @@
|
|||||||
/* globals
|
/* globals
|
||||||
constructForm,
|
constructForm,
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
inventreeDelete,
|
inventreeMultiDelete,
|
||||||
loadTableFilters,
|
loadTableFilters,
|
||||||
makeIconButton,
|
makeIconButton,
|
||||||
renderLink,
|
renderLink,
|
||||||
setupFilterList,
|
setupFilterList,
|
||||||
showQuestionDialog,
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
@ -16,6 +15,8 @@
|
|||||||
createManufacturerPart,
|
createManufacturerPart,
|
||||||
createSupplierPart,
|
createSupplierPart,
|
||||||
deleteManufacturerParts,
|
deleteManufacturerParts,
|
||||||
|
deleteManufacturerPartParameters,
|
||||||
|
deleteSupplierParts,
|
||||||
editCompany,
|
editCompany,
|
||||||
loadCompanyTable,
|
loadCompanyTable,
|
||||||
loadManufacturerPartTable,
|
loadManufacturerPartTable,
|
||||||
@ -101,15 +102,6 @@ function editManufacturerPart(part, options={}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteManufacturerPart(part, options={}) {
|
|
||||||
|
|
||||||
constructForm(`/api/company/part/manufacturer/${part}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
title: '{% trans "Delete Manufacturer Part" %}',
|
|
||||||
onSuccess: options.onSuccess,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function supplierPartFields() {
|
function supplierPartFields() {
|
||||||
|
|
||||||
@ -211,12 +203,76 @@ function editSupplierPart(part, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deleteSupplierPart(part, options={}) {
|
/*
|
||||||
|
* Delete one or more SupplierPart objects from the database.
|
||||||
|
* - User will be provided with a modal form, showing all the parts to be deleted.
|
||||||
|
* - Delete operations are performed sequentialy, not simultaneously
|
||||||
|
*/
|
||||||
|
function deleteSupplierParts(parts, options={}) {
|
||||||
|
|
||||||
constructForm(`/api/company/part/${part}/`, {
|
if (parts.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPart(sup_part) {
|
||||||
|
var part = sup_part.part_detail;
|
||||||
|
var thumb = thumbnailImage(part.thumbnail || part.image);
|
||||||
|
var supplier = '-';
|
||||||
|
var MPN = '-';
|
||||||
|
|
||||||
|
if (sup_part.supplier_detail) {
|
||||||
|
supplier = sup_part.supplier_detail.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sup_part.manufacturer_part_detail) {
|
||||||
|
MPN = sup_part.manufacturer_part_detail.MPN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${thumb} ${part.full_name}</td>
|
||||||
|
<td>${sup_part.SKU}</td>
|
||||||
|
<td>${supplier}</td>
|
||||||
|
<td>${MPN}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = '';
|
||||||
|
|
||||||
|
parts.forEach(function(sup_part) {
|
||||||
|
rows += renderPart(sup_part);
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
{% trans "All selected supplier parts will be deleted" %}
|
||||||
|
</div>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Part" %}</th>
|
||||||
|
<th>{% trans "SKU" %}</th>
|
||||||
|
<th>{% trans "Supplier" %}</th>
|
||||||
|
<th>{% trans "MPN" %}</th>
|
||||||
|
</tr>
|
||||||
|
${rows}
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
title: '{% trans "Delete Supplier Part" %}',
|
title: '{% trans "Delete Supplier Parts" %}',
|
||||||
onSuccess: options.onSuccess,
|
preFormContent: html,
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
|
||||||
|
inventreeMultiDelete(
|
||||||
|
'{% url "api-supplier-part-list" %}',
|
||||||
|
parts,
|
||||||
|
{
|
||||||
|
modal: opts.modal,
|
||||||
|
success: options.success
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,58 +449,116 @@ function loadCompanyTable(table, url, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Delete one or more ManufacturerPart objects from the database.
|
||||||
|
* - User will be provided with a modal form, showing all the parts to be deleted.
|
||||||
|
* - Delete operations are performed sequentialy, not simultaneously
|
||||||
|
*/
|
||||||
function deleteManufacturerParts(selections, options={}) {
|
function deleteManufacturerParts(selections, options={}) {
|
||||||
|
|
||||||
if (selections.length == 0) {
|
if (selections.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = [];
|
function renderPart(man_part, opts={}) {
|
||||||
|
var part = man_part.part_detail;
|
||||||
|
var thumb = thumbnailImage(part.thumbnail || part.image);
|
||||||
|
|
||||||
var text = `
|
return `
|
||||||
<div class='alert alert-block alert-danger'>
|
<tr>
|
||||||
<p>{% trans "The following manufacturer parts will be deleted" %}:</p>
|
<td>${thumb} ${part.full_name}</td>
|
||||||
<ul>`;
|
<td>${man_part.MPN}</td>
|
||||||
|
<td>${man_part.manufacturer_detail.name}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
selections.forEach(function(item) {
|
var rows = '';
|
||||||
parts.push(item.pk);
|
|
||||||
|
|
||||||
text += `
|
selections.forEach(function(man_part) {
|
||||||
<li>
|
rows += renderPart(man_part);
|
||||||
<p>${item.MPN} - ${item.part_detail.full_name}</p>
|
|
||||||
</li>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
text += `
|
var html = `
|
||||||
</ul>
|
<div class='alert alert-block alert-danger'>
|
||||||
</div>`;
|
{% trans "All selected manufacturer parts will be deleted" %}
|
||||||
|
</div>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Part" %}</th>
|
||||||
|
<th>{% trans "MPN" %}</th>
|
||||||
|
<th>{% trans "Manufacturer" %}</th>
|
||||||
|
</tr>
|
||||||
|
${rows}
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
showQuestionDialog(
|
constructFormBody({}, {
|
||||||
'{% trans "Delete Manufacturer Parts" %}',
|
method: 'DELETE',
|
||||||
text,
|
title: '{% trans "Delete Manufacturer Parts" %}',
|
||||||
{
|
preFormContent: html,
|
||||||
accept_text: '{% trans "Delete" %}',
|
onSubmit: function(fields, opts) {
|
||||||
accept: function() {
|
|
||||||
|
|
||||||
// Delete each manufacturer part
|
inventreeMultiDelete(
|
||||||
var requests = [];
|
'{% url "api-manufacturer-part-list" %}',
|
||||||
|
selections,
|
||||||
parts.forEach(function(pk) {
|
{
|
||||||
var url = `/api/company/part/manufacturer/${pk}`;
|
modal: opts.modal,
|
||||||
|
success: options.success,
|
||||||
requests.push(inventreeDelete(url));
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Wait for all the requests to complete
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
|
|
||||||
if (options.onSuccess) {
|
|
||||||
options.onSuccess();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deleteManufacturerPartParameters(selections, options={}) {
|
||||||
|
|
||||||
|
if (selections.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderParam(param) {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${param.name}</td>
|
||||||
|
<td>${param.units}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = '';
|
||||||
|
|
||||||
|
selections.forEach(function(param) {
|
||||||
|
rows += renderParam(param);
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
{% trans "All selected parameters will be deleted" %}
|
||||||
|
</div>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Value" %}</th>
|
||||||
|
</tr>
|
||||||
|
${rows}
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
method: 'DELETE',
|
||||||
|
title: '{% trans "Delete Parameters" %}',
|
||||||
|
preFormContent: html,
|
||||||
|
onSubmit: function(fields, opts) {
|
||||||
|
inventreeMultiDelete(
|
||||||
|
'{% url "api-manufacturer-part-parameter-list" %}',
|
||||||
|
selections,
|
||||||
|
{
|
||||||
|
modal: opts.modal,
|
||||||
|
success: options.success,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -473,6 +587,7 @@ function loadManufacturerPartTable(table, url, options) {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
original: params,
|
original: params,
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
|
uniqueId: 'pk',
|
||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
name: 'manufacturerparts',
|
name: 'manufacturerparts',
|
||||||
groupBy: false,
|
groupBy: false,
|
||||||
@ -588,11 +703,12 @@ function loadManufacturerPartTable(table, url, options) {
|
|||||||
|
|
||||||
$(table).find('.button-manufacturer-part-delete').click(function() {
|
$(table).find('.button-manufacturer-part-delete').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
deleteManufacturerPart(
|
deleteManufacturerParts(
|
||||||
pk,
|
[row],
|
||||||
{
|
{
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -618,7 +734,7 @@ function loadManufacturerPartParameterTable(table, url, options) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupFilterList("manufacturer-part-parameters", $(table));
|
setupFilterList('manufacturer-part-parameters', $(table));
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: url,
|
url: url,
|
||||||
@ -730,6 +846,7 @@ function loadSupplierPartTable(table, url, options) {
|
|||||||
method: 'get',
|
method: 'get',
|
||||||
original: params,
|
original: params,
|
||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
|
uniqueId: 'pk',
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
name: 'supplierparts',
|
name: 'supplierparts',
|
||||||
groupBy: false,
|
groupBy: false,
|
||||||
@ -886,11 +1003,12 @@ function loadSupplierPartTable(table, url, options) {
|
|||||||
|
|
||||||
$(table).find('.button-supplier-part-delete').click(function() {
|
$(table).find('.button-supplier-part-delete').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
deleteSupplierPart(
|
deleteSupplierParts(
|
||||||
pk,
|
[row],
|
||||||
{
|
{
|
||||||
onSuccess: function() {
|
success: function() {
|
||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,11 +247,6 @@ function constructChangeForm(fields, options) {
|
|||||||
*/
|
*/
|
||||||
function constructDeleteForm(fields, options) {
|
function constructDeleteForm(fields, options) {
|
||||||
|
|
||||||
// Force the "confirm" property if not set
|
|
||||||
if (!('confirm' in options)) {
|
|
||||||
options.confirm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request existing data from the API endpoint
|
// Request existing data from the API endpoint
|
||||||
// This data can be used to render some information on the form
|
// This data can be used to render some information on the form
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -430,6 +425,21 @@ function constructFormBody(fields, options) {
|
|||||||
// otherwise *all* fields will be displayed
|
// otherwise *all* fields will be displayed
|
||||||
var displayed_fields = options.fields || fields;
|
var displayed_fields = options.fields || fields;
|
||||||
|
|
||||||
|
// Override default option values if a 'DELETE' form is specified
|
||||||
|
if (options.method == 'DELETE') {
|
||||||
|
if (!('confirm' in options)) {
|
||||||
|
options.confirm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('submitClass' in options)) {
|
||||||
|
options.submitClass = 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('submitText' in options)) {
|
||||||
|
options.submitText = '{% trans "Delete" %}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle initial data overrides
|
// Handle initial data overrides
|
||||||
if (options.data) {
|
if (options.data) {
|
||||||
for (const field in options.data) {
|
for (const field in options.data) {
|
||||||
@ -797,7 +807,7 @@ function submitFormData(fields, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show the progress spinner
|
// Show the progress spinner
|
||||||
$(options.modal).find('#modal-progress-spinner').show();
|
showModalSpinner(options.modal);
|
||||||
|
|
||||||
// Submit data
|
// Submit data
|
||||||
upload_func(
|
upload_func(
|
||||||
@ -2625,7 +2635,7 @@ function selectImportFields(url, data={}, options={}) {
|
|||||||
columns.push(getFormFieldValue(`column_${idx}`, {}, opts));
|
columns.push(getFormFieldValue(`column_${idx}`, {}, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
$(opts.modal).find('#modal-progress-spinner').show();
|
showModalSpinner(opts.modal);
|
||||||
|
|
||||||
inventreePut(
|
inventreePut(
|
||||||
opts.url,
|
opts.url,
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
showModalImage,
|
showModalImage,
|
||||||
removeRowFromModalForm,
|
removeRowFromModalForm,
|
||||||
showQuestionDialog,
|
showQuestionDialog,
|
||||||
|
showModalSpinner,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1146,3 +1147,13 @@ function showModalImage(image_url) {
|
|||||||
hideModalImage();
|
hideModalImage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Show (or hide) a progress spinner icon in the dialog */
|
||||||
|
function showModalSpinner(modal, show=true) {
|
||||||
|
if (show) {
|
||||||
|
$(modal).find('#modal-progress-spinner').show();
|
||||||
|
} else {
|
||||||
|
$(modal).find('#modal-progress-spinner').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -55,6 +55,12 @@ function salesOrderShipmentFields(options={}) {
|
|||||||
tracking_number: {
|
tracking_number: {
|
||||||
icon: 'fa-hashtag',
|
icon: 'fa-hashtag',
|
||||||
},
|
},
|
||||||
|
invoice_number: {
|
||||||
|
icon: 'fa-dollar-sign',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
icon: 'fa-link',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If order is specified, hide the order field
|
// If order is specified, hide the order field
|
||||||
@ -129,11 +135,20 @@ function completeShipment(shipment_id, options={}) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: `{% trans "Complete Shipment" %} ${shipment.reference}`,
|
title: `{% trans "Complete Shipment" %} ${shipment.reference}`,
|
||||||
fields: {
|
fields: {
|
||||||
tracking_number: {
|
|
||||||
value: shipment.tracking_number,
|
|
||||||
},
|
|
||||||
shipment_date: {
|
shipment_date: {
|
||||||
value: moment().format('YYYY-MM-DD'),
|
value: moment().format('YYYY-MM-DD'),
|
||||||
|
},
|
||||||
|
tracking_number: {
|
||||||
|
value: shipment.tracking_number,
|
||||||
|
icon: 'fa-hashtag',
|
||||||
|
},
|
||||||
|
invoice_number: {
|
||||||
|
value: shipment.invoice_number,
|
||||||
|
icon: 'fa-dollar-sign',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
value: shipment.link,
|
||||||
|
icon: 'fa-link',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
@ -1489,7 +1504,8 @@ function removePurchaseOrderLineItem(e) {
|
|||||||
* Load a table displaying list of purchase orders
|
* Load a table displaying list of purchase orders
|
||||||
*/
|
*/
|
||||||
function loadPurchaseOrderTable(table, options) {
|
function loadPurchaseOrderTable(table, options) {
|
||||||
/* Create a purchase-order table */
|
// Ensure the table starts in a known state
|
||||||
|
$(table).bootstrapTable('destroy');
|
||||||
|
|
||||||
options.params = options.params || {};
|
options.params = options.params || {};
|
||||||
|
|
||||||
@ -1505,6 +1521,71 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
|
|
||||||
setupFilterList('purchaseorder', $(table), target, {download: true});
|
setupFilterList('purchaseorder', $(table), target, {download: true});
|
||||||
|
|
||||||
|
var display_mode = inventreeLoad('purchaseorder-table-display-mode', 'list');
|
||||||
|
|
||||||
|
// Function for rendering PurchaseOrder calendar display
|
||||||
|
function buildEvents(calendar) {
|
||||||
|
|
||||||
|
var start = startDate(calendar);
|
||||||
|
var end = endDate(calendar);
|
||||||
|
|
||||||
|
clearEvents(calendar);
|
||||||
|
|
||||||
|
// Extract current filters from table
|
||||||
|
var table_options = $(table).bootstrapTable('getOptions');
|
||||||
|
var filters = table_options.query_params || {};
|
||||||
|
|
||||||
|
filters.supplier_detail = true;
|
||||||
|
filters.min_date = start;
|
||||||
|
filters.max_date = end;
|
||||||
|
|
||||||
|
// Request purchase orders from the server within specified date range
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-po-list" %}',
|
||||||
|
filters,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
|
||||||
|
var order = response[idx];
|
||||||
|
|
||||||
|
var date = order.creation_date;
|
||||||
|
|
||||||
|
if (order.complete_date) {
|
||||||
|
date = order.complete_date;
|
||||||
|
} else if (order.target_date) {
|
||||||
|
date = order.target_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = `${prefix}${order.reference} - ${order.supplier_detail.name}`;
|
||||||
|
|
||||||
|
var color = '#4c68f5';
|
||||||
|
|
||||||
|
if (order.complete_date) {
|
||||||
|
color = '#25c235';
|
||||||
|
} else if (order.overdue) {
|
||||||
|
color = '#c22525';
|
||||||
|
} else {
|
||||||
|
color = '#4c68f5';
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = {
|
||||||
|
title: title,
|
||||||
|
start: date,
|
||||||
|
end: date,
|
||||||
|
url: `/order/purchase-order/${order.pk}/`,
|
||||||
|
backgroundColor: color,
|
||||||
|
};
|
||||||
|
|
||||||
|
calendar.addEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: '{% url "api-po-list" %}',
|
url: '{% url "api-po-list" %}',
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
@ -1512,9 +1593,22 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
groupBy: false,
|
groupBy: false,
|
||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
original: options.params,
|
original: options.params,
|
||||||
|
showColumns: display_mode == 'list',
|
||||||
|
disablePagination: display_mode == 'calendar',
|
||||||
|
showCustomViewButton: false,
|
||||||
|
showCustomView: display_mode == 'calendar',
|
||||||
|
search: display_mode != 'calendar',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No purchase orders found" %}';
|
return '{% trans "No purchase orders found" %}';
|
||||||
},
|
},
|
||||||
|
buttons: constructOrderTableButtons({
|
||||||
|
prefix: 'purchaseorder',
|
||||||
|
disableTreeView: true,
|
||||||
|
callback: function() {
|
||||||
|
// Reload the entire table
|
||||||
|
loadPurchaseOrderTable(table, options);
|
||||||
|
}
|
||||||
|
}),
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@ -1614,6 +1708,30 @@ function loadPurchaseOrderTable(table, options) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
customView: function(data) {
|
||||||
|
return `<div id='purchase-order-calendar'></div>`;
|
||||||
|
},
|
||||||
|
onRefresh: function() {
|
||||||
|
loadPurchaseOrderTable(table, options);
|
||||||
|
},
|
||||||
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
|
if (display_mode == 'calendar') {
|
||||||
|
var el = document.getElementById('purchase-order-calendar');
|
||||||
|
|
||||||
|
calendar = new FullCalendar.Calendar(el, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
nowIndicator: true,
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
locale: options.locale,
|
||||||
|
datesSet: function() {
|
||||||
|
buildEvents(calendar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2176,6 +2294,9 @@ function loadPurchaseOrderExtraLineTable(table, options={}) {
|
|||||||
*/
|
*/
|
||||||
function loadSalesOrderTable(table, options) {
|
function loadSalesOrderTable(table, options) {
|
||||||
|
|
||||||
|
// Ensure the table starts in a known state
|
||||||
|
$(table).bootstrapTable('destroy');
|
||||||
|
|
||||||
options.params = options.params || {};
|
options.params = options.params || {};
|
||||||
options.params['customer_detail'] = true;
|
options.params['customer_detail'] = true;
|
||||||
|
|
||||||
@ -2191,6 +2312,70 @@ function loadSalesOrderTable(table, options) {
|
|||||||
|
|
||||||
setupFilterList('salesorder', $(table), target, {download: true});
|
setupFilterList('salesorder', $(table), target, {download: true});
|
||||||
|
|
||||||
|
var display_mode = inventreeLoad('salesorder-table-display-mode', 'list');
|
||||||
|
|
||||||
|
function buildEvents(calendar) {
|
||||||
|
|
||||||
|
var start = startDate(calendar);
|
||||||
|
var end = endDate(calendar);
|
||||||
|
|
||||||
|
clearEvents(calendar);
|
||||||
|
|
||||||
|
// Extract current filters from table
|
||||||
|
var table_options = $(table).bootstrapTable('getOptions');
|
||||||
|
var filters = table_options.query_params || {};
|
||||||
|
|
||||||
|
filters.customer_detail = true;
|
||||||
|
filters.min_date = start;
|
||||||
|
filters.max_date = end;
|
||||||
|
|
||||||
|
// Request orders from the server within specified date range
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-so-list" %}',
|
||||||
|
filters,
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
var order = response[idx];
|
||||||
|
|
||||||
|
var date = order.creation_date;
|
||||||
|
|
||||||
|
if (order.shipment_date) {
|
||||||
|
date = order.shipment_date;
|
||||||
|
} else if (order.target_date) {
|
||||||
|
date = order.target_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = `${prefix}${order.reference} - ${order.customer_detail.name}`;
|
||||||
|
|
||||||
|
// Default color is blue
|
||||||
|
var color = '#4c68f5';
|
||||||
|
|
||||||
|
// Overdue orders are red
|
||||||
|
if (order.overdue) {
|
||||||
|
color = '#c22525';
|
||||||
|
} else if (order.status == {{ SalesOrderStatus.SHIPPED }}) {
|
||||||
|
color = '#25c235';
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = {
|
||||||
|
title: title,
|
||||||
|
start: date,
|
||||||
|
end: date,
|
||||||
|
url: `/order/sales-order/${order.pk}/`,
|
||||||
|
backgroundColor: color,
|
||||||
|
};
|
||||||
|
|
||||||
|
calendar.addEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
@ -2198,9 +2383,46 @@ function loadSalesOrderTable(table, options) {
|
|||||||
groupBy: false,
|
groupBy: false,
|
||||||
sidePagination: 'server',
|
sidePagination: 'server',
|
||||||
original: options.params,
|
original: options.params,
|
||||||
|
showColums: display_mode != 'calendar',
|
||||||
|
search: display_mode != 'calendar',
|
||||||
|
showCustomViewButton: false,
|
||||||
|
showCustomView: display_mode == 'calendar',
|
||||||
|
disablePagination: display_mode == 'calendar',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No sales orders found" %}';
|
return '{% trans "No sales orders found" %}';
|
||||||
},
|
},
|
||||||
|
buttons: constructOrderTableButtons({
|
||||||
|
prefix: 'salesorder',
|
||||||
|
disableTreeView: true,
|
||||||
|
callback: function() {
|
||||||
|
// Reload the entire table
|
||||||
|
loadSalesOrderTable(table, options);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
customView: function(data) {
|
||||||
|
return `<div id='purchase-order-calendar'></div>`;
|
||||||
|
},
|
||||||
|
onRefresh: function() {
|
||||||
|
loadPurchaseOrderTable(table, options);
|
||||||
|
},
|
||||||
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
|
if (display_mode == 'calendar') {
|
||||||
|
var el = document.getElementById('purchase-order-calendar');
|
||||||
|
|
||||||
|
calendar = new FullCalendar.Calendar(el, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
nowIndicator: true,
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
locale: options.locale,
|
||||||
|
datesSet: function() {
|
||||||
|
buildEvents(calendar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@ -2445,10 +2667,26 @@ function loadSalesOrderShipmentTable(table, options={}) {
|
|||||||
field: 'tracking_number',
|
field: 'tracking_number',
|
||||||
title: '{% trans "Tracking" %}',
|
title: '{% trans "Tracking" %}',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'invoice_number',
|
||||||
|
title: '{% trans "Invoice" %}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'link',
|
||||||
|
title: '{% trans "Link" %}',
|
||||||
|
formatter: function(value) {
|
||||||
|
if (value) {
|
||||||
|
return renderLink(value, value);
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'notes',
|
field: 'notes',
|
||||||
title: '{% trans "Notes" %}',
|
title: '{% trans "Notes" %}',
|
||||||
visible: false,
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
// TODO: Implement 'notes' field
|
// TODO: Implement 'notes' field
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2643,7 +2881,7 @@ function allocateStockToSalesOrder(order_id, line_items, options={}) {
|
|||||||
|
|
||||||
fields.reference.value = ref;
|
fields.reference.value = ref;
|
||||||
fields.reference.prefix = global_settings.SALESORDER_REFERENCE_PREFIX + options.reference;
|
fields.reference.prefix = global_settings.SALESORDER_REFERENCE_PREFIX + options.reference;
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1560,7 +1560,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
/* Button callbacks for part table buttons */
|
/* Button callbacks for part table buttons */
|
||||||
|
|
||||||
$('#multi-part-order').click(function() {
|
$('#multi-part-order').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
@ -1594,7 +1594,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-part-print-label').click(function() {
|
$('#multi-part-print-label').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
|
@ -17,6 +17,41 @@ function closeSearchPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Keep track of the roles / permissions available to the current user
|
||||||
|
var search_user_roles = null;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check if the user has the specified role and permission
|
||||||
|
*/
|
||||||
|
function checkPermission(role, permission='view') {
|
||||||
|
|
||||||
|
if (!search_user_roles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(role in search_user_roles)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = search_user_roles[role];
|
||||||
|
|
||||||
|
if (!roles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
search_user_roles[role].forEach(function(p) {
|
||||||
|
if (String(p).valueOf() == String(permission).valueOf()) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Callback when the search panel is opened.
|
* Callback when the search panel is opened.
|
||||||
* Ensure the panel is in a known state
|
* Ensure the panel is in a known state
|
||||||
@ -27,6 +62,16 @@ function openSearchPanel() {
|
|||||||
|
|
||||||
clearSearchResults();
|
clearSearchResults();
|
||||||
|
|
||||||
|
// Request user roles if we do not have them
|
||||||
|
if (search_user_roles == null) {
|
||||||
|
inventreeGet('{% url "api-user-roles" %}', {}, {
|
||||||
|
success: function(response) {
|
||||||
|
search_user_roles = response.roles || {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback for text input changed
|
||||||
panel.find('#search-input').on('keyup change', searchTextChanged);
|
panel.find('#search-input').on('keyup change', searchTextChanged);
|
||||||
|
|
||||||
// Callback for "clear search" button
|
// Callback for "clear search" button
|
||||||
@ -84,7 +129,7 @@ function updateSearch() {
|
|||||||
// Show the "searching" text
|
// Show the "searching" text
|
||||||
$('#offcanvas-search').find('#search-pending').show();
|
$('#offcanvas-search').find('#search-pending').show();
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
|
if (checkPermission('part') && user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
|
||||||
|
|
||||||
var params = {};
|
var params = {};
|
||||||
|
|
||||||
@ -106,7 +151,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
|
if (checkPermission('part_category') && user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
|
||||||
// Search for matching part categories
|
// Search for matching part categories
|
||||||
addSearchQuery(
|
addSearchQuery(
|
||||||
'category',
|
'category',
|
||||||
@ -120,7 +165,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
|
if (checkPermission('stock') && user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
|
||||||
// Search for matching stock items
|
// Search for matching stock items
|
||||||
|
|
||||||
var filters = {
|
var filters = {
|
||||||
@ -146,7 +191,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
|
if (checkPermission('stock_location') && user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
|
||||||
// Search for matching stock locations
|
// Search for matching stock locations
|
||||||
addSearchQuery(
|
addSearchQuery(
|
||||||
'location',
|
'location',
|
||||||
@ -160,7 +205,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
|
if ((checkPermission('sales_order') || checkPermission('purchase_order')) && user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
|
||||||
// Search for matching companies
|
// Search for matching companies
|
||||||
addSearchQuery(
|
addSearchQuery(
|
||||||
'company',
|
'company',
|
||||||
@ -174,7 +219,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
|
if (checkPermission('purchase_order') && user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
|
||||||
|
|
||||||
var filters = {
|
var filters = {
|
||||||
supplier_detail: true,
|
supplier_detail: true,
|
||||||
@ -197,7 +242,7 @@ function updateSearch() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
|
if (checkPermission('sales_order') && user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
|
||||||
|
|
||||||
var filters = {
|
var filters = {
|
||||||
customer_detail: true,
|
customer_detail: true,
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
global_settings,
|
global_settings,
|
||||||
handleFormErrors,
|
handleFormErrors,
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
inventreeDelete,
|
|
||||||
inventreeGet,
|
inventreeGet,
|
||||||
|
inventreeMultiDelete,
|
||||||
inventreePut,
|
inventreePut,
|
||||||
launchModalForm,
|
launchModalForm,
|
||||||
linkButtonsToSelection,
|
linkButtonsToSelection,
|
||||||
@ -1106,25 +1106,15 @@ function adjustStock(action, items, options={}) {
|
|||||||
|
|
||||||
// Delete action is handled differently
|
// Delete action is handled differently
|
||||||
if (action == 'delete') {
|
if (action == 'delete') {
|
||||||
var requests = [];
|
|
||||||
|
|
||||||
item_pk_values.forEach(function(pk) {
|
inventreeMultiDelete(
|
||||||
requests.push(
|
'{% url "api-stock-list" %}',
|
||||||
inventreeDelete(
|
items,
|
||||||
`/api/stock/${pk}/`,
|
{
|
||||||
)
|
modal: opts.modal,
|
||||||
);
|
success: options.success,
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for *all* the requests to complete
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
// Destroy the modal window
|
|
||||||
$(opts.modal).modal('hide');
|
|
||||||
|
|
||||||
if (options.success) {
|
|
||||||
options.success();
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1955,7 +1945,7 @@ function loadStockTable(table, options) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function stockAdjustment(action) {
|
function stockAdjustment(action) {
|
||||||
var items = $(table).bootstrapTable('getSelections');
|
var items = getTableData(table);
|
||||||
|
|
||||||
adjustStock(action, items, {
|
adjustStock(action, items, {
|
||||||
success: function() {
|
success: function() {
|
||||||
@ -1967,7 +1957,7 @@ function loadStockTable(table, options) {
|
|||||||
// Automatically link button callbacks
|
// Automatically link button callbacks
|
||||||
|
|
||||||
$('#multi-item-print-label').click(function() {
|
$('#multi-item-print-label').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@ -1979,7 +1969,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-print-test-report').click(function() {
|
$('#multi-item-print-test-report').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@ -1992,7 +1982,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
if (global_settings.BARCODE_ENABLE) {
|
if (global_settings.BARCODE_ENABLE) {
|
||||||
$('#multi-item-barcode-scan-into-location').click(function() {
|
$('#multi-item-barcode-scan-into-location').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@ -2021,7 +2011,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-merge').click(function() {
|
$('#multi-item-merge').click(function() {
|
||||||
var items = $(table).bootstrapTable('getSelections');
|
var items = getTableData(table);
|
||||||
|
|
||||||
mergeStockItems(items, {
|
mergeStockItems(items, {
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
@ -2036,7 +2026,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
$('#multi-item-assign').click(function() {
|
$('#multi-item-assign').click(function() {
|
||||||
|
|
||||||
var items = $(table).bootstrapTable('getSelections');
|
var items = getTableData(table);
|
||||||
|
|
||||||
assignStockToCustomer(items, {
|
assignStockToCustomer(items, {
|
||||||
success: function() {
|
success: function() {
|
||||||
@ -2046,7 +2036,8 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-order').click(function() {
|
$('#multi-item-order').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
|
||||||
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var parts = [];
|
var parts = [];
|
||||||
|
|
||||||
@ -2063,7 +2054,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
$('#multi-item-set-status').click(function() {
|
$('#multi-item-set-status').click(function() {
|
||||||
// Select and set the STATUS field for selected stock items
|
// Select and set the STATUS field for selected stock items
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
// Select stock status
|
// Select stock status
|
||||||
var modal = '#modal-form';
|
var modal = '#modal-form';
|
||||||
@ -2149,7 +2140,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-delete').click(function() {
|
$('#multi-item-delete').click(function() {
|
||||||
var selections = $(table).bootstrapTable('getSelections');
|
var selections = getTableData(table);
|
||||||
|
|
||||||
var stock = [];
|
var stock = [];
|
||||||
|
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
/* exported
|
/* exported
|
||||||
customGroupSorter,
|
customGroupSorter,
|
||||||
downloadTableData,
|
downloadTableData,
|
||||||
|
getTableData,
|
||||||
reloadtable,
|
reloadtable,
|
||||||
renderLink,
|
renderLink,
|
||||||
reloadTableFilters,
|
reloadTableFilters,
|
||||||
|
constructOrderTableButtons,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,6 +24,96 @@ function reloadtable(table) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a set of extra buttons to display against a list of orders,
|
||||||
|
* allowing the orders to be displayed in various 'view' modes:
|
||||||
|
*
|
||||||
|
* - Calendar view
|
||||||
|
* - List view
|
||||||
|
* - Tree view
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* - callback: Callback function to be called when one of the buttons is pressed
|
||||||
|
* - prefix: The prefix to use when saving display data to user session
|
||||||
|
* - display: Which button to set as 'active' by default
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function constructOrderTableButtons(options={}) {
|
||||||
|
|
||||||
|
var display_mode = options.display;
|
||||||
|
|
||||||
|
var key = `${options.prefix || order}-table-display-mode`;
|
||||||
|
|
||||||
|
// If display mode is not provided, look up from session
|
||||||
|
if (!display_mode) {
|
||||||
|
display_mode = inventreeLoad(key, 'list');
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = 0;
|
||||||
|
var buttons = [];
|
||||||
|
|
||||||
|
function buttonCallback(view_mode) {
|
||||||
|
inventreeSave(key, view_mode);
|
||||||
|
|
||||||
|
if (options.callback) {
|
||||||
|
options.callback(view_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var class_calendar = display_mode == 'calendar' ? 'btn-secondary' : 'btn-outline-secondary';
|
||||||
|
var class_list = display_mode == 'list' ? 'btn-secondary' : 'btn-outline-secondary';
|
||||||
|
var class_tree = display_mode == 'tree' ? 'btn-secondary' : 'btn-outline-secondary';
|
||||||
|
|
||||||
|
// Calendar view button
|
||||||
|
if (!options.disableCalendarView) {
|
||||||
|
buttons.push({
|
||||||
|
html: `<button type='button' name='${idx++}' class='btn ${class_calendar}' title='{% trans "Display calendar view" %}'><span class='fas fa-calendar-alt'></span></button>`,
|
||||||
|
event: function() {
|
||||||
|
buttonCallback('calendar');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// List view button
|
||||||
|
if (!options.disableListView) {
|
||||||
|
buttons.push({
|
||||||
|
html: `<button type='button' name='${idx++}' class='btn ${class_list}' title='{% trans "Display list view" %}'><span class='fas fa-th-list'></span></button>`,
|
||||||
|
event: function() {
|
||||||
|
buttonCallback('list');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tree view button
|
||||||
|
if (!options.disableTreeView) {
|
||||||
|
buttons.push({
|
||||||
|
html: `<button type='button' name='${idx++}' class='btn ${class_tree}' title='{% trans "Display tree view" %}'><span class='fas fa-sitemap'></span></button>`,
|
||||||
|
event: function() {
|
||||||
|
buttonCallback('tree');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Return the 'selected' data rows from a bootstrap table.
|
||||||
|
* If allowEmpty = false, and the returned dataset is empty,
|
||||||
|
* then instead try to return *all* the data
|
||||||
|
*/
|
||||||
|
function getTableData(table, allowEmpty=false) {
|
||||||
|
|
||||||
|
var data = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
|
if (data.length == 0 && !allowEmpty) {
|
||||||
|
data = $(table).bootstrapTable('getData');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download data from a table, via the API.
|
* Download data from a table, via the API.
|
||||||
* This requires a number of conditions to be met:
|
* This requires a number of conditions to be met:
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
On release, ensure that the release tag matches the InvenTree version number!
|
Ensure that the release tag matches the InvenTree version number:
|
||||||
|
|
||||||
|
master / main branch:
|
||||||
|
- version number must end with 'dev'
|
||||||
|
|
||||||
|
stable branch:
|
||||||
|
- version number must *not* end with 'dev'
|
||||||
|
- version number cannot already exist as a release tag
|
||||||
|
|
||||||
|
tagged branch:
|
||||||
|
- version number must match tag being built
|
||||||
|
- version number cannot already exist as a release tag
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@ -11,6 +22,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
# GITHUB_REF_TYPE may be either 'branch' or 'tag'
|
||||||
|
GITHUB_REF_TYPE = os.environ['GITHUB_REF_TYPE']
|
||||||
|
|
||||||
|
# GITHUB_REF may be either 'refs/heads/<branch>' or 'refs/heads/<tag>'
|
||||||
|
GITHUB_REF = os.environ['GITHUB_REF']
|
||||||
|
|
||||||
|
GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
|
||||||
|
|
||||||
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
||||||
|
|
||||||
version = None
|
version = None
|
||||||
@ -30,66 +49,65 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
print(f"InvenTree Version: '{version}'")
|
print(f"InvenTree Version: '{version}'")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
# Determine which docker tag we are going to use
|
||||||
parser.add_argument('-t', '--tag', help='Compare against specified version tag', action='store')
|
docker_tag = None
|
||||||
parser.add_argument('-r', '--release', help='Check that this is a release version', action='store_true')
|
|
||||||
parser.add_argument('-d', '--dev', help='Check that this is a development version', action='store_true')
|
|
||||||
parser.add_argument('-b', '--branch', help='Check against a particular branch', action='store')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
if GITHUB_REF_TYPE == 'branch' and ('stable' in GITHUB_REF or 'stable' in GITHUB_BASE_REF):
|
||||||
|
print("Checking requirements for 'stable' release")
|
||||||
if args.branch:
|
|
||||||
"""
|
|
||||||
Version number requirement depends on format of branch
|
|
||||||
|
|
||||||
'master': development branch
|
|
||||||
'stable': release branch
|
|
||||||
"""
|
|
||||||
|
|
||||||
print(f"Checking version number for branch '{args.branch}'")
|
|
||||||
|
|
||||||
if args.branch == 'master':
|
|
||||||
print("- This is a development branch")
|
|
||||||
args.dev = True
|
|
||||||
elif args.branch == 'stable':
|
|
||||||
print("- This is a stable release branch")
|
|
||||||
args.release = True
|
|
||||||
|
|
||||||
if args.dev:
|
|
||||||
"""
|
|
||||||
Check that the current verrsion number matches the "development" format
|
|
||||||
e.g. "0.5 dev"
|
|
||||||
"""
|
|
||||||
|
|
||||||
print("Checking development branch")
|
|
||||||
|
|
||||||
pattern = r"^\d+(\.\d+)+ dev$"
|
|
||||||
|
|
||||||
result = re.match(pattern, version)
|
|
||||||
|
|
||||||
if result is None:
|
|
||||||
print(f"Version number '{version}' does not match required pattern for development branch")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elif args.release:
|
|
||||||
"""
|
|
||||||
Check that the current version number matches the "release" format
|
|
||||||
e.g. "0.5.1"
|
|
||||||
"""
|
|
||||||
|
|
||||||
print("Checking release branch")
|
|
||||||
|
|
||||||
pattern = r"^\d+(\.\d+)+$"
|
pattern = r"^\d+(\.\d+)+$"
|
||||||
|
|
||||||
result = re.match(pattern, version)
|
result = re.match(pattern, version)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
print(f"Version number '{version}' does not match required pattern for stable branch")
|
print(f"Version number '{version}' does not match required pattern for stable branch")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Version number '{version}' matches stable branch")
|
||||||
|
|
||||||
if args.tag:
|
docker_tag = 'stable'
|
||||||
if args.tag != version:
|
|
||||||
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
|
elif GITHUB_REF_TYPE == 'branch' and ('master' in GITHUB_REF or 'master' in GITHUB_BASE_REF):
|
||||||
|
print("Checking requirements for main development branch:")
|
||||||
|
|
||||||
|
pattern = r"^\d+(\.\d+)+ dev$"
|
||||||
|
result = re.match(pattern, version)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
print(f"Version number '{version}' does not match required pattern for development branch")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Version number '{version}' matches development branch")
|
||||||
|
|
||||||
sys.exit(0)
|
docker_tag = 'latest'
|
||||||
|
|
||||||
|
elif GITHUB_REF_TYPE == 'tag':
|
||||||
|
# GITHUB_REF should be of th eform /refs/heads/<tag>
|
||||||
|
version_tag = GITHUB_REF.split('/')[-1]
|
||||||
|
print(f"Checking requirements for tagged release - '{version_tag}'")
|
||||||
|
|
||||||
|
if version_tag != version:
|
||||||
|
print(f"Version number '{version}' does not match tag '{version_tag}'")
|
||||||
|
sys.exit
|
||||||
|
|
||||||
|
# TODO: Check if there is already a release with this tag!
|
||||||
|
|
||||||
|
docker_tag = version_tag
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Unsupported branch / version combination:")
|
||||||
|
print(f"InvenTree Version: {version}")
|
||||||
|
print("GITHUB_REF_TYPE:", GITHUB_REF_TYPE)
|
||||||
|
print("GITHUB_BASE_REF:", GITHUB_BASE_REF)
|
||||||
|
print("GITHUB_REF:", GITHUB_REF)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if docker_tag is None:
|
||||||
|
print("Docker tag could not be determined")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Version check passed for '{version}'!")
|
||||||
|
print(f"Docker tag: '{docker_tag}'")
|
||||||
|
|
||||||
|
# Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/
|
||||||
|
with open(os.getenv('GITHUB_ENV'), 'a') as env_file:
|
||||||
|
env_file.write(f"docker_tag={docker_tag}\n")
|
||||||
|
@ -101,4 +101,4 @@ volumes:
|
|||||||
o: bind
|
o: bind
|
||||||
# This directory specified where InvenTree source code is stored "outside" the docker containers
|
# This directory specified where InvenTree source code is stored "outside" the docker containers
|
||||||
# By default, this directory is one level above the "docker" directory
|
# By default, this directory is one level above the "docker" directory
|
||||||
device: ${INVENTREE_EXT_VOLUME:-../}
|
device: ${INVENTREE_EXT_VOLUME:-./}
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# exit when any command fails
|
# exit when any command fails
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
@ -16,6 +16,12 @@ INVENTREE_WEB_PORT=1337
|
|||||||
INVENTREE_DEBUG=False
|
INVENTREE_DEBUG=False
|
||||||
INVENTREE_LOG_LEVEL=WARNING
|
INVENTREE_LOG_LEVEL=WARNING
|
||||||
|
|
||||||
|
# InvenTree admin account details
|
||||||
|
# Un-comment (and complete) these lines to auto-create an admin acount
|
||||||
|
#INVENTREE_ADMIN_USER=
|
||||||
|
#INVENTREE_ADMIN_PASSWORD=
|
||||||
|
#INVENTREE_ADMIN_EMAIL=
|
||||||
|
|
||||||
# Database configuration options
|
# Database configuration options
|
||||||
# Note: The example setup is for a PostgreSQL database
|
# Note: The example setup is for a PostgreSQL database
|
||||||
INVENTREE_DB_ENGINE=postgresql
|
INVENTREE_DB_ENGINE=postgresql
|
||||||
|
@ -29,16 +29,16 @@ django-sslserver==0.22 # Secure HTTP development server
|
|||||||
django-stdimage==5.1.1 # Advanced ImageField management
|
django-stdimage==5.1.1 # Advanced ImageField management
|
||||||
django-test-migrations==1.1.0 # Unit testing for database migrations
|
django-test-migrations==1.1.0 # Unit testing for database migrations
|
||||||
django-user-sessions==1.7.1 # user sessions in DB
|
django-user-sessions==1.7.1 # user sessions in DB
|
||||||
django-weasyprint==1.0.1 # django weasyprint integration
|
django-weasyprint==2.1.0 # django weasyprint integration
|
||||||
djangorestframework==3.12.4 # DRF framework
|
djangorestframework==3.12.4 # DRF framework
|
||||||
django-xforwardedfor-middleware==2.0 # IP forwarding metadata
|
django-xforwardedfor-middleware==2.0 # IP forwarding metadata
|
||||||
flake8==3.8.3 # PEP checking
|
flake8==3.8.3 # PEP checking
|
||||||
flake8-docstrings==1.6.0 # docstring format testing
|
flake8-docstrings==1.6.0 # docstring format testing
|
||||||
gunicorn>=20.1.0 # Gunicorn web server
|
gunicorn>=20.1.0 # Gunicorn web server
|
||||||
importlib_metadata # Backport for importlib.metadata
|
importlib_metadata # Backport for importlib.metadata
|
||||||
inventree # Install the latest version of the InvenTree API python library
|
|
||||||
isort==5.10.1 # DEV: python import sorting
|
isort==5.10.1 # DEV: python import sorting
|
||||||
markdown==3.3.4 # Force particular version of markdown
|
markdown==3.3.4 # Force particular version of markdown
|
||||||
|
pdf2image==1.16.0 # PDF to image conversion
|
||||||
pep8-naming==0.11.1 # PEP naming convention extension
|
pep8-naming==0.11.1 # PEP naming convention extension
|
||||||
pre-commit==2.19.0 # Git pre-commit
|
pre-commit==2.19.0 # Git pre-commit
|
||||||
pillow==9.1.0 # Image manipulation
|
pillow==9.1.0 # Image manipulation
|
||||||
@ -48,4 +48,4 @@ python-barcode[images]==0.13.1 # Barcode generator
|
|||||||
qrcode[pil]==6.1 # QR code generator
|
qrcode[pil]==6.1 # QR code generator
|
||||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||||
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
|
||||||
weasyprint==52.5 # PDF generation library (Note: in the future need to update to 53)
|
weasyprint==55.0 # PDF generation library
|
||||||
|
10
tasks.py
10
tasks.py
@ -82,7 +82,7 @@ def plugins(c):
|
|||||||
print(f"Installing plugin packages from '{plugin_file}'")
|
print(f"Installing plugin packages from '{plugin_file}'")
|
||||||
|
|
||||||
# Install the plugins
|
# Install the plugins
|
||||||
c.run(f"pip3 install -U -r '{plugin_file}'")
|
c.run(f"pip3 install --disable-pip-version-check -U -r '{plugin_file}'")
|
||||||
|
|
||||||
|
|
||||||
@task(post=[plugins])
|
@task(post=[plugins])
|
||||||
@ -94,7 +94,7 @@ def install(c):
|
|||||||
print("Installing required python packages from 'requirements.txt'")
|
print("Installing required python packages from 'requirements.txt'")
|
||||||
|
|
||||||
# Install required Python packages with PIP
|
# Install required Python packages with PIP
|
||||||
c.run('pip3 install -U -r requirements.txt')
|
c.run('pip3 install --no-cache-dir --disable-pip-version-check -U -r requirements.txt')
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
@ -554,9 +554,9 @@ def test_translations(c):
|
|||||||
|
|
||||||
# complie regex
|
# complie regex
|
||||||
reg = re.compile(
|
reg = re.compile(
|
||||||
r"[a-zA-Z0-9]{1}"+ # match any single letter and number
|
r"[a-zA-Z0-9]{1}" + # match any single letter and number # noqa: W504
|
||||||
r"(?![^{\(\<]*[}\)\>])"+ # that is not inside curly brackets, brackets or a tag
|
r"(?![^{\(\<]*[}\)\>])" + # that is not inside curly brackets, brackets or a tag # noqa: W504
|
||||||
r"(?<![^\%][^\(][)][a-z])"+ # that is not a specially formatted variable with singles
|
r"(?<![^\%][^\(][)][a-z])" + # that is not a specially formatted variable with singles # noqa: W504
|
||||||
r"(?![^\\][\n])" # that is not a newline
|
r"(?![^\\][\n])" # that is not a newline
|
||||||
)
|
)
|
||||||
last_string = ''
|
last_string = ''
|
||||||
|
Loading…
Reference in New Issue
Block a user