mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into ci-only-in-inventree
This commit is contained in:
commit
c1a68f7c49
.devcontainer
.vscode
CONTRIBUTING.mdDockerfileInvenTree
InvenTree
common
generic/templating
label
locale
bg/LC_MESSAGES
cs/LC_MESSAGES
da/LC_MESSAGES
de/LC_MESSAGES
el/LC_MESSAGES
en/LC_MESSAGES
es/LC_MESSAGES
es_MX/LC_MESSAGES
fa/LC_MESSAGES
fi/LC_MESSAGES
fr/LC_MESSAGES
he/LC_MESSAGES
hi/LC_MESSAGES
hu/LC_MESSAGES
id/LC_MESSAGES
it/LC_MESSAGES
ja/LC_MESSAGES
ko/LC_MESSAGES
nl/LC_MESSAGES
no/LC_MESSAGES
pl/LC_MESSAGES
pt/LC_MESSAGES
ru/LC_MESSAGES
sk/LC_MESSAGES
sl/LC_MESSAGES
sr/LC_MESSAGES
sv/LC_MESSAGES
th/LC_MESSAGES
tr/LC_MESSAGES
vi/LC_MESSAGES
zh/LC_MESSAGES
zh_Hans/LC_MESSAGES
machine
plugin
report
templates/socialaccount
docs
src/frontend/src
enums
forms
locales
bg
cs
da
de
el
en
es-mx
es
fa
fi
fr
he
hi
hu
id
it
ja
ko
nl
no
pl
pseudo-LOCALE
pt-br
pt
ru
sk
sl
sr
sv
th
tr
vi
zh-hans
zh-hant
zh
pages
@ -1,16 +1,11 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3
|
||||||
{
|
{
|
||||||
"name": "InvenTree",
|
"name": "InvenTree devcontainer",
|
||||||
"build": {
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
"dockerfile": "../Dockerfile",
|
"service": "inventree",
|
||||||
"context": "..",
|
"overrideCommand": true,
|
||||||
"target": "devcontainer",
|
"workspaceFolder": "/home/inventree/",
|
||||||
"args": {
|
|
||||||
"base_image": "mcr.microsoft.com/vscode/devcontainers/base:alpine-3.18",
|
|
||||||
"workspace": "${containerWorkspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
// Configure tool-specific properties.
|
||||||
"customizations": {
|
"customizations": {
|
||||||
@ -42,37 +37,27 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [5173, 8000],
|
"forwardPorts": [5173, 8000, 8080],
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
"5173": {
|
"5173": {
|
||||||
"label": "Vite server"
|
"label": "Vite Server"
|
||||||
},
|
},
|
||||||
"8000": {
|
"8000": {
|
||||||
"label": "InvenTree server"
|
"label": "InvenTree Server"
|
||||||
|
},
|
||||||
|
"8080": {
|
||||||
|
"label": "mkdocs server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"postCreateCommand": "./.devcontainer/postCreateCommand.sh ${containerWorkspaceFolder}",
|
"postCreateCommand": ".devcontainer/postCreateCommand.sh",
|
||||||
|
|
||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "vscode",
|
"remoteUser": "vscode",
|
||||||
"containerUser": "vscode",
|
"containerUser": "vscode",
|
||||||
|
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
// InvenTree config
|
|
||||||
"INVENTREE_DEBUG": "True",
|
|
||||||
"INVENTREE_LOG_LEVEL": "INFO",
|
|
||||||
"INVENTREE_DB_ENGINE": "sqlite3",
|
|
||||||
"INVENTREE_DB_NAME": "${containerWorkspaceFolder}/dev/database.sqlite3",
|
|
||||||
"INVENTREE_MEDIA_ROOT": "${containerWorkspaceFolder}/dev/media",
|
|
||||||
"INVENTREE_STATIC_ROOT": "${containerWorkspaceFolder}/dev/static",
|
|
||||||
"INVENTREE_BACKUP_DIR": "${containerWorkspaceFolder}/dev/backup",
|
|
||||||
"INVENTREE_CONFIG_FILE": "${containerWorkspaceFolder}/dev/config.yaml",
|
|
||||||
"INVENTREE_SECRET_KEY_FILE": "${containerWorkspaceFolder}/dev/secret_key.txt",
|
|
||||||
"INVENTREE_PLUGINS_ENABLED": "True",
|
|
||||||
"INVENTREE_PLUGIN_DIR": "${containerWorkspaceFolder}/dev/plugins",
|
|
||||||
"INVENTREE_PLUGIN_FILE": "${containerWorkspaceFolder}/dev/plugins.txt",
|
|
||||||
|
|
||||||
// Python config
|
// Python config
|
||||||
"PIP_USER": "no",
|
"PIP_USER": "no",
|
||||||
|
38
.devcontainer/docker-compose.yml
Normal file
38
.devcontainer/docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- 5432/tcp
|
||||||
|
volumes:
|
||||||
|
- ../dev:/var/lib/postgresql/data:z
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: inventree
|
||||||
|
POSTGRES_USER: inventree_user
|
||||||
|
POSTGRES_PASSWORD: inventree_password
|
||||||
|
|
||||||
|
inventree:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
target: dev
|
||||||
|
args:
|
||||||
|
base_image: "mcr.microsoft.com/vscode/devcontainers/base:alpine-3.18"
|
||||||
|
workspace: "${containerWorkspaceFolder}"
|
||||||
|
data_dir: "dev"
|
||||||
|
volumes:
|
||||||
|
- ../:/home/inventree:z
|
||||||
|
|
||||||
|
environment:
|
||||||
|
INVENTREE_DEBUG: True
|
||||||
|
INVENTREE_DB_ENGINE: postgresql
|
||||||
|
INVENTREE_DB_NAME: inventree
|
||||||
|
INVENTREE_DB_HOST: db
|
||||||
|
INVENTREE_DB_USER: inventree_user
|
||||||
|
INVENTREE_DB_PASSWORD: inventree_password
|
||||||
|
INVENTREE_PLUGINS_ENABLED: True
|
||||||
|
INVENTREE_PY_ENV: /home/inventree/dev/venv
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- db
|
@ -1,20 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Avoiding Dubious Ownership in Dev Containers for setup commands that use git
|
# Avoiding Dubious Ownership in Dev Containers for setup commands that use git
|
||||||
# Note that the local workspace directory is passed through as the first argument $1
|
git config --global --add safe.directory /home/inventree
|
||||||
git config --global --add safe.directory $1
|
|
||||||
|
|
||||||
# create folders
|
|
||||||
mkdir -p $1/dev/{commandhistory,plugins}
|
|
||||||
cd $1
|
|
||||||
|
|
||||||
# create venv
|
# create venv
|
||||||
python3 -m venv $1/dev/venv
|
python3 -m venv /home/inventree/dev/venv --system-site-packages --upgrade-deps
|
||||||
. $1/dev/venv/bin/activate
|
. /home/inventree/dev/venv/bin/activate
|
||||||
|
|
||||||
# setup InvenTree server
|
# setup InvenTree server
|
||||||
pip install invoke
|
invoke update -s
|
||||||
invoke update
|
|
||||||
invoke setup-dev
|
invoke setup-dev
|
||||||
invoke frontend-install
|
invoke frontend-install
|
||||||
|
|
||||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -14,7 +14,7 @@
|
|||||||
"justMyCode": true
|
"justMyCode": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Django - 3rd party",
|
"name": "InvenTree Server - 3rd party",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/InvenTree/manage.py",
|
"program": "${workspaceFolder}/InvenTree/manage.py",
|
||||||
|
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@ -45,7 +45,7 @@
|
|||||||
{
|
{
|
||||||
"label": "setup-test",
|
"label": "setup-test",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "inv setup-test --path dev/inventree-demo-dataset",
|
"command": "inv setup-test -i --path dev/inventree-demo-dataset",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
265
CONTRIBUTING.md
265
CONTRIBUTING.md
@ -1,263 +1,6 @@
|
|||||||
|
### Contributing to InvenTree
|
||||||
|
|
||||||
Hi there, thank you for your interest in contributing!
|
Hi there, thank you for your interest in contributing!
|
||||||
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
Please read our contribution guidelines, before submitting your first pull request to the InvenTree codebase.
|
||||||
|
|
||||||
## Quickstart
|
Refer to our [contribution guidelines](https://docs.inventree.org/en/latest/develop/contributing/) for more information!
|
||||||
|
|
||||||
The following commands will get you quickly configure and run a development server, complete with a demo dataset to work with:
|
|
||||||
|
|
||||||
### Bare Metal
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
|
|
||||||
python3 -m venv env && source env/bin/activate
|
|
||||||
pip install invoke && invoke
|
|
||||||
pip install invoke && invoke setup-dev --tests
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
|
|
||||||
docker compose run inventree-dev-server invoke install
|
|
||||||
docker compose run inventree-dev-server invoke setup-test --dev
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Read the [InvenTree setup documentation](https://docs.inventree.org/en/latest/start/intro/) for a complete installation reference guide.
|
|
||||||
|
|
||||||
### Setup Devtools
|
|
||||||
|
|
||||||
Run the following command to set up all toolsets for development.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
invoke setup-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
*We recommend you run this command before starting to contribute. This will install and set up `pre-commit` to run some checks before each commit and help reduce errors.*
|
|
||||||
|
|
||||||
## Branches and Versioning
|
|
||||||
|
|
||||||
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
|
||||||
|
|
||||||
### Version Numbering
|
|
||||||
|
|
||||||
InvenTree version numbering follows the [semantic versioning](https://semver.org/) specification.
|
|
||||||
|
|
||||||
### Master Branch
|
|
||||||
|
|
||||||
The HEAD of the "main" or "master" branch of InvenTree represents the current "latest" state of code development.
|
|
||||||
|
|
||||||
- All feature branches are merged into master
|
|
||||||
- All bug fixes are merged into master
|
|
||||||
|
|
||||||
**No pushing to master:** New features must be submitted as a pull request from a separate branch (one branch per feature).
|
|
||||||
|
|
||||||
### Feature Branches
|
|
||||||
|
|
||||||
Feature branches should be branched *from* the *master* branch.
|
|
||||||
|
|
||||||
- One major feature per branch / pull request
|
|
||||||
- Feature pull requests are merged back *into* the master branch
|
|
||||||
- Features *may* also be merged into a release candidate branch
|
|
||||||
|
|
||||||
### Stable Branch
|
|
||||||
|
|
||||||
The HEAD of the "stable" branch represents the latest stable release code.
|
|
||||||
|
|
||||||
- Versioned releases are merged into the "stable" branch
|
|
||||||
- Bug fix branches are made *from* the "stable" branch
|
|
||||||
|
|
||||||
#### Release Candidate Branches
|
|
||||||
|
|
||||||
- Release candidate branches are made from master, and merged into stable.
|
|
||||||
- RC branches are targeted at a major/minor version e.g. "0.5"
|
|
||||||
- When a release candidate branch is merged into *stable*, the release is tagged
|
|
||||||
|
|
||||||
#### Bugfix Branches
|
|
||||||
|
|
||||||
- If a bug is discovered in a tagged release version of InvenTree, a "bugfix" or "hotfix" branch should be made *from* that tagged release
|
|
||||||
- When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2)
|
|
||||||
- The bugfix *must* also be cherry picked into the *master* branch.
|
|
||||||
|
|
||||||
## API versioning
|
|
||||||
|
|
||||||
The [API version](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
### Target version
|
|
||||||
We are currently targeting:
|
|
||||||
| Name | Minimum version | Note |
|
|
||||||
|---|---| --- |
|
|
||||||
| Python | 3.9 | |
|
|
||||||
| Django | 3.2 | |
|
|
||||||
| Node | 18 | Only needed for frontend development |
|
|
||||||
|
|
||||||
### Auto creating updates
|
|
||||||
The following tools can be used to auto-upgrade syntax that was depreciated in new versions:
|
|
||||||
```bash
|
|
||||||
pip install pyupgrade
|
|
||||||
pip install django-upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
To update the codebase run the following script.
|
|
||||||
```bash
|
|
||||||
pyupgrade `find . -name "*.py"`
|
|
||||||
django-upgrade --target-version 3.2 `find . -name "*.py"`
|
|
||||||
```
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree/blob/master/docs/docs/credits.md). Please try to do that as timely as possible.
|
|
||||||
|
|
||||||
|
|
||||||
## Migration Files
|
|
||||||
|
|
||||||
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.
|
|
||||||
|
|
||||||
*Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*
|
|
||||||
|
|
||||||
## Unit Testing
|
|
||||||
|
|
||||||
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage for any new features is insufficient, or the overall code coverage is decreased.
|
|
||||||
|
|
||||||
The InvenTree code base makes use of [GitHub actions](https://github.com/features/actions) to run a suite of automated tests against the code base every time a new pull request is received. These actions include (but are not limited to):
|
|
||||||
|
|
||||||
- Checking Python and Javascript code against standard style guides
|
|
||||||
- Running unit test suite
|
|
||||||
- Automated building and pushing of docker images
|
|
||||||
- Generating translation files
|
|
||||||
|
|
||||||
The various github actions can be found in the `./github/workflows` directory
|
|
||||||
|
|
||||||
### Run tests locally
|
|
||||||
|
|
||||||
To run test locally, use:
|
|
||||||
```
|
|
||||||
invoke test
|
|
||||||
```
|
|
||||||
|
|
||||||
To run only partial tests, for example for a module use:
|
|
||||||
```
|
|
||||||
invoke test --runtest order
|
|
||||||
```
|
|
||||||
|
|
||||||
To see all the available options:
|
|
||||||
|
|
||||||
```
|
|
||||||
invoke test --help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
Code style is automatically checked as part of the project's CI pipeline on GitHub. This means that any pull requests which do not conform to the style guidelines will fail CI checks.
|
|
||||||
|
|
||||||
### Backend Code
|
|
||||||
|
|
||||||
Backend code (Python) is checked against the [PEP style guidelines](https://peps.python.org/pep-0008/). Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python.
|
|
||||||
|
|
||||||
### Frontend Code
|
|
||||||
|
|
||||||
Frontend code (Javascript) is checked using [eslint](https://eslint.org/). While docstrings are not enforced for front-end code, good code documentation is encouraged!
|
|
||||||
|
|
||||||
### Running Checks Locally
|
|
||||||
|
|
||||||
If you have followed the setup devtools procedure, then code style checking is performend automatically whenever you commit changes to the code.
|
|
||||||
|
|
||||||
### Django templates
|
|
||||||
|
|
||||||
Django are checked by [djlint](https://github.com/Riverside-Healthcare/djlint) through pre-commit.
|
|
||||||
|
|
||||||
The following rules out of the [default set](https://djlint.com/docs/linter/) are not applied:
|
|
||||||
```bash
|
|
||||||
D018: (Django) Internal links should use the { % url ... % } pattern
|
|
||||||
H006: Img tag should have height and width attributes
|
|
||||||
H008: Attributes should be double quoted
|
|
||||||
H021: Inline styles should be avoided
|
|
||||||
H023: Do not use entity references
|
|
||||||
H025: Tag seems to be an orphan
|
|
||||||
H030: Consider adding a meta description
|
|
||||||
H031: Consider adding meta keywords
|
|
||||||
T002: Double quotes should be used in tags
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
New features or updates to existing features should be accompanied by user documentation.
|
|
||||||
|
|
||||||
## Translations
|
|
||||||
|
|
||||||
Any user-facing strings *must* be passed through the translation engine.
|
|
||||||
|
|
||||||
- InvenTree code is written in English
|
|
||||||
- User translatable strings are provided in English as the primary language
|
|
||||||
- Secondary language translations are provided [via Crowdin](https://crowdin.com/project/inventree)
|
|
||||||
|
|
||||||
*Note: Translation files are updated via GitHub actions - you do not need to compile translations files before submitting a pull request!*
|
|
||||||
|
|
||||||
### Python Code
|
|
||||||
|
|
||||||
For strings exposed via Python code, use the following format:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
user_facing_string = _('This string will be exposed to the translation engine!')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Templated Strings
|
|
||||||
|
|
||||||
HTML and javascript files are passed through the django templating engine. Translatable strings are implemented as follows:
|
|
||||||
|
|
||||||
```html
|
|
||||||
{ % load i18n % }
|
|
||||||
|
|
||||||
<span>{ % trans "This string will be translated" % } - this string will not!</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Github use
|
|
||||||
|
|
||||||
### Tags
|
|
||||||
|
|
||||||
The tags describe issues and PRs in multiple areas:
|
|
||||||
|
|
||||||
| Area | Name | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Triage Labels | | |
|
|
||||||
| | triage:not-checked | Item was not checked by the core team |
|
|
||||||
| | triage:not-approved | Item is not green-light by maintainer |
|
|
||||||
| Type Labels | | |
|
|
||||||
| | breaking | Indicates a major update or change which breaks compatibility |
|
|
||||||
| | bug | Identifies a bug which needs to be addressed |
|
|
||||||
| | dependency | Relates to a project dependency |
|
|
||||||
| | duplicate | Duplicate of another issue or PR |
|
|
||||||
| | enhancement | This is an suggested enhancement, extending the functionality of an existing feature |
|
|
||||||
| | experimental | This is a new *experimental* feature which needs to be enabled manually |
|
|
||||||
| | feature | This is a new feature, introducing novel functionality |
|
|
||||||
| | help wanted | Assistance required |
|
|
||||||
| | invalid | This issue or PR is considered invalid |
|
|
||||||
| | inactive | Indicates lack of activity |
|
|
||||||
| | migration | Database migration, requires special attention |
|
|
||||||
| | question | This is a question |
|
|
||||||
| | roadmap | This is a roadmap feature with no immediate plans for implementation |
|
|
||||||
| | security | Relates to a security issue |
|
|
||||||
| | starter | Good issue for a developer new to the project |
|
|
||||||
| | wontfix | No work will be done against this issue or PR |
|
|
||||||
| Feature Labels | | |
|
|
||||||
| | API | Relates to the API |
|
|
||||||
| | barcode | Barcode scanning and integration |
|
|
||||||
| | build | Build orders |
|
|
||||||
| | importer | Data importing and processing |
|
|
||||||
| | order | Purchase order and sales orders |
|
|
||||||
| | part | Parts |
|
|
||||||
| | plugin | Plugin ecosystem |
|
|
||||||
| | pricing | Pricing functionality |
|
|
||||||
| | report | Report generation |
|
|
||||||
| | stock | Stock item management |
|
|
||||||
| | user interface | User interface |
|
|
||||||
| Ecosystem Labels | | |
|
|
||||||
| | backport | Tags that the issue will be backported to a stable branch as a bug-fix |
|
|
||||||
| | demo | Relates to the InvenTree demo server or dataset |
|
|
||||||
| | docker | Docker / docker-compose |
|
|
||||||
| | CI | CI / unit testing ecosystem |
|
|
||||||
| | refactor | Refactoring existing code |
|
|
||||||
| | setup | Relates to the InvenTree setup / installation process |
|
|
||||||
|
15
Dockerfile
15
Dockerfile
@ -17,6 +17,8 @@ ARG commit_tag=""
|
|||||||
ARG commit_hash=""
|
ARG commit_hash=""
|
||||||
ARG commit_date=""
|
ARG commit_date=""
|
||||||
|
|
||||||
|
ARG data_dir="data"
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
ENV INVOKE_RUN_SHELL="/bin/ash"
|
ENV INVOKE_RUN_SHELL="/bin/ash"
|
||||||
@ -27,7 +29,7 @@ ENV INVENTREE_DOCKER="true"
|
|||||||
# InvenTree paths
|
# InvenTree paths
|
||||||
ENV INVENTREE_HOME="/home/inventree"
|
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_dir}"
|
||||||
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_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
|
ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
|
||||||
@ -94,7 +96,7 @@ FROM inventree_base as prebuild
|
|||||||
|
|
||||||
ENV PATH=/root/.local/bin:$PATH
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
|
RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
|
||||||
pip install --user uv --no-cache-dir && uv pip install -r base_requirements.txt -r requirements.txt --no-cache && \
|
pip install --user uv --no-cache-dir && pip install -r base_requirements.txt -r requirements.txt --no-cache && \
|
||||||
apk --purge del .build-deps
|
apk --purge del .build-deps
|
||||||
|
|
||||||
# Frontend builder image:
|
# Frontend builder image:
|
||||||
@ -139,7 +141,7 @@ EXPOSE 5173
|
|||||||
# Install packages required for building python packages
|
# Install packages required for building python packages
|
||||||
RUN ./install_build_packages.sh
|
RUN ./install_build_packages.sh
|
||||||
|
|
||||||
RUN pip install uv --no-cache-dir && uv pip install -r base_requirements.txt --no-cache
|
RUN pip install uv --no-cache-dir && pip install -r base_requirements.txt --no-cache
|
||||||
|
|
||||||
# Install nodejs / npm / yarn
|
# Install nodejs / npm / yarn
|
||||||
|
|
||||||
@ -162,10 +164,3 @@ ENTRYPOINT ["/bin/ash", "./docker/init.sh"]
|
|||||||
|
|
||||||
# Launch the development server
|
# Launch the development server
|
||||||
CMD ["invoke", "server", "-a", "${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}"]
|
CMD ["invoke", "server", "-a", "${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}"]
|
||||||
|
|
||||||
# Image target for devcontainer
|
|
||||||
FROM dev as devcontainer
|
|
||||||
|
|
||||||
ARG workspace="/workspaces/InvenTree"
|
|
||||||
|
|
||||||
WORKDIR ${WORKSPACE}
|
|
||||||
|
@ -10,6 +10,9 @@ import string
|
|||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import Storage
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
CONFIG_DATA = None
|
CONFIG_DATA = None
|
||||||
CONFIG_LOOKUPS = {}
|
CONFIG_LOOKUPS = {}
|
||||||
@ -69,11 +72,16 @@ def get_base_dir() -> Path:
|
|||||||
return Path(__file__).parent.parent.resolve()
|
return Path(__file__).parent.parent.resolve()
|
||||||
|
|
||||||
|
|
||||||
def ensure_dir(path: Path) -> None:
|
def ensure_dir(path: Path, storage=None) -> None:
|
||||||
"""Ensure that a directory exists.
|
"""Ensure that a directory exists.
|
||||||
|
|
||||||
If it does not exist, create it.
|
If it does not exist, create it.
|
||||||
"""
|
"""
|
||||||
|
if storage and isinstance(storage, Storage):
|
||||||
|
if not storage.exists(str(path)):
|
||||||
|
storage.save(str(path / '.empty'), ContentFile(''))
|
||||||
|
return
|
||||||
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
@ -994,6 +994,9 @@ ALLOWED_HOSTS = get_setting(
|
|||||||
typecast=list,
|
typecast=list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if SITE_URL and SITE_URL not in ALLOWED_HOSTS:
|
||||||
|
ALLOWED_HOSTS.append(SITE_URL)
|
||||||
|
|
||||||
# List of trusted origins for unsafe requests
|
# List of trusted origins for unsafe requests
|
||||||
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
|
# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
|
||||||
CSRF_TRUSTED_ORIGINS = get_setting(
|
CSRF_TRUSTED_ORIGINS = get_setting(
|
||||||
@ -1004,7 +1007,7 @@ CSRF_TRUSTED_ORIGINS = get_setting(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If a list of trusted is not specified, but a site URL has been specified, use that
|
# If a list of trusted is not specified, but a site URL has been specified, use that
|
||||||
if SITE_URL and len(CSRF_TRUSTED_ORIGINS) == 0:
|
if SITE_URL and SITE_URL not in CSRF_TRUSTED_ORIGINS:
|
||||||
CSRF_TRUSTED_ORIGINS.append(SITE_URL)
|
CSRF_TRUSTED_ORIGINS.append(SITE_URL)
|
||||||
|
|
||||||
USE_X_FORWARDED_HOST = get_boolean_setting(
|
USE_X_FORWARDED_HOST = get_boolean_setting(
|
||||||
@ -1045,7 +1048,7 @@ CORS_ALLOWED_ORIGINS = get_setting(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If no CORS origins are specified, but a site URL has been specified, use that
|
# If no CORS origins are specified, but a site URL has been specified, use that
|
||||||
if SITE_URL and len(CORS_ALLOWED_ORIGINS) == 0:
|
if SITE_URL and SITE_URL not in CORS_ALLOWED_ORIGINS:
|
||||||
CORS_ALLOWED_ORIGINS.append(SITE_URL)
|
CORS_ALLOWED_ORIGINS.append(SITE_URL)
|
||||||
|
|
||||||
for app in SOCIAL_BACKENDS:
|
for app in SOCIAL_BACKENDS:
|
||||||
|
@ -675,12 +675,14 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setting = cls.objects.get(**filters)
|
setting = cls.objects.filter(**filters).first()
|
||||||
except cls.DoesNotExist:
|
|
||||||
if create:
|
if not setting:
|
||||||
setting = cls(key=key, **kwargs)
|
if create:
|
||||||
else:
|
setting = cls(key=key, **kwargs)
|
||||||
return
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
if not key.startswith('_'):
|
if not key.startswith('_'):
|
||||||
logger.warning("Database is locked, cannot set setting '%s'", key)
|
logger.warning("Database is locked, cannot set setting '%s'", key)
|
||||||
|
0
InvenTree/generic/templating/__init__.py
Normal file
0
InvenTree/generic/templating/__init__.py
Normal file
140
InvenTree/generic/templating/apps.py
Normal file
140
InvenTree/generic/templating/apps.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"""Shared templating code."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
||||||
|
|
||||||
|
from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
|
||||||
|
|
||||||
|
import InvenTree.helpers
|
||||||
|
from InvenTree.config import ensure_dir
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_STORAGE_DIR = Path(settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatingMixin:
|
||||||
|
"""Mixin that contains shared templating code."""
|
||||||
|
|
||||||
|
name: str = ''
|
||||||
|
db: str = ''
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Ensure that the required properties are set."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.name == '':
|
||||||
|
raise NotImplementedError('ref must be set')
|
||||||
|
if self.db == '':
|
||||||
|
raise NotImplementedError('db must be set')
|
||||||
|
|
||||||
|
def create_defaults(self):
|
||||||
|
"""Function that creates all default templates for the app."""
|
||||||
|
raise NotImplementedError('create_defaults must be implemented')
|
||||||
|
|
||||||
|
def get_src_dir(self, ref_name):
|
||||||
|
"""Get the source directory for the default templates."""
|
||||||
|
raise NotImplementedError('get_src_dir must be implemented')
|
||||||
|
|
||||||
|
def get_new_obj_data(self, data, filename):
|
||||||
|
"""Get the data for a new template db object."""
|
||||||
|
raise NotImplementedError('get_new_obj_data must be implemented')
|
||||||
|
|
||||||
|
# Standardized code
|
||||||
|
def ready(self):
|
||||||
|
"""This function is called whenever the app is loaded."""
|
||||||
|
import InvenTree.ready
|
||||||
|
|
||||||
|
# skip loading if plugin registry is not loaded or we run in a background thread
|
||||||
|
if (
|
||||||
|
not InvenTree.ready.isPluginRegistryLoaded()
|
||||||
|
or not InvenTree.ready.isInMainThread()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not InvenTree.ready.canAppAccessDatabase(allow_test=False):
|
||||||
|
return # pragma: no cover
|
||||||
|
|
||||||
|
with maintenance_mode_on():
|
||||||
|
try:
|
||||||
|
self.create_defaults()
|
||||||
|
except (
|
||||||
|
AppRegistryNotReady,
|
||||||
|
IntegrityError,
|
||||||
|
OperationalError,
|
||||||
|
ProgrammingError,
|
||||||
|
):
|
||||||
|
# Database might not yet be ready
|
||||||
|
warnings.warn(
|
||||||
|
f'Database was not ready for creating {self.name}s', stacklevel=2
|
||||||
|
)
|
||||||
|
|
||||||
|
set_maintenance_mode(False)
|
||||||
|
|
||||||
|
def create_template_dir(self, model, data):
|
||||||
|
"""Create folder and database entries for the default templates, if they do not already exist."""
|
||||||
|
ref_name = model.getSubdir()
|
||||||
|
|
||||||
|
# Create root dir for templates
|
||||||
|
src_dir = self.get_src_dir(ref_name)
|
||||||
|
dst_dir = MEDIA_STORAGE_DIR.joinpath(self.name, 'inventree', ref_name)
|
||||||
|
ensure_dir(dst_dir, default_storage)
|
||||||
|
|
||||||
|
# Copy each template across (if required)
|
||||||
|
for entry in data:
|
||||||
|
self.create_template_file(model, src_dir, entry, ref_name)
|
||||||
|
|
||||||
|
def create_template_file(self, model, src_dir, data, ref_name):
|
||||||
|
"""Ensure a label template is in place."""
|
||||||
|
# Destination filename
|
||||||
|
filename = os.path.join(self.name, 'inventree', ref_name, data['file'])
|
||||||
|
|
||||||
|
src_file = src_dir.joinpath(data['file'])
|
||||||
|
dst_file = MEDIA_STORAGE_DIR.joinpath(filename)
|
||||||
|
|
||||||
|
do_copy = False
|
||||||
|
|
||||||
|
if not dst_file.exists():
|
||||||
|
logger.info("%s template '%s' is not present", self.name, filename)
|
||||||
|
do_copy = True
|
||||||
|
else:
|
||||||
|
# Check if the file contents are different
|
||||||
|
src_hash = InvenTree.helpers.hash_file(src_file)
|
||||||
|
dst_hash = InvenTree.helpers.hash_file(dst_file)
|
||||||
|
|
||||||
|
if src_hash != dst_hash:
|
||||||
|
logger.info("Hash differs for '%s'", filename)
|
||||||
|
do_copy = True
|
||||||
|
|
||||||
|
if do_copy:
|
||||||
|
logger.info("Copying %s template '%s'", self.name, dst_file)
|
||||||
|
# Ensure destination dir exists
|
||||||
|
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy file
|
||||||
|
default_storage.save(filename, src_file.open('rb'))
|
||||||
|
|
||||||
|
# Check if a file matching the template already exists
|
||||||
|
try:
|
||||||
|
if model.objects.filter(**{self.db: filename}).exists():
|
||||||
|
return # pragma: no cover
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to query %s for '%s' - you should run 'invoke update' first!",
|
||||||
|
self.name,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Creating entry for %s '%s'", model, data.get('name'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
model.objects.create(**self.get_new_obj_data(data, filename))
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to create %s '%s'", self.name, data['name'])
|
@ -1,69 +1,31 @@
|
|||||||
"""label app specification."""
|
"""Config options for the label app."""
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
|
||||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
|
||||||
|
|
||||||
from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
|
from generic.templating.apps import TemplatingMixin
|
||||||
|
|
||||||
import InvenTree.helpers
|
|
||||||
import InvenTree.ready
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
|
||||||
|
|
||||||
|
|
||||||
class LabelConfig(AppConfig):
|
class LabelConfig(TemplatingMixin, AppConfig):
|
||||||
"""App configuration class for the 'label' app."""
|
"""Configuration class for the "label" app."""
|
||||||
|
|
||||||
name = 'label'
|
name = 'label'
|
||||||
|
db = 'label'
|
||||||
|
|
||||||
def ready(self):
|
def create_defaults(self):
|
||||||
"""This function is called whenever the label app is loaded."""
|
|
||||||
# skip loading if plugin registry is not loaded or we run in a background thread
|
|
||||||
if (
|
|
||||||
not InvenTree.ready.isPluginRegistryLoaded()
|
|
||||||
or not InvenTree.ready.isInMainThread()
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not InvenTree.ready.canAppAccessDatabase(allow_test=False):
|
|
||||||
return # pragma: no cover
|
|
||||||
|
|
||||||
with maintenance_mode_on():
|
|
||||||
try:
|
|
||||||
self.create_labels() # pragma: no cover
|
|
||||||
except (
|
|
||||||
AppRegistryNotReady,
|
|
||||||
IntegrityError,
|
|
||||||
OperationalError,
|
|
||||||
ProgrammingError,
|
|
||||||
):
|
|
||||||
# Database might not yet be ready
|
|
||||||
warnings.warn(
|
|
||||||
'Database was not ready for creating labels', stacklevel=2
|
|
||||||
)
|
|
||||||
|
|
||||||
set_maintenance_mode(False)
|
|
||||||
|
|
||||||
def create_labels(self):
|
|
||||||
"""Create all default templates."""
|
"""Create all default templates."""
|
||||||
# Test if models are ready
|
# Test if models are ready
|
||||||
import label.models
|
try:
|
||||||
|
import label.models
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
# Database is not ready yet
|
||||||
|
return
|
||||||
assert bool(label.models.StockLocationLabel is not None)
|
assert bool(label.models.StockLocationLabel is not None)
|
||||||
|
|
||||||
# Create the categories
|
# Create the categories
|
||||||
self.create_labels_category(
|
self.create_template_dir(
|
||||||
label.models.StockItemLabel,
|
label.models.StockItemLabel,
|
||||||
'stockitem',
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'file': 'qr.html',
|
'file': 'qr.html',
|
||||||
@ -75,9 +37,8 @@ class LabelConfig(AppConfig):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.create_labels_category(
|
self.create_template_dir(
|
||||||
label.models.StockLocationLabel,
|
label.models.StockLocationLabel,
|
||||||
'stocklocation',
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'file': 'qr.html',
|
'file': 'qr.html',
|
||||||
@ -96,9 +57,8 @@ class LabelConfig(AppConfig):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.create_labels_category(
|
self.create_template_dir(
|
||||||
label.models.PartLabel,
|
label.models.PartLabel,
|
||||||
'part',
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'file': 'part_label.html',
|
'file': 'part_label.html',
|
||||||
@ -117,9 +77,8 @@ class LabelConfig(AppConfig):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.create_labels_category(
|
self.create_template_dir(
|
||||||
label.models.BuildLineLabel,
|
label.models.BuildLineLabel,
|
||||||
'buildline',
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'file': 'buildline_label.html',
|
'file': 'buildline_label.html',
|
||||||
@ -131,72 +90,18 @@ class LabelConfig(AppConfig):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_labels_category(self, model, ref_name, labels):
|
def get_src_dir(self, ref_name):
|
||||||
"""Create folder and database entries for the default templates, if they do not already exist."""
|
"""Get the source directory."""
|
||||||
# Create root dir for templates
|
return Path(__file__).parent.joinpath('templates', self.name, ref_name)
|
||||||
src_dir = Path(__file__).parent.joinpath('templates', 'label', ref_name)
|
|
||||||
|
|
||||||
dst_dir = settings.MEDIA_ROOT.joinpath('label', 'inventree', ref_name)
|
def get_new_obj_data(self, data, filename):
|
||||||
|
"""Get the data for a new template db object."""
|
||||||
if not dst_dir.exists():
|
return {
|
||||||
logger.info("Creating required directory: '%s'", dst_dir)
|
'name': data['name'],
|
||||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
'description': data['description'],
|
||||||
|
'label': filename,
|
||||||
# Create labels
|
'filters': '',
|
||||||
for label in labels:
|
'enabled': True,
|
||||||
self.create_template_label(model, src_dir, ref_name, label)
|
'width': data['width'],
|
||||||
|
'height': data['height'],
|
||||||
def create_template_label(self, model, src_dir, ref_name, label):
|
}
|
||||||
"""Ensure a label template is in place."""
|
|
||||||
filename = os.path.join('label', 'inventree', ref_name, label['file'])
|
|
||||||
|
|
||||||
src_file = src_dir.joinpath(label['file'])
|
|
||||||
dst_file = settings.MEDIA_ROOT.joinpath(filename)
|
|
||||||
|
|
||||||
to_copy = False
|
|
||||||
|
|
||||||
if dst_file.exists():
|
|
||||||
# File already exists - let's see if it is the "same"
|
|
||||||
|
|
||||||
if InvenTree.helpers.hash_file(dst_file) != InvenTree.helpers.hash_file(
|
|
||||||
src_file
|
|
||||||
): # pragma: no cover
|
|
||||||
logger.info("Hash differs for '%s'", filename)
|
|
||||||
to_copy = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.info("Label template '%s' is not present", filename)
|
|
||||||
to_copy = True
|
|
||||||
|
|
||||||
if to_copy:
|
|
||||||
logger.info("Copying label template '%s'", dst_file)
|
|
||||||
# Ensure destination dir exists
|
|
||||||
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Copy file
|
|
||||||
shutil.copyfile(src_file, dst_file)
|
|
||||||
|
|
||||||
# Check if a label matching the template already exists
|
|
||||||
try:
|
|
||||||
if model.objects.filter(label=filename).exists():
|
|
||||||
return # pragma: no cover
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
"Failed to query label for '%s' - you should run 'invoke update' first!",
|
|
||||||
filename,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Creating entry for %s '%s'", model, label['name'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
model.objects.create(
|
|
||||||
name=label['name'],
|
|
||||||
description=label['description'],
|
|
||||||
label=filename,
|
|
||||||
filters='',
|
|
||||||
enabled=True,
|
|
||||||
width=label['width'],
|
|
||||||
height=label['height'],
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.warning("Failed to create label '%s'", label['name'])
|
|
||||||
|
@ -96,8 +96,13 @@ class LabelTemplate(InvenTree.models.InvenTreeMetadataModel):
|
|||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getSubdir(cls) -> str:
|
||||||
|
"""Return the subdirectory for this label."""
|
||||||
|
return cls.SUBDIR
|
||||||
|
|
||||||
# Each class of label files will be stored in a separate subdirectory
|
# Each class of label files will be stored in a separate subdirectory
|
||||||
SUBDIR = 'label'
|
SUBDIR: str = 'label'
|
||||||
|
|
||||||
# Object we will be printing against (will be filled out later)
|
# Object we will be printing against (will be filled out later)
|
||||||
object_to_print = None
|
object_to_print = None
|
||||||
|
@ -30,7 +30,7 @@ class LabelTest(InvenTreeAPITestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Ensure that some label instances exist as part of init routine."""
|
"""Ensure that some label instances exist as part of init routine."""
|
||||||
super().setUpTestData()
|
super().setUpTestData()
|
||||||
apps.get_app_config('label').create_labels()
|
apps.get_app_config('label').create_defaults()
|
||||||
|
|
||||||
def test_default_labels(self):
|
def test_default_labels(self):
|
||||||
"""Test that the default label templates are copied across."""
|
"""Test that the default label templates are copied across."""
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -247,7 +247,7 @@ class TestLabelPrinterMachineType(TestMachineRegistryMixin, InvenTreeAPITestCase
|
|||||||
plugin_ref = 'inventreelabelmachine'
|
plugin_ref = 'inventreelabelmachine'
|
||||||
|
|
||||||
# setup the label app
|
# setup the label app
|
||||||
apps.get_app_config('label').create_labels() # type: ignore
|
apps.get_app_config('label').create_defaults() # type: ignore
|
||||||
plg_registry.reload_plugins()
|
plg_registry.reload_plugins()
|
||||||
config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore
|
config = cast(PluginConfig, plg_registry.get_plugin(plugin_ref).plugin_config()) # type: ignore
|
||||||
config.active = True
|
config.active = True
|
||||||
|
@ -289,13 +289,13 @@ def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin:
|
|||||||
|
|
||||||
# Check that the 'plugin' specified is valid
|
# Check that the 'plugin' specified is valid
|
||||||
try:
|
try:
|
||||||
plugin_cgf = PluginConfig.objects.get(**filter)
|
plugin_cgf = PluginConfig.objects.filter(**filter).first()
|
||||||
except PluginConfig.DoesNotExist:
|
except PluginConfig.DoesNotExist:
|
||||||
raise NotFound(detail=f"Plugin '{ref}' not installed")
|
raise NotFound(detail=f"Plugin '{ref}' not installed")
|
||||||
|
|
||||||
if plugin_cgf is None:
|
if plugin_cgf is None:
|
||||||
# This only occurs if the plugin mechanism broke
|
# This only occurs if the plugin mechanism broke
|
||||||
raise NotFound(detail=f"Plugin '{ref}' not found") # pragma: no cover
|
raise NotFound(detail=f"Plugin '{ref}' not installed") # pragma: no cover
|
||||||
|
|
||||||
# Check that the plugin is activated
|
# Check that the plugin is activated
|
||||||
if not plugin_cgf.active:
|
if not plugin_cgf.active:
|
||||||
|
@ -121,7 +121,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
def test_printing_process(self):
|
def test_printing_process(self):
|
||||||
"""Test that a label can be printed."""
|
"""Test that a label can be printed."""
|
||||||
# Ensure the labels were created
|
# Ensure the labels were created
|
||||||
apps.get_app_config('label').create_labels()
|
apps.get_app_config('label').create_defaults()
|
||||||
|
|
||||||
# Lookup references
|
# Lookup references
|
||||||
part = Part.objects.first()
|
part = Part.objects.first()
|
||||||
@ -183,7 +183,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
def test_printing_options(self):
|
def test_printing_options(self):
|
||||||
"""Test printing options."""
|
"""Test printing options."""
|
||||||
# Ensure the labels were created
|
# Ensure the labels were created
|
||||||
apps.get_app_config('label').create_labels()
|
apps.get_app_config('label').create_defaults()
|
||||||
|
|
||||||
# Lookup references
|
# Lookup references
|
||||||
parts = Part.objects.all()[:2]
|
parts = Part.objects.all()[:2]
|
||||||
@ -224,7 +224,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
|
|||||||
plugin_ref = 'samplelabelprinter'
|
plugin_ref = 'samplelabelprinter'
|
||||||
|
|
||||||
# Activate the label components
|
# Activate the label components
|
||||||
apps.get_app_config('label').create_labels()
|
apps.get_app_config('label').create_defaults()
|
||||||
self.do_activate_plugin()
|
self.do_activate_plugin()
|
||||||
|
|
||||||
def run_print_test(label, qs, url_name, url_single):
|
def run_print_test(label, qs, url_name, url_single):
|
||||||
|
@ -115,7 +115,11 @@ class PluginsRegistry:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cfg, _created = PluginConfig.objects.get_or_create(key=slug)
|
cfg = PluginConfig.objects.filter(key=slug).first()
|
||||||
|
|
||||||
|
if not cfg:
|
||||||
|
cfg = PluginConfig.objects.create(key=slug)
|
||||||
|
|
||||||
except PluginConfig.DoesNotExist:
|
except PluginConfig.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
except (IntegrityError, OperationalError, ProgrammingError): # pragma: no cover
|
except (IntegrityError, OperationalError, ProgrammingError): # pragma: no cover
|
||||||
|
@ -1,256 +1,124 @@
|
|||||||
"""Config options for the 'report' app."""
|
"""Config options for the report app."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
|
||||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
|
||||||
|
|
||||||
from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
|
from generic.templating.apps import TemplatingMixin
|
||||||
|
|
||||||
import InvenTree.helpers
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
|
||||||
|
|
||||||
|
|
||||||
class ReportConfig(AppConfig):
|
class ReportConfig(TemplatingMixin, AppConfig):
|
||||||
"""Configuration class for the 'report' app."""
|
"""Configuration class for the "report" app."""
|
||||||
|
|
||||||
name = 'report'
|
name = 'report'
|
||||||
|
db = 'template'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
"""This function is called whenever the report app is loaded."""
|
"""This function is called whenever the app is loaded."""
|
||||||
import InvenTree.ready
|
|
||||||
|
|
||||||
# skip loading if plugin registry is not loaded or we run in a background thread
|
|
||||||
if (
|
|
||||||
not InvenTree.ready.isPluginRegistryLoaded()
|
|
||||||
or not InvenTree.ready.isInMainThread()
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not InvenTree.ready.canAppAccessDatabase(allow_test=False):
|
|
||||||
return # pragma: no cover
|
|
||||||
|
|
||||||
# Configure logging for PDF generation (disable "info" messages)
|
# Configure logging for PDF generation (disable "info" messages)
|
||||||
logging.getLogger('fontTools').setLevel(logging.WARNING)
|
logging.getLogger('fontTools').setLevel(logging.WARNING)
|
||||||
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
logging.getLogger('weasyprint').setLevel(logging.WARNING)
|
||||||
|
|
||||||
with maintenance_mode_on():
|
super().ready()
|
||||||
self.create_reports()
|
|
||||||
|
|
||||||
set_maintenance_mode(False)
|
def create_defaults(self):
|
||||||
|
"""Create all default templates."""
|
||||||
def create_reports(self):
|
# Test if models are ready
|
||||||
"""Create default report templates."""
|
|
||||||
try:
|
try:
|
||||||
self.create_default_test_reports()
|
import report.models
|
||||||
self.create_default_build_reports()
|
|
||||||
self.create_default_bill_of_materials_reports()
|
|
||||||
self.create_default_purchase_order_reports()
|
|
||||||
self.create_default_sales_order_reports()
|
|
||||||
self.create_default_return_order_reports()
|
|
||||||
self.create_default_stock_location_reports()
|
|
||||||
except (
|
|
||||||
AppRegistryNotReady,
|
|
||||||
IntegrityError,
|
|
||||||
OperationalError,
|
|
||||||
ProgrammingError,
|
|
||||||
):
|
|
||||||
# Database might not yet be ready
|
|
||||||
warnings.warn('Database was not ready for creating reports', stacklevel=2)
|
|
||||||
|
|
||||||
def create_default_reports(self, model, reports):
|
|
||||||
"""Copy default report files across to the media directory."""
|
|
||||||
# Source directory for report templates
|
|
||||||
src_dir = Path(__file__).parent.joinpath('templates', 'report')
|
|
||||||
|
|
||||||
# Destination directory
|
|
||||||
dst_dir = settings.MEDIA_ROOT.joinpath('report', 'inventree', model.getSubdir())
|
|
||||||
|
|
||||||
if not dst_dir.exists():
|
|
||||||
logger.info("Creating missing directory: '%s'", dst_dir)
|
|
||||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Copy each report template across (if required)
|
|
||||||
for report in reports:
|
|
||||||
# Destination filename
|
|
||||||
filename = os.path.join(
|
|
||||||
'report', 'inventree', model.getSubdir(), report['file']
|
|
||||||
)
|
|
||||||
|
|
||||||
src_file = src_dir.joinpath(report['file'])
|
|
||||||
dst_file = settings.MEDIA_ROOT.joinpath(filename)
|
|
||||||
|
|
||||||
do_copy = False
|
|
||||||
|
|
||||||
if not dst_file.exists():
|
|
||||||
logger.info("Report template '%s' is not present", filename)
|
|
||||||
do_copy = True
|
|
||||||
else:
|
|
||||||
# Check if the file contents are different
|
|
||||||
src_hash = InvenTree.helpers.hash_file(src_file)
|
|
||||||
dst_hash = InvenTree.helpers.hash_file(dst_file)
|
|
||||||
|
|
||||||
if src_hash != dst_hash:
|
|
||||||
logger.info("Hash differs for '%s'", filename)
|
|
||||||
do_copy = True
|
|
||||||
|
|
||||||
if do_copy:
|
|
||||||
logger.info("Copying test report template '%s'", dst_file)
|
|
||||||
shutil.copyfile(src_file, dst_file)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Check if a report matching the template already exists
|
|
||||||
if model.objects.filter(template=filename).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info("Creating new TestReport for '%s'", report.get('name'))
|
|
||||||
|
|
||||||
model.objects.create(
|
|
||||||
name=report['name'],
|
|
||||||
description=report['description'],
|
|
||||||
template=filename,
|
|
||||||
enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_default_test_reports(self):
|
|
||||||
"""Create database entries for the default TestReport templates, if they do not already exist."""
|
|
||||||
try:
|
|
||||||
from .models import TestReport
|
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# Database is not ready yet
|
# Database is not ready yet
|
||||||
return
|
return
|
||||||
|
assert bool(report.models.TestReport is not None)
|
||||||
|
|
||||||
# List of test reports to copy across
|
# Create the categories
|
||||||
reports = [
|
self.create_template_dir(
|
||||||
{
|
report.models.TestReport,
|
||||||
'file': 'inventree_test_report.html',
|
[
|
||||||
'name': 'InvenTree Test Report',
|
{
|
||||||
'description': 'Stock item test report',
|
'file': 'inventree_test_report.html',
|
||||||
}
|
'name': 'InvenTree Test Report',
|
||||||
]
|
'description': 'Stock item test report',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
self.create_default_reports(TestReport, reports)
|
self.create_template_dir(
|
||||||
|
report.models.BuildReport,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'file': 'inventree_build_order.html',
|
||||||
|
'name': 'InvenTree Build Order',
|
||||||
|
'description': 'Build Order job sheet',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def create_default_bill_of_materials_reports(self):
|
self.create_template_dir(
|
||||||
"""Create database entries for the default Bill of Material templates (if they do not already exist)."""
|
report.models.BillOfMaterialsReport,
|
||||||
try:
|
[
|
||||||
from .models import BillOfMaterialsReport
|
{
|
||||||
except Exception: # pragma: no cover
|
'file': 'inventree_bill_of_materials_report.html',
|
||||||
# Database is not ready yet
|
'name': 'Bill of Materials',
|
||||||
return
|
'description': 'Bill of Materials report',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# List of Build reports to copy across
|
self.create_template_dir(
|
||||||
reports = [
|
report.models.PurchaseOrderReport,
|
||||||
{
|
[
|
||||||
'file': 'inventree_bill_of_materials_report.html',
|
{
|
||||||
'name': 'Bill of Materials',
|
'file': 'inventree_po_report.html',
|
||||||
'description': 'Bill of Materials report',
|
'name': 'InvenTree Purchase Order',
|
||||||
}
|
'description': 'Purchase Order example report',
|
||||||
]
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
self.create_default_reports(BillOfMaterialsReport, reports)
|
self.create_template_dir(
|
||||||
|
report.models.SalesOrderReport,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'file': 'inventree_so_report.html',
|
||||||
|
'name': 'InvenTree Sales Order',
|
||||||
|
'description': 'Sales Order example report',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def create_default_build_reports(self):
|
self.create_template_dir(
|
||||||
"""Create database entries for the default BuildReport templates (if they do not already exist)."""
|
report.models.ReturnOrderReport,
|
||||||
try:
|
[
|
||||||
from .models import BuildReport
|
{
|
||||||
except Exception: # pragma: no cover
|
'file': 'inventree_return_order_report.html',
|
||||||
# Database is not ready yet
|
'name': 'InvenTree Return Order',
|
||||||
return
|
'description': 'Return Order example report',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# List of Build reports to copy across
|
self.create_template_dir(
|
||||||
reports = [
|
report.models.StockLocationReport,
|
||||||
{
|
[
|
||||||
'file': 'inventree_build_order.html',
|
{
|
||||||
'name': 'InvenTree Build Order',
|
'file': 'inventree_slr_report.html',
|
||||||
'description': 'Build Order job sheet',
|
'name': 'InvenTree Stock Location',
|
||||||
}
|
'description': 'Stock Location example report',
|
||||||
]
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
self.create_default_reports(BuildReport, reports)
|
def get_src_dir(self, ref_name):
|
||||||
|
"""Get the source directory."""
|
||||||
|
return Path(__file__).parent.joinpath('templates', self.name)
|
||||||
|
|
||||||
def create_default_purchase_order_reports(self):
|
def get_new_obj_data(self, data, filename):
|
||||||
"""Create database entries for the default SalesOrderReport templates (if they do not already exist)."""
|
"""Get the data for a new template db object."""
|
||||||
try:
|
return {
|
||||||
from .models import PurchaseOrderReport
|
'name': data['name'],
|
||||||
except Exception: # pragma: no cover
|
'description': data['description'],
|
||||||
# Database is not ready yet
|
'template': filename,
|
||||||
return
|
'enabled': True,
|
||||||
|
}
|
||||||
# List of Build reports to copy across
|
|
||||||
reports = [
|
|
||||||
{
|
|
||||||
'file': 'inventree_po_report.html',
|
|
||||||
'name': 'InvenTree Purchase Order',
|
|
||||||
'description': 'Purchase Order example report',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.create_default_reports(PurchaseOrderReport, reports)
|
|
||||||
|
|
||||||
def create_default_sales_order_reports(self):
|
|
||||||
"""Create database entries for the default Sales Order report templates (if they do not already exist)."""
|
|
||||||
try:
|
|
||||||
from .models import SalesOrderReport
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
# Database is not ready yet
|
|
||||||
return
|
|
||||||
|
|
||||||
# List of Build reports to copy across
|
|
||||||
reports = [
|
|
||||||
{
|
|
||||||
'file': 'inventree_so_report.html',
|
|
||||||
'name': 'InvenTree Sales Order',
|
|
||||||
'description': 'Sales Order example report',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.create_default_reports(SalesOrderReport, reports)
|
|
||||||
|
|
||||||
def create_default_return_order_reports(self):
|
|
||||||
"""Create database entries for the default ReturnOrderReport templates."""
|
|
||||||
try:
|
|
||||||
from report.models import ReturnOrderReport
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
# Database not yet ready
|
|
||||||
return
|
|
||||||
|
|
||||||
# List of templates to copy across
|
|
||||||
reports = [
|
|
||||||
{
|
|
||||||
'file': 'inventree_return_order_report.html',
|
|
||||||
'name': 'InvenTree Return Order',
|
|
||||||
'description': 'Return Order example report',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.create_default_reports(ReturnOrderReport, reports)
|
|
||||||
|
|
||||||
def create_default_stock_location_reports(self):
|
|
||||||
"""Create database entries for the default StockLocationReport templates."""
|
|
||||||
try:
|
|
||||||
from report.models import StockLocationReport
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
# Database not yet ready
|
|
||||||
return
|
|
||||||
|
|
||||||
# List of templates to copy across
|
|
||||||
reports = [
|
|
||||||
{
|
|
||||||
'file': 'inventree_slr_report.html',
|
|
||||||
'name': 'InvenTree Stock Location',
|
|
||||||
'description': 'Stock Location example report',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.create_default_reports(StockLocationReport, reports)
|
|
||||||
|
@ -8,9 +8,7 @@
|
|||||||
<h3>{% trans "Sign Up" %}</h3>
|
<h3>{% trans "Sign Up" %}</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with provider_name=account.get_provider.name site_name=site.name %}
|
{% blocktrans with provider_name=account.get_provider.name site_name=site.name %}You are about to use your {{provider_name}} account to login to {{site_name}}.{% endblocktrans %}
|
||||||
You are about to use your {{provider_name}} account to login to {{site_name}}.
|
|
||||||
{% endblocktrans %}
|
|
||||||
<br>
|
<br>
|
||||||
{% trans "As a final step, please complete the following form" %}:
|
{% trans "As a final step, please complete the following form" %}:
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,3 +1,285 @@
|
|||||||
{!
|
---
|
||||||
include-markdown "../../../CONTRIBUTING.md"
|
title: Contribution Guide
|
||||||
!}
|
---
|
||||||
|
|
||||||
|
|
||||||
|
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
The following commands will get you quickly configure and run a development server, complete with a demo dataset to work with:
|
||||||
|
|
||||||
|
### Devcontainer
|
||||||
|
|
||||||
|
The recommended method for getting up and running with an InvenTree development environment is to use our [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) setup in [vscode](https://code.visualstudio.com/).
|
||||||
|
|
||||||
|
!!! success "Devcontainer Guide"
|
||||||
|
Refer to the [devcontainer guide](./devcontainer.md) for more information!
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
To setup a development environment using [docker](../start/docker.md), run the following instructions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
|
||||||
|
docker compose run inventree-dev-server invoke install
|
||||||
|
docker compose run inventree-dev-server invoke setup-test --dev
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bare Metal
|
||||||
|
|
||||||
|
A "bare metal" development setup can be installed as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
|
||||||
|
python3 -m venv env && source env/bin/activate
|
||||||
|
pip install invoke && invoke
|
||||||
|
pip install invoke && invoke setup-dev --tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the [InvenTree setup documentation](../start/intro.md) for a complete installation reference guide.
|
||||||
|
|
||||||
|
### Setup Devtools
|
||||||
|
|
||||||
|
Run the following command to set up all toolsets for development.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
invoke setup-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
*We recommend you run this command before starting to contribute. This will install and set up `pre-commit` to run some checks before each commit and help reduce errors.*
|
||||||
|
|
||||||
|
## Branches and Versioning
|
||||||
|
|
||||||
|
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
InvenTree version numbering follows the [semantic versioning](https://semver.org/) specification.
|
||||||
|
|
||||||
|
### Master Branch
|
||||||
|
|
||||||
|
The HEAD of the "main" or "master" branch of InvenTree represents the current "latest" state of code development.
|
||||||
|
|
||||||
|
- All feature branches are merged into master
|
||||||
|
- All bug fixes are merged into master
|
||||||
|
|
||||||
|
**No pushing to master:** New features must be submitted as a pull request from a separate branch (one branch per feature).
|
||||||
|
|
||||||
|
### Feature Branches
|
||||||
|
|
||||||
|
Feature branches should be branched *from* the *master* branch.
|
||||||
|
|
||||||
|
- One major feature per branch / pull request
|
||||||
|
- Feature pull requests are merged back *into* the master branch
|
||||||
|
- Features *may* also be merged into a release candidate branch
|
||||||
|
|
||||||
|
### Stable Branch
|
||||||
|
|
||||||
|
The HEAD of the "stable" branch represents the latest stable release code.
|
||||||
|
|
||||||
|
- Versioned releases are merged into the "stable" branch
|
||||||
|
- Bug fix branches are made *from* the "stable" branch
|
||||||
|
|
||||||
|
#### Release Candidate Branches
|
||||||
|
|
||||||
|
- Release candidate branches are made from master, and merged into stable.
|
||||||
|
- RC branches are targeted at a major/minor version e.g. "0.5"
|
||||||
|
- When a release candidate branch is merged into *stable*, the release is tagged
|
||||||
|
|
||||||
|
#### Bugfix Branches
|
||||||
|
|
||||||
|
- If a bug is discovered in a tagged release version of InvenTree, a "bugfix" or "hotfix" branch should be made *from* that tagged release
|
||||||
|
- When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2)
|
||||||
|
- The bugfix *must* also be cherry picked into the *master* branch.
|
||||||
|
|
||||||
|
## API versioning
|
||||||
|
|
||||||
|
The [API version](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
### Software Versions
|
||||||
|
|
||||||
|
The core software modules are targeting the following versions:
|
||||||
|
|
||||||
|
| Name | Minimum version | Note |
|
||||||
|
|---|---| --- |
|
||||||
|
| Python | {{ config.extra.min_python_version }} | Minimum required version |
|
||||||
|
| Invoke | {{ config.extra.min_invoke_version }} | Minimum required version |
|
||||||
|
| Django | {{ config.extra.django_version }} | Pinned version |
|
||||||
|
| Node | 18 | Only needed for frontend development |
|
||||||
|
|
||||||
|
Any other software dependencies are handled by the project package config.
|
||||||
|
|
||||||
|
### Auto creating updates
|
||||||
|
|
||||||
|
The following tools can be used to auto-upgrade syntax that was depreciated in new versions:
|
||||||
|
```bash
|
||||||
|
pip install pyupgrade
|
||||||
|
pip install django-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
To update the codebase run the following script.
|
||||||
|
```bash
|
||||||
|
pyupgrade `find . -name "*.py"`
|
||||||
|
django-upgrade --target-version {{ config.extra.django_version }} `find . -name "*.py"`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
If you add any new dependencies / libraries, they should be added to [the credits page](../credits.md).
|
||||||
|
|
||||||
|
## Migration Files
|
||||||
|
|
||||||
|
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.
|
||||||
|
|
||||||
|
*Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*
|
||||||
|
|
||||||
|
## Unit Testing
|
||||||
|
|
||||||
|
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage for any new features is insufficient, or the overall code coverage is decreased.
|
||||||
|
|
||||||
|
The InvenTree code base makes use of [GitHub actions](https://github.com/features/actions) to run a suite of automated tests against the code base every time a new pull request is received. These actions include (but are not limited to):
|
||||||
|
|
||||||
|
- Checking Python and Javascript code against standard style guides
|
||||||
|
- Running unit test suite
|
||||||
|
- Automated building and pushing of docker images
|
||||||
|
- Generating translation files
|
||||||
|
|
||||||
|
The various github actions can be found in the `./github/workflows` directory
|
||||||
|
|
||||||
|
### Run tests locally
|
||||||
|
|
||||||
|
To run test locally, use:
|
||||||
|
```
|
||||||
|
invoke test
|
||||||
|
```
|
||||||
|
|
||||||
|
To run only partial tests, for example for a module use:
|
||||||
|
```
|
||||||
|
invoke test --runtest order
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all the available options:
|
||||||
|
|
||||||
|
```
|
||||||
|
invoke test --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
Code style is automatically checked as part of the project's CI pipeline on GitHub. This means that any pull requests which do not conform to the style guidelines will fail CI checks.
|
||||||
|
|
||||||
|
### Backend Code
|
||||||
|
|
||||||
|
Backend code (Python) is checked against the [PEP style guidelines](https://peps.python.org/pep-0008/). Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python.
|
||||||
|
|
||||||
|
### Frontend Code
|
||||||
|
|
||||||
|
Frontend code (Javascript) is checked using [eslint](https://eslint.org/). While docstrings are not enforced for front-end code, good code documentation is encouraged!
|
||||||
|
|
||||||
|
### Running Checks Locally
|
||||||
|
|
||||||
|
If you have followed the setup devtools procedure, then code style checking is performend automatically whenever you commit changes to the code.
|
||||||
|
|
||||||
|
### Django templates
|
||||||
|
|
||||||
|
Django are checked by [djlint](https://github.com/Riverside-Healthcare/djlint) through pre-commit.
|
||||||
|
|
||||||
|
The following rules out of the [default set](https://djlint.com/docs/linter/) are not applied:
|
||||||
|
```bash
|
||||||
|
D018: (Django) Internal links should use the { % url ... % } pattern
|
||||||
|
H006: Img tag should have height and width attributes
|
||||||
|
H008: Attributes should be double quoted
|
||||||
|
H021: Inline styles should be avoided
|
||||||
|
H023: Do not use entity references
|
||||||
|
H025: Tag seems to be an orphan
|
||||||
|
H030: Consider adding a meta description
|
||||||
|
H031: Consider adding meta keywords
|
||||||
|
T002: Double quotes should be used in tags
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
New features or updates to existing features should be accompanied by user documentation.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
Any user-facing strings *must* be passed through the translation engine.
|
||||||
|
|
||||||
|
- InvenTree code is written in English
|
||||||
|
- User translatable strings are provided in English as the primary language
|
||||||
|
- Secondary language translations are provided [via Crowdin](https://crowdin.com/project/inventree)
|
||||||
|
|
||||||
|
*Note: Translation files are updated via GitHub actions - you do not need to compile translations files before submitting a pull request!*
|
||||||
|
|
||||||
|
### Python Code
|
||||||
|
|
||||||
|
For strings exposed via Python code, use the following format:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
user_facing_string = _('This string will be exposed to the translation engine!')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templated Strings
|
||||||
|
|
||||||
|
HTML and javascript files are passed through the django templating engine. Translatable strings are implemented as follows:
|
||||||
|
|
||||||
|
```html
|
||||||
|
{ % load i18n % }
|
||||||
|
|
||||||
|
<span>{ % trans "This string will be translated" % } - this string will not!</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Github use
|
||||||
|
|
||||||
|
### Tags
|
||||||
|
|
||||||
|
The tags describe issues and PRs in multiple areas:
|
||||||
|
|
||||||
|
| Area | Name | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Triage Labels | | |
|
||||||
|
| | triage:not-checked | Item was not checked by the core team |
|
||||||
|
| | triage:not-approved | Item is not green-light by maintainer |
|
||||||
|
| Type Labels | | |
|
||||||
|
| | breaking | Indicates a major update or change which breaks compatibility |
|
||||||
|
| | bug | Identifies a bug which needs to be addressed |
|
||||||
|
| | dependency | Relates to a project dependency |
|
||||||
|
| | duplicate | Duplicate of another issue or PR |
|
||||||
|
| | enhancement | This is an suggested enhancement, extending the functionality of an existing feature |
|
||||||
|
| | experimental | This is a new *experimental* feature which needs to be enabled manually |
|
||||||
|
| | feature | This is a new feature, introducing novel functionality |
|
||||||
|
| | help wanted | Assistance required |
|
||||||
|
| | invalid | This issue or PR is considered invalid |
|
||||||
|
| | inactive | Indicates lack of activity |
|
||||||
|
| | migration | Database migration, requires special attention |
|
||||||
|
| | question | This is a question |
|
||||||
|
| | roadmap | This is a roadmap feature with no immediate plans for implementation |
|
||||||
|
| | security | Relates to a security issue |
|
||||||
|
| | starter | Good issue for a developer new to the project |
|
||||||
|
| | wontfix | No work will be done against this issue or PR |
|
||||||
|
| Feature Labels | | |
|
||||||
|
| | API | Relates to the API |
|
||||||
|
| | barcode | Barcode scanning and integration |
|
||||||
|
| | build | Build orders |
|
||||||
|
| | importer | Data importing and processing |
|
||||||
|
| | order | Purchase order and sales orders |
|
||||||
|
| | part | Parts |
|
||||||
|
| | plugin | Plugin ecosystem |
|
||||||
|
| | pricing | Pricing functionality |
|
||||||
|
| | report | Report generation |
|
||||||
|
| | stock | Stock item management |
|
||||||
|
| | user interface | User interface |
|
||||||
|
| Ecosystem Labels | | |
|
||||||
|
| | backport | Tags that the issue will be backported to a stable branch as a bug-fix |
|
||||||
|
| | demo | Relates to the InvenTree demo server or dataset |
|
||||||
|
| | docker | Docker / docker-compose |
|
||||||
|
| | CI | CI / unit testing ecosystem |
|
||||||
|
| | refactor | Refactoring existing code |
|
||||||
|
| | setup | Relates to the InvenTree setup / installation process |
|
||||||
|
@ -16,22 +16,29 @@ You need to make sure that you have the following tools installed before continu
|
|||||||
- [docker](https://www.docker.com/products/docker-desktop/) is needed to run the devcontainer
|
- [docker](https://www.docker.com/products/docker-desktop/) is needed to run the devcontainer
|
||||||
- [vscode](https://code.visualstudio.com/Download) is needed to edit and debug code
|
- [vscode](https://code.visualstudio.com/Download) is needed to edit and debug code
|
||||||
|
|
||||||
|
#### Docker Containers
|
||||||
|
|
||||||
|
The InvenTree devcontainer setup will install two docker containers:
|
||||||
|
|
||||||
|
| Container | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| db | InvenTree database (postgresql) |
|
||||||
|
| inventree | InvenTree server |
|
||||||
|
|
||||||
#### Setup/Installation
|
#### Setup/Installation
|
||||||
|
|
||||||
1. Clone the repository (If you want to submit changes fork it and use the url to your fork in the next step)
|
1. Clone the repository (If you want to submit changes fork it and use the url to your fork in the next step)
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/inventree/InvenTree.git
|
git clone https://github.com/inventree/InvenTree.git
|
||||||
```
|
```
|
||||||
2. open vscode, navigate to the extensions sidebar and search for `ms-vscode-remote.remote-containers`. Click on install.
|
2. Open vscode, navigate to the extensions sidebar and search for `ms-vscode-remote.remote-containers`. Click on install.
|
||||||
3. open the cloned folder from above by clicking on `file > open folder`
|
3. Open the cloned folder from above by clicking on `file > open folder`
|
||||||
4. vscode should now ask you if you'd like to reopen this folder in a devcontainer. Click `Reopen in Container`. If it does not ask you, open the command palette (<kbd>CTRL/CMD</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>) and search for `Reopen in Container`. This can take a few minutes until the image is downloaded, build and setup with all dependencies.
|
4. vscode should now ask you if you'd like to reopen this folder in a devcontainer. Click `Reopen in Container`. If it does not ask you, open the command palette (<kbd>CTRL/CMD</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>) and search for `Reopen in Container`. This can take a few minutes until the image is downloaded, build and setup with all dependencies.
|
||||||
5. Open a new terminal from the top menu by clicking `Terminal > New Terminal`
|
5. Open a new terminal from the top menu by clicking `Terminal > New Terminal`
|
||||||
6. The last line in your terminal should now show the text `(venv)` at the start of the line
|
6. The last line in your terminal should now show the text `(venv)` at the start of the line
|
||||||
7. From here' we need to setup the InvenTree specific development environment
|
7. You are done! You now should have a functioning InvenTree development installation
|
||||||
8. From the newly opened terminal, run: `invoke install`
|
|
||||||
9. If you want test data on your server, run: `invoke setup-test --dev`. If not, run `invoke setup-dev`
|
|
||||||
|
|
||||||
### Setup in codespaces
|
### Setup in Codespaces
|
||||||
|
|
||||||
Open [inventree/InvenTree](https://github.com/inventree/InvenTree) with your browser and click on `Code`, select the `codespaces` tab and click on create codespace on current branch. This may can take a few minutes until your inventree development environment is setup.
|
Open [inventree/InvenTree](https://github.com/inventree/InvenTree) with your browser and click on `Code`, select the `codespaces` tab and click on create codespace on current branch. This may can take a few minutes until your inventree development environment is setup.
|
||||||
|
|
||||||
@ -52,14 +59,14 @@ If you only need a superuser, run the `superuser` task. It should prompt you for
|
|||||||
|
|
||||||
#### Run background workers
|
#### Run background workers
|
||||||
|
|
||||||
If you need to process your queue with background workers, run the `worker` task.
|
If you need to process your queue with background workers, run the `worker` task. This is a foreground task which will execute in the terminal.
|
||||||
|
|
||||||
### Running InvenTree
|
### Running InvenTree
|
||||||
|
|
||||||
You can either only run InvenTree or use the integrated debugger for debugging. Goto the `Run and debug` side panel make sure `InvenTree Server` is selected. Click on the play button on the left.
|
You can either only run InvenTree or use the integrated debugger for debugging. Goto the `Run and debug` side panel make sure `InvenTree Server` is selected. Click on the play button on the left.
|
||||||
|
|
||||||
!!! tip "Debug with 3rd party"
|
!!! tip "Debug with 3rd party"
|
||||||
Sometimes you need to debug also some 3rd party packages. Just select `python: Django - 3rd party`
|
Sometimes you need to debug also some 3rd party packages. Just select `InvenTree Servre - 3rd party`
|
||||||
|
|
||||||
You can now set breakpoints and vscode will automatically pause execution if that point is hit. You can see all variables available in that context and evaluate some code with the debugger console at the bottom. Use the play or step buttons to continue execution.
|
You can now set breakpoints and vscode will automatically pause execution if that point is hit. You can see all variables available in that context and evaluate some code with the debugger console at the bottom. Use the play or step buttons to continue execution.
|
||||||
|
|
||||||
@ -94,12 +101,15 @@ Your plugin should now be activateable from the InvenTree settings. You can also
|
|||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
#### Your ssh-keys are not available in the devcontainer but are loaded to the active `ssh-agent` on macOS
|
#### Your ssh-keys are not available in the devcontainer but are loaded to the active `ssh-agent` on macOS
|
||||||
|
|
||||||
Make sure you enabled full disk access on macOS for vscode, otherwise your ssh-keys are not available inside the container (Ref: [Automatically add SSH keys to ssh-agent [comment]](https://github.com/microsoft/vscode-remote-release/issues/4024#issuecomment-831671081)).
|
Make sure you enabled full disk access on macOS for vscode, otherwise your ssh-keys are not available inside the container (Ref: [Automatically add SSH keys to ssh-agent [comment]](https://github.com/microsoft/vscode-remote-release/issues/4024#issuecomment-831671081)).
|
||||||
|
|
||||||
#### You're not able to use your gpg-keys inside the devcontainer to sign commits on macOS
|
#### You're not able to use your gpg-keys inside the devcontainer to sign commits on macOS
|
||||||
|
|
||||||
Make sure you have `gnupg` and `pinentry-mac` installed and set up correctly. Read this [medium post](https://medium.com/@jma/setup-gpg-for-git-on-macos-4ad69e8d3733) for more info on how to set it up correctly.
|
Make sure you have `gnupg` and `pinentry-mac` installed and set up correctly. Read this [medium post](https://medium.com/@jma/setup-gpg-for-git-on-macos-4ad69e8d3733) for more info on how to set it up correctly.
|
||||||
|
|
||||||
#### Where are the database, media files, ... stored?
|
#### Where are the database, media files, ... stored?
|
||||||
|
|
||||||
Backups, Commandhistory, media/static files, venv, plugin.txt, secret_key.txt, ... are stored in the `dev` folder. If you want to start with a clean setup, you can remove that folder, but be aware that this will delete everything you already setup in InvenTree.
|
Backups, Commandhistory, media/static files, venv, plugin.txt, secret_key.txt, ... are stored in the `dev` folder. If you want to start with a clean setup, you can remove that folder, but be aware that this will delete everything you already setup in InvenTree.
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: Getting started
|
|
||||||
---
|
|
||||||
|
|
||||||
InvenTree consists of a Django-based backend server, and a HTML / vanilla JS based frontend that uses Bootstrap. The main languages used are Python and Javascript.
|
|
||||||
As part of the larger project other languages/techniques are used, such as docker (dev/deployment), bash (installer) and markdown (documentation).
|
|
||||||
|
|
||||||
### Getting started
|
|
||||||
#### Getting to know the basics
|
|
||||||
|
|
||||||
The Django framework is a powerful tool for creating web applications. It is well documented and has a large community. The [Django documentation]({% include "django.html" %}) is a good place to start.
|
|
||||||
|
|
||||||
In particular the [tutorial]({% include "django.html" %}/intro/tutorial01/) is a good way to get to know the basics of Django.
|
|
||||||
InvenTree follows the best practies for Django so most of the contents should be applicable to InvenTree as well. The REST API is based on the [Django REST framework](https://www.django-rest-framework.org/).
|
|
||||||
|
|
||||||
#### Setting up a development environment
|
|
||||||
|
|
||||||
The recommended way to set up a development environment is to use VSCode devcontainers. The required files are provided with the repo, the docs are on a [dedicated page](./devcontainer.md).
|
|
||||||
|
|
||||||
It is also possible to use [docker](../start/docker.md) or [bare metal development](../start/bare_dev.md). With these you need to install the required dependencies manually with a dedicated task.
|
|
||||||
```bash
|
|
||||||
invoke setup-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Following standards
|
|
||||||
|
|
||||||
Before contributing to the project, please read the [contributing guidelines](contributing.md). Pull requests that do not follow the guidelines, do not pass QC checks or lower the test coverage will not be accepted.
|
|
@ -60,7 +60,7 @@ Always activate the virtual environment before running server commands!
|
|||||||
|
|
||||||
### 'str' object has no attribute 'removeSuffix'
|
### 'str' object has no attribute 'removeSuffix'
|
||||||
|
|
||||||
This error occurs because your installed python version is not up to date. We [require Python v3.9 or newer](./start/intro.md#python-requirements)
|
This error occurs because your installed python version is not up to date. We [require Python {{ config.extra.min_python_version }} or newer](./start/intro.md#python-requirements)
|
||||||
|
|
||||||
You (or your system administrator) needs to update python to meet the minimum requirements for InvenTree.
|
You (or your system administrator) needs to update python to meet the minimum requirements for InvenTree.
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ title: Setup Introduction
|
|||||||
A functional InvenTree server can be hosted with minimal setup requirements. Multiple installation methods and database back-ends are supported, allowing for flexibility where required.
|
A functional InvenTree server can be hosted with minimal setup requirements. Multiple installation methods and database back-ends are supported, allowing for flexibility where required.
|
||||||
|
|
||||||
!!! info "Production Ready"
|
!!! info "Production Ready"
|
||||||
InvenTree is designed to be a production-ready application, and can be deployed in a variety of environments. The following instructions are designed to help you get started with a *production* setup. For a development setup, refer to the [development setup guide](../develop/starting.md).
|
InvenTree is designed to be a production-ready application, and can be deployed in a variety of environments. The following instructions are designed to help you get started with a *production* setup. For a development setup, refer to the [devcontainer setup guide](../develop/devcontainer.md).
|
||||||
|
|
||||||
## Installation Methods
|
## Installation Methods
|
||||||
|
|
||||||
|
@ -77,7 +77,6 @@ nav:
|
|||||||
- Terminology: concepts/terminology.md
|
- Terminology: concepts/terminology.md
|
||||||
- Physical Units: concepts/units.md
|
- Physical Units: concepts/units.md
|
||||||
- Development:
|
- Development:
|
||||||
- Getting started: develop/starting.md
|
|
||||||
- Contributing: develop/contributing.md
|
- Contributing: develop/contributing.md
|
||||||
- Devcontainer: develop/devcontainer.md
|
- Devcontainer: develop/devcontainer.md
|
||||||
- React Frontend: develop/react-frontend.md
|
- React Frontend: develop/react-frontend.md
|
||||||
|
@ -10,7 +10,7 @@ export enum UserRoles {
|
|||||||
return_order = 'return_order',
|
return_order = 'return_order',
|
||||||
sales_order = 'sales_order',
|
sales_order = 'sales_order',
|
||||||
stock = 'stock',
|
stock = 'stock',
|
||||||
stock_location = 'stocklocation',
|
stock_location = 'stock_location',
|
||||||
stocktake = 'stocktake'
|
stocktake = 'stocktake'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,99 +1,94 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { IconPackages } from '@tabler/icons-react';
|
import { IconPackages } from '@tabler/icons-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a set of fields for creating / editing a Part instance
|
* Construct a set of fields for creating / editing a Part instance
|
||||||
*/
|
*/
|
||||||
export function partFields({
|
export function usePartFields({
|
||||||
editing = false,
|
create = false
|
||||||
category_id
|
|
||||||
}: {
|
}: {
|
||||||
editing?: boolean;
|
create?: boolean;
|
||||||
category_id?: number;
|
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
let fields: ApiFormFieldSet = {
|
return useMemo(() => {
|
||||||
category: {
|
const fields: ApiFormFieldSet = {
|
||||||
filters: {
|
category: {
|
||||||
structural: false
|
filters: {
|
||||||
}
|
structural: false
|
||||||
},
|
}
|
||||||
name: {},
|
},
|
||||||
IPN: {},
|
name: {},
|
||||||
revision: {},
|
IPN: {},
|
||||||
description: {},
|
revision: {},
|
||||||
variant_of: {},
|
description: {},
|
||||||
keywords: {},
|
variant_of: {},
|
||||||
units: {},
|
keywords: {},
|
||||||
link: {},
|
units: {},
|
||||||
default_location: {
|
link: {},
|
||||||
filters: {
|
default_location: {
|
||||||
structural: false
|
filters: {
|
||||||
}
|
structural: false
|
||||||
},
|
}
|
||||||
default_expiry: {},
|
},
|
||||||
minimum_stock: {},
|
default_expiry: {},
|
||||||
responsible: {
|
minimum_stock: {},
|
||||||
filters: {
|
responsible: {
|
||||||
is_active: true
|
filters: {
|
||||||
}
|
is_active: true
|
||||||
},
|
}
|
||||||
component: {},
|
},
|
||||||
assembly: {},
|
component: {},
|
||||||
is_template: {},
|
assembly: {},
|
||||||
trackable: {},
|
is_template: {},
|
||||||
purchaseable: {},
|
trackable: {},
|
||||||
salable: {},
|
purchaseable: {},
|
||||||
virtual: {},
|
salable: {},
|
||||||
active: {}
|
virtual: {},
|
||||||
};
|
active: {}
|
||||||
|
|
||||||
if (category_id != null) {
|
|
||||||
// TODO: Set the value of the category field
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional fields for creation
|
|
||||||
if (!editing) {
|
|
||||||
// TODO: Hide 'active' field
|
|
||||||
|
|
||||||
fields.copy_category_parameters = {};
|
|
||||||
|
|
||||||
fields.initial_stock = {
|
|
||||||
icon: <IconPackages />,
|
|
||||||
children: {
|
|
||||||
quantity: {},
|
|
||||||
location: {}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fields.initial_supplier = {
|
// Additional fields for creation
|
||||||
children: {
|
if (create) {
|
||||||
supplier: {
|
fields.copy_category_parameters = {};
|
||||||
filters: {
|
|
||||||
is_supplier: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sku: {},
|
|
||||||
manufacturer: {
|
|
||||||
filters: {
|
|
||||||
is_manufacturer: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mpn: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: pop 'expiry' field if expiry not enabled
|
fields.initial_stock = {
|
||||||
delete fields['default_expiry'];
|
icon: <IconPackages />,
|
||||||
|
children: {
|
||||||
|
quantity: {},
|
||||||
|
location: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: pop 'revision' field if PART_ENABLE_REVISION is False
|
fields.initial_supplier = {
|
||||||
delete fields['revision'];
|
children: {
|
||||||
|
supplier: {
|
||||||
|
filters: {
|
||||||
|
is_supplier: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sku: {},
|
||||||
|
manufacturer: {
|
||||||
|
filters: {
|
||||||
|
is_manufacturer: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mpn: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: handle part duplications
|
// TODO: pop 'expiry' field if expiry not enabled
|
||||||
|
delete fields['default_expiry'];
|
||||||
|
|
||||||
return fields;
|
// TODO: pop 'revision' field if PART_ENABLE_REVISION is False
|
||||||
|
delete fields['revision'];
|
||||||
|
|
||||||
|
// TODO: handle part duplications
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}, [create]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
import {
|
||||||
|
ApiFormAdjustFilterType,
|
||||||
|
ApiFormFieldSet
|
||||||
|
} from '../components/forms/fields/ApiFormField';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm';
|
||||||
|
|
||||||
@ -37,6 +40,13 @@ export function useStockFields({
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
supplier_detail: true,
|
supplier_detail: true,
|
||||||
...(part ? { part } : {})
|
...(part ? { part } : {})
|
||||||
|
},
|
||||||
|
adjustFilters: (value: ApiFormAdjustFilterType) => {
|
||||||
|
if (value.data.part) {
|
||||||
|
value.filters['part'] = value.data.part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.filters;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
use_pack_size: {
|
use_pack_size: {
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@ import { StylishText } from '../../components/items/StylishText';
|
|||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { partCategoryFields, partFields } from '../../forms/PartForms';
|
import { partCategoryFields, usePartFields } from '../../forms/PartForms';
|
||||||
import { useCreateStockItem } from '../../forms/StockForms';
|
import { useCreateStockItem } from '../../forms/StockForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
@ -28,10 +28,13 @@ function ApiFormsPlayground() {
|
|||||||
fields: fields
|
fields: fields
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createPartFields = usePartFields({ create: true });
|
||||||
|
const editPartFields = usePartFields({ create: false });
|
||||||
|
|
||||||
const newPart = useCreateApiFormModal({
|
const newPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.part_list,
|
url: ApiEndpoints.part_list,
|
||||||
title: 'Create Part',
|
title: 'Create Part',
|
||||||
fields: partFields({}),
|
fields: createPartFields,
|
||||||
initialData: {
|
initialData: {
|
||||||
description: 'A part created via the API'
|
description: 'A part created via the API'
|
||||||
}
|
}
|
||||||
@ -41,7 +44,7 @@ function ApiFormsPlayground() {
|
|||||||
url: ApiEndpoints.part_list,
|
url: ApiEndpoints.part_list,
|
||||||
pk: 1,
|
pk: 1,
|
||||||
title: 'Edit Part',
|
title: 'Edit Part',
|
||||||
fields: partFields({ editing: true })
|
fields: editPartFields
|
||||||
});
|
});
|
||||||
|
|
||||||
const newAttachment = useCreateApiFormModal({
|
const newAttachment = useCreateApiFormModal({
|
||||||
@ -62,7 +65,7 @@ function ApiFormsPlayground() {
|
|||||||
const [name, setName] = useState('Hello');
|
const [name, setName] = useState('Hello');
|
||||||
|
|
||||||
const partFieldsState: any = useMemo<any>(() => {
|
const partFieldsState: any = useMemo<any>(() => {
|
||||||
const fields = partFields({});
|
const fields = editPartFields;
|
||||||
fields.name = {
|
fields.name = {
|
||||||
...fields.name,
|
...fields.name,
|
||||||
value: name,
|
value: name,
|
||||||
|
@ -8,13 +8,14 @@ import {
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
|
import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import ParametricPartTable from '../../tables/part/ParametricPartTable';
|
||||||
import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
import { PartCategoryTable } from '../../tables/part/PartCategoryTable';
|
||||||
|
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
||||||
import { PartListTable } from '../../tables/part/PartTable';
|
import { PartListTable } from '../../tables/part/PartTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,7 +71,7 @@ export default function CategoryDetail({}: {}) {
|
|||||||
name: 'parameters',
|
name: 'parameters',
|
||||||
label: t`Parameters`,
|
label: t`Parameters`,
|
||||||
icon: <IconListDetails />,
|
icon: <IconListDetails />,
|
||||||
content: <PlaceholderPanel />
|
content: <ParametricPartTable categoryId={id} />
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[category, id]
|
[category, id]
|
||||||
|
@ -46,7 +46,7 @@ import { formatPriceRange } from '../../defaults/formatters';
|
|||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { partFields } from '../../forms/PartForms';
|
import { usePartFields } from '../../forms/PartForms';
|
||||||
import { useEditApiFormModal } from '../../hooks/UseForm';
|
import { useEditApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
@ -630,11 +630,13 @@ export default function PartDetail() {
|
|||||||
);
|
);
|
||||||
}, [part, id]);
|
}, [part, id]);
|
||||||
|
|
||||||
|
const partFields = usePartFields({ create: false });
|
||||||
|
|
||||||
const editPart = useEditApiFormModal({
|
const editPart = useEditApiFormModal({
|
||||||
url: ApiEndpoints.part_list,
|
url: ApiEndpoints.part_list,
|
||||||
pk: part.pk,
|
pk: part.pk,
|
||||||
title: t`Edit Part`,
|
title: t`Edit Part`,
|
||||||
fields: partFields({ editing: true }),
|
fields: partFields,
|
||||||
onFormSuccess: refreshInstance
|
onFormSuccess: refreshInstance
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user