Docs refactor (#9545)

* Refactor / reognaize docs structure

* Refactor plugin docs structure

* More refactoring / cleanup

* Update build images

* Gallery updates

* Order images

* Update part docs

* Settings images

* Stock images

* Reitntroduce gallery

* Add custom icon macro

* Update icons

* Cleanup

* Fix link

* Fix internal links

* Revert some page moves

* Fix links

* Fix links
This commit is contained in:
Oliver
2025-04-22 08:18:32 +10:00
committed by GitHub
parent 9b489911e5
commit 6b08e45eac
240 changed files with 964 additions and 831 deletions
+169
View File
@@ -0,0 +1,169 @@
---
title: Developing Plugins
---
## How to Develop a Plugin
A short introductory guide for plugin beginners.
### Should it be a plugin?
First of all figure out what your plugin / code should do.
If you want to change how InvenTree base mechanics and business logic work, a plugin will not be sufficient. Maybe fork the project or better [start a discussion](https://github.com/inventree/InvenTree/discussions) on GitHub. There might be an easier / established way to do what you want.
If you want to remove parts of the user interface -> remove the permissions for those objects / actions and the users will not see them.
If you add a lot of code (over ~1000 LOC) maybe split it into multiple plugins to make upgrading and testing simpler.
### It will be a plugin!
Great. Now please read the [plugin documentation](./index.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings.
### Pick your building blocks
Consider the use-case for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
- Is it just a simple REST-endpoint that runs a function ([ActionMixin](./mixins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./mixins/barcode.md))?
- How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./mixins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./mixins/navigation.md)).
- Do you need to extend reporting functionality? Check out the [ReportMixin](./mixins/report.md).
- Will it make calls to external APIs ([APICallMixin](./mixins/api.md) helps there)?
- Do you need to run in the background ([ScheduleMixin](./mixins/schedule.md)) or when things in InvenTree change ([EventMixin](./mixins/event.md))?
- Does the plugin need configuration that should be user changeable ([SettingsMixin](./mixins/settings.md)) or static (just use a yaml in the config dir)?
- You want to receive webhooks? Do not code your own untested function, use the WebhookEndpoint model as a base and override the perform_action method.
- Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./mixins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance.
### Define the metadata
Do not forget to [declare the metadata](./index.md#plugin-options) for your plugin, those will be used in the settings. At least provide a web link so users can file issues / reach you.
### Development guidelines
If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case.
- Keep it simple - more that 1000 LOC are normally to much for a plugin
- Use mixins where possible - we try to keep coverage high for them so they are not likely to break
- Do not use internal functions - if a functions name starts with `_` it is internal and might change at any time
- Keep you imports clean - the APIs for plugins and mixins are young and evolving (see [here](./index.md#imports)). Use
```
from plugin import InvenTreePlugin, registry
from plugin.mixins import APICallMixin, SettingsMixin, ScheduleMixin, BarcodeMixin
```
- Feliver as a package (see [below](#packaging))
- If you need to use a private infrastructure, use the 'Releases' functions in GitHub or Gitlab. Point to the 'latest' release endpoint when installing to make sure the update function works
- Tag your GitHub repo with `inventree` and `inventreeplugins` to make discovery easier. A discovery mechanism using these tags is on the roadmap.
- Use GitHub actions to test your plugin regularly (you can [schedule actions](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule)) against the 'latest' [docker-build](https://hub.docker.com/r/inventree/inventree) of InvenTree
- If you use the AppMixin pin your plugin against the stable branch of InvenTree, your migrations might get messed up otherwise
### Packaging
!!! tip "Package-Discovery can be tricky"
Most problems with packaging stem from problems with discovery. [This guide](https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#automatic-discovery) by the PyPA contains a lot of information about discovery during packaging. These mechanisms generally apply to most discovery processes in InvenTree and the wider Django ecosystem.
The recommended way of distribution is as a [PEP 561](https://peps.python.org/pep-0561/) compliant package. If you can use the official Package Index (PyPi - [official website](https://pypi.org/)) as a registry.
Please follow PyPAs official [packaging guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/) to ensure your package installs correctly suing InvenTrees install mechanisms.
Your package must expose you plugin class as an [entrypoint](https://setuptools.pypa.io/en/latest/userguide/entry_point.html) with the name `inventree_plugins` to work with InvenTree.
```setup.cfg
# Example setup.cfg
[options.entry_points]
inventree_plugins =
ShopifyIntegrationPlugin = path.to.source:ShopifyIntegrationPluginClass
```
```setup.py
# Example setup.py
import setuptools
# ...
setuptools.setup(
name='ShopifyIntegrationPlugin'
.# ..
entry_points={"inventree_plugins": ["ShopifyIntegrationPlugin = path.to.source:ShopifyIntegrationPluginClass"]}
```
#### Including Extra Files
In some cases you may wish to copy across extra files when the package is installed. For example, you may have custom template files which need to be copied across to the installation directory.
In this case, you will need to include a `MANIFEST.in` file in the root directory of your plugin, and include the line `include_package_data=True` in your `setup.py` file.
!!! tip "Setuptools Documentation"
Read more about `MANIFEST.in` in the [setuptools documentation](https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html)
As an example, you have a plugin codebase with the following directory structure:
```
- my_plugin # Core plugin code
- my_plugin/templates/ # Template files
- MANIFEST.in # Manifest file
- setup.py # Setuptools script
```
To ensure that the templates are copied into the installation directory, `MANIFEST.in` should look like:
```
recursive-include my_plugin/templates *
```
Other files and directories can be copied in a similar manner.
### Local Plugin Development
If you are developing a plugin (either from scratch, or making changes to an existing plugin), it can be useful to install the plugin using an [editable install](https://setuptools.pypa.io/en/latest/userguide/development_mode.html).
An *editable install* installs the plugin (via PIP) into your local python virtual environment, but does not *copy* the code into the environment. Instead, it loads the code directly from where it is located, and also monitors for live changes in the code. This means that you can make changes to the plugin on the fly, and the InvenTree development server will detect any code changes and re-load the plugin automatically.
Note that to use an *editable install*, your plugin must be installable via PIP.
#### Example
To setup an editable install:
- Download the source code for the plugin (or create a new plugin)
- Ensure that your setup file (either `setup.py` or `pyproject.toml`) is valid
- Launch a command line and activate your development virtual environment
- `cd` into the top-level directory of your plugin project, where the setup file is located
- Setup an editable install with the following command:
```bash
pip install --editable .
```
### A simple example
This example adds a new action under `/api/action/sample` using the ActionMixin.
``` py
# -*- coding: utf-8 -*-
"""sample implementation for ActionPlugin"""
from plugin import InvenTreePlugin
from plugin.mixins import ActionMixin
class SampleActionPlugin(ActionMixin, InvenTreePlugin):
"""Use docstrings for everything."""
NAME = "SampleActionPlugin"
ACTION_NAME = "sample"
# metadata
AUTHOR = "Sample Author"
DESCRIPTION = "A very basic plugin with one mixin"
PUBLISH_DATE = "2222-02-22"
VERSION = "1.2.3" # We recommend semver and increase the major version with each new major release of InvenTree
WEBSITE = "https://example.com/"
LICENSE = "MIT" # use what you want - OSI approved is ♥
# Everything form here is for the ActionMixin
def perform_action(self):
print("Action plugin in action!")
def get_info(self):
return {
"user": self.user.username,
"hello": "world",
}
def get_result(self):
return True # This is returned to the client
```
+134
View File
@@ -0,0 +1,134 @@
---
title: Plugins
---
## InvenTree Plugin Architecture
The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviors which are decoupled from core InvenTree code.
Plugins can be added from multiple sources:
- Plugins can be installed using the PIP Python package manager
- Plugins can be placed in the external [plugins directory](../start/config.md#plugin-options)
- InvenTree built-in plugins are located within the InvenTree source code
For further information, read more about [installing plugins](./install.md).
### Configuration Options
Plugin behaviour can be controlled via the InvenTree configuration options. Refer to the [configuration guide](../start/config.md#plugin-options) for the available plugin configuration options.
### Creating a Plugin
To assist in creating a new plugin, we provide a [plugin creator command line tool](https://github.com/inventree/plugin-creator). This allows developers to quickly scaffold a new InvenTree plugin, and provides a basic structure to build upon.
To install and run the plugin creator:
```bash
pip install inventree-plugin-creator
create-inventree-plugin
```
## Plugin Code Structure
### Plugin Base Class
Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
!!! warning "Name Change"
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`.
### Imports
As the code base is evolving import paths might change. Therefore we provide stable import targets for important python APIs.
Please read all release notes and watch out for warnings - we generally provide backports for depreciated interfaces for at least one minor release.
#### Plugins
General classes and mechanisms are provided under the `plugin` [namespaces]({{ sourcefile("src/backend/InvenTree/plugin/__init__.py") }}). These include:
```python
# Management objects
registry # Object that manages all plugin states and integrations
# Base classes
InvenTreePlugin # Base class for all plugins
# Errors
MixinImplementationError # Is raised if a mixin is implemented wrong (default not overwritten for example)
MixinNotImplementedError # Is raised if a mixin was not implemented (core mechanisms are missing from the plugin)
```
#### Mixins
Mixins are split up internally to keep the source tree clean and enable better testing separation. All public APIs that should be used are exposed under `plugin.mixins`. These include all built-in mixins and notification methods. An up-to-date reference can be found in the source code [can be found here]({{ sourcefile("src/backend/InvenTree/plugin/mixins/__init__.py") }}).
#### Models and other internal InvenTree APIs
!!! warning "Danger Zone"
The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `InvenTree` org if you are not sure you are using something the intended way.
We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases.
If you need to react to state changes please use the [EventMixin](./mixins/event.md).
### Plugin Options
Some metadata options can be defined as constants in the plugins class.
``` python
NAME = '' # Used as a general reference to the plugin
SLUG = None # Used in URLs, setting-names etc. when a unique slug as a reference is needed -> the plugin name is used if not set
TITLE = None # A nice human friendly name for the plugin -> used in titles, as plugin name etc.
AUTHOR = None # Author of the plugin, git commit information is used if not present
PUBLISH_DATE = None # Publishing date of the plugin, git commit information is used if not present
WEBSITE = None # Website for the plugin, developer etc. -> is shown in plugin overview if set
VERSION = None # Version of the plugin
MIN_VERSION = None # Lowest InvenTree version number that is supported by the plugin
MAX_VERSION = None # Highest InvenTree version number that is supported by the plugin
```
Refer to the [sample plugins]({{ sourcedir("src/backend/InvenTree/plugin/samples") }}) for further examples.
### Plugin Config
A *PluginConfig* database entry will be created for each plugin "discovered" when the server launches. This configuration entry is used to determine if a particular plugin is enabled.
The configuration entries must be enabled via the [InvenTree admin interface](../settings/admin.md).
!!! warning "Disabled by Default"
Newly discovered plugins are disabled by default, and must be manually enabled (in the admin interface) by a user with staff privileges.
## Plugin Mixins
Common use cases are covered by pre-supplied modules in the form of *mixins* (similar to how [Django]({% include "django.html" %}/topics/class-based-views/mixins/) does it). Each mixin enables the integration into a specific area of InvenTree. Sometimes it also enhances the plugin with helper functions to supply often used functions out-of-the-box.
Supported mixin classes are:
| Mixin | Description |
| --- | --- |
| [ActionMixin](./mixins/action.md) | Run custom actions |
| [APICallMixin](./mixins/api.md) | Perform calls to external APIs |
| [AppMixin](./mixins/app.md) | Integrate additional database tables |
| [BarcodeMixin](./mixins/barcode.md) | Support custom barcode actions |
| [CurrencyExchangeMixin](./mixins/currency.md) | Custom interfaces for currency exchange rates |
| [DataExport](./mixins/export.md) | Customize data export functionality |
| [EventMixin](./mixins/event.md) | Respond to events |
| [LabelPrintingMixin](./mixins/label.md) | Custom label printing support |
| [LocateMixin](./mixins/locate.md) | Locate and identify stock items |
| [NavigationMixin](./mixins/navigation.md) | Add custom pages to the web interface |
| [ReportMixin](./mixins/report.md) | Add custom context data to reports |
| [ScheduleMixin](./mixins/schedule.md) | Schedule periodic tasks |
| [SettingsMixin](./mixins/settings.md) | Integrate user configurable settings |
| [UserInterfaceMixin](./mixins/ui.md) | Add custom user interface features |
| [UrlsMixin](./mixins/urls.md) | Respond to custom URL endpoints |
| [ValidationMixin](./mixins/validation.md) | Provide custom validation of database models |
## Static Files
If your plugin requires static files (e.g. CSS, JavaScript, images), these should be placed in the top level `static` directory within the distributed plugin package. These files will be automatically collected by InvenTree when the plugin is installed, and copied to an appropriate location.
These files will be available to the InvenTree web interface, and can be accessed via the URL `/static/plugins/<plugin_name>/<filename>`. Static files are served by the [proxy server](../start/processes.md#proxy-server).
For example, if the plugin is named `my_plugin`, and contains a file `CustomPanel.js`, it can be accessed via the URL `/static/plugins/my_plugin/CustomPanel.js`.
+92
View File
@@ -0,0 +1,92 @@
---
title: Installing Plugins
---
## Installing a Plugin
Plugins can either be loaded from paths in the InvenTree install directory or as a plugin installed via pip. We recommend installation via pip as this enables hassle-free upgrades.
### Common Issues
Installing plugins can be complex! Some common issues are outlined below:
#### Enable Plugin Support
To enable custom plugins, plugin support must be activated in the [server configuration](../start/config.md#plugin-options). This step must be performed by a system administrator before the InvenTree server is started.
#### Restart Server
Plugins are discovered and loaded only when the server is started. When new plugins are installed (and activated), both the web server and background worker must be restarted.
#### Container Environments
In certain container environments (such as docker), plugins are installed into an *ephemeral* virtual environment which persists only for the lifetime of the container. To allow for this, InvenTree provides a configurable setting which can automatically install plugins whenever the container is loaded.
!!! tip "Check Plugins on Startup"
Ensure the **Check Plugins on Startup** option is enabled, when running InvenTree in a container environment!
{% with id="check_plugins", url="plugin/check_on_startup.png", description="Check plugins on startup" %}
{% include 'img.html' %}
{% endwith %}
### Installation Methods
#### Builtin Plugins
Builtin plugins ship in `src/backend/InvenTree/plugin/builtin`. To achieve full unit-testing for all mixins there are some sample implementations in `src/backend/InvenTree/plugin/samples`.
!!! success "Builtin Plugins"
Builtin plugins are always enabled, as they are required for core InvenTree functionality
!!! info "Debug Only"
The sample plugins are not loaded in production mode.
#### Plugin Installation File (PIP)
Plugins installation can be simplified by providing a list of plugins in a plugin configuration file. This file (by default, *plugins.txt* in the same directory as the server configuration file) contains a list of required plugin packages.
Plugins can be then installed from this file by simply running the command `invoke plugins`.
Installation via PIP (using the *plugins.txt* file) provides a number of advantages:
- Any required secondary packages are installed automatically
- You can update plugins simply by specifying version numbers in *plugins.txt*
- Migrating plugins between systems is simplified
- You can install plugins via any source supported by PIP
!!! success "Auto Update"
When the server installation is updated via the `invoke update` command, the plugins (as specified in *plugins.txt*) will also be updated automatically.
!!! info "Plugin File Location"
The location of your plugin configuration file will depend on your [server configuration](../start/config.md)
#### Web Interface
Admin users can install plugins directly from the web interface, via the "Plugin Settings" view:
{% with id="plugin_install", url="plugin/plugin_install_web.png", description="Install via web interface" %}
{% include 'img.html' %}
{% endwith %}
!!! success "Plugin File"
A plugin installed via the web interface is added to the [plugins.txt](#plugin-installation-file-pip) plugin file.
#### Local Directory
Custom plugins can be placed in the `data/plugins/` directory, where they will be automatically discovered. This can be useful for developing and testing plugins, but can prove more difficult in production (e.g. when using Docker).
!!! info "Git Tracking"
The `data/plugins/` directory is excluded from Git version tracking - any plugin files here will be hidden from Git
!!! warning "Not Recommended For Production"
Loading plugins via the local *plugins* directory is not recommended for production. If you cannot use PIP installation (above), specify a custom plugin directory (below) or use a [VCS](https://pip.pypa.io/en/stable/topics/vcs-support/) as a plugin install source.
#### Custom Directory
If you wish to install plugins from local source, rather than PIP, it is better to place your plugins in a directory outside the InvenTree source directory.
To achieve this, set the `INVENTREE_PLUGIN_DIR` environment variable to the directory where locally sourced plugins are located. Refer to the [configuration options](../start/config.md#plugin-options) for further information.
!!! info "Docker"
When running InvenTree in docker, a *plugins* directory is automatically created in the mounted data volume. Any plugins can be placed there, and will be automatically loaded when the server is started.
+11
View File
@@ -0,0 +1,11 @@
---
title: Third Party Integrations
---
## Third Party Integrations
A list of known third-party InvenTree extensions is provided [on our website](https://inventree.org/extend/integrate/) If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/).
## Available Plugins
Refer to the [InvenTree website](https://inventree.org/plugins.html) for a (non exhaustive) list of plugins that are available for InvenTree. This includes both official and third-party plugins.
@@ -0,0 +1,35 @@
## Label printer
Label printer machines can directly print labels for various items in InvenTree. They replace standard [`LabelPrintingMixin`](../mixins/label.md) plugins that are used to connect to physical printers. Using machines rather than a standard `LabelPrintingMixin` plugin has the advantage that machines can be created multiple times using different settings but the same driver. That way multiple label printers of the same brand can be connected.
### Writing your own printing driver
Take a look at the most basic required code for a driver in this [example](./overview.md#example-driver). Next either implement the [`print_label`](#machine.machine_types.LabelPrinterBaseDriver.print_label) or [`print_labels`](#machine.machine_types.LabelPrinterBaseDriver.print_labels) function.
### Label printer status
There are a couple of predefined status codes for label printers. By default the `UNKNOWN` status code is set for each machine, but they can be changed at any time by the driver. For more info about status code see [Machine status codes](./overview.md#machine-status).
::: machine.machine_types.label_printer.LabelPrinterStatus
options:
heading_level: 4
show_bases: false
show_docstring_description: false
### LabelPrintingDriver API
::: machine.machine_types.LabelPrinterBaseDriver
options:
heading_level: 4
show_bases: false
members:
- print_label
- print_labels
- get_printers
- PrintingOptionsSerializer
- get_printing_options_serializer
- machine_plugin
- render_to_pdf
- render_to_pdf_data
- render_to_html
- render_to_png
+214
View File
@@ -0,0 +1,214 @@
---
title: Machines
---
## Machines
InvenTree has a builtin machine registry. There are different machine types available where each type can have different drivers. Drivers and even custom machine types can be provided by plugins.
!!! info "Requires Redis"
If the machines features is used in production setup using workers, a shared [redis cache](../../start/processes.md#cache-server) is required to function properly.
### Registry
The machine registry is the main component which gets initialized on server start and manages all configured machines.
#### Initialization process
The machine registry initialization process can be divided into three stages:
- **Stage 1: Discover machine types:** by looking for classes that inherit the BaseMachineType class
- **Stage 2: Discover drivers:** by looking for classes that inherit the BaseDriver class (and are not referenced as base driver for any discovered machine type)
- **Stage 3: Machine loading:**
1. For each MachineConfig in database instantiate the MachineType class (drivers get instantiated here as needed and passed to the machine class. But only one instance of the driver class is maintained along the registry)
2. The driver.init_driver function is called for each used driver
3. The machine.initialize function is called for each machine, which calls the driver.init_machine function for each machine, then the machine.initialized state is set to true
#### Production setup (with a worker)
If a worker is connected, there exist multiple instances of the machine registry (one in each worker thread and one in the main thread) due to the nature of how python handles state in different processes. Therefore the machine instances and drivers are instantiated multiple times (The `__init__` method is called multiple times). But the init functions and update hooks (e.g. `init_machine`) are only called once from the main process.
The registry, driver and machine state (e.g. machine status codes, errors, ...) is stored in the cache. Therefore a shared redis cache is needed. (The local in-memory cache which is used by default is not capable to cache across multiple processes)
### Machine types
Each machine type can provide a different type of connection functionality between inventree and a physical machine. These machine types are already built into InvenTree.
#### Built-in types
| Name | Description |
| --- | --- |
| [Label printer](./label_printer.md) | Directly print labels for various items. |
#### Example machine type
If you want to create your own machine type, please also take a look at the already existing machine types in `machines/machine_types/*.py`. The following example creates a machine type called `abc`.
```py
from django.utils.translation import gettext_lazy as _
from generic.states import ColorEnum
from plugin.machine import BaseDriver, BaseMachineType, MachineStatus
class ABCBaseDriver(BaseDriver):
"""Base xyz driver."""
machine_type = 'abc'
def my_custom_required_method(self):
"""This function must be overridden."""
raise NotImplementedError('The `my_custom_required_method` function must be overridden!')
def my_custom_method(self):
"""This function can be overridden."""
raise NotImplementedError('The `my_custom_method` function can be overridden!')
required_overrides = [my_custom_required_method]
class ABCMachine(BaseMachineType):
SLUG = 'abc'
NAME = _('ABC')
DESCRIPTION = _('This is an awesome machine type for ABC.')
base_driver = ABCBaseDriver
class ABCStatus(MachineStatus):
CONNECTED = 100, _('Connected'), ColorEnum.success
STANDBY = 101, _('Standby'), ColorEnum.success
PRINTING = 110, _('Printing'), ColorEnum.primary
MACHINE_STATUS = ABCStatus
default_machine_status = ABCStatus.DISCONNECTED
```
#### Machine Type API
The machine type class gets instantiated for each machine on server startup and the reference is stored in the machine registry. (Therefore `machine.NAME` is the machine type name and `machine.name` links to the machine instances user defined name)
::: machine.BaseMachineType
options:
heading_level: 5
show_bases: false
members:
- machine_config
- name
- active
- initialize
- update
- restart
- handle_error
- clear_errors
- get_setting
- set_setting
- check_setting
- set_status
- set_status_text
### Drivers
Drivers provide the connection layer between physical machines and inventree. There can be multiple drivers defined for the same machine type. Drivers are provided by plugins that are enabled and extend the corresponding base driver for the particular machine type. Each machine type already provides a base driver that needs to be inherited.
#### Example driver
A basic driver only needs to specify the basic attributes like `SLUG`, `NAME`, `DESCRIPTION`. The others are given by the used base driver, so take a look at [Machine types](#machine-types). The following example will create an driver called `abc` for the `xyz` machine type. The class will be discovered if it is provided by an **installed & activated** plugin just like this:
```py
from plugin import InvenTreePlugin
from plugin.machine.machine_types import ABCBaseDriver
class MyXyzAbcDriverPlugin(InvenTreePlugin):
NAME = "XyzAbcDriver"
SLUG = "xyz-driver"
TITLE = "Xyz Abc Driver"
# ...
class XYZDriver(ABCBaseDriver):
SLUG = 'my-xyz-driver'
NAME = 'My XYZ driver'
DESCRIPTION = 'This is an awesome XYZ driver for a ABC machine'
```
#### Driver API
::: machine.BaseDriver
options:
heading_level: 5
show_bases: false
members:
- init_driver
- init_machine
- update_machine
- restart_machine
- get_machines
- handle_error
### Settings
Each machine can have different settings configured. There are machine settings that are specific to that machine type and driver settings that are specific to the driver, but both can be specified individually for each machine. Define them by adding a `MACHINE_SETTINGS` dictionary attribute to either the driver or the machine type. The format follows the same pattern as the `SETTINGS` for normal plugins documented on the [`SettingsMixin`](../mixins/settings.md)
```py
class MyXYZDriver(ABCBaseDriver):
MACHINE_SETTINGS = {
'SERVER': {
'name': _('Server'),
'description': _('IP/Hostname to connect to the cups server'),
'default': 'localhost',
'required': True,
}
}
```
Settings can even marked as `'required': True` which prevents the machine from starting if the setting is not defined.
### Machine status
Machine status can be used to report the machine status to the users. They can be set by the driver for each machine, but get lost on a server restart.
#### Codes
Each machine type has a set of status codes defined that can be set for each machine by the driver. There also needs to be a default status code defined.
```py
from plugin.machine import MachineStatus, BaseMachineType
class XYZStatus(MachineStatus):
CONNECTED = 100, _('Connected'), 'success'
STANDBY = 101, _('Standby'), 'success'
DISCONNECTED = 400, _('Disconnected'), 'danger'
class XYZMachineType(BaseMachineType):
# ...
MACHINE_STATUS = XYZStatus
default_machine_status = XYZStatus.DISCONNECTED
```
And to set a status code for a machine by the driver.
```py
class MyXYZDriver(ABCBaseDriver):
# ...
def init_machine(self, machine):
# ... do some init stuff here
machine.set_status(XYZMachineType.MACHINE_STATUS.CONNECTED)
```
**`MachineStatus` API**
::: machine.machine_type.MachineStatus
options:
heading_level: 5
show_bases: false
#### Free text
There can also be a free text status code defined.
```py
class MyXYZDriver(ABCBaseDriver):
# ...
def init_machine(self, machine):
# ... do some init stuff here
machine.set_status_text("Paper missing")
```
+131
View File
@@ -0,0 +1,131 @@
---
title: Model Metadata
---
## Model Metadata
Plugins have access to internal database models (such at [Parts](../part/index.md)), and any associated data associated with these models. It may be the case that a particular plugin needs to store some extra information about a particular model instance, to be able to perform custom functionality.
One way of achieving this would be to create an entirely new database model to keep track of this information, using the [app plugin mixin](./mixins/app.md). However, this is a very heavy-handed (and complicated) approach!
A much simpler and more accessible method of recording custom information against a given model instance is provided "out of the box" - using *Model Metadata*.
### MetadataMixin Class
*Most* of the models in the InvenTree database inherit from the `MetadataMixin` class, which adds the `metadata` field to each inheriting model. The `metadata` field is a [JSONField]({% include "django.html" %}/ref/models/fields/#django.db.models.JSONField) which allows for storing arbitrary JSON data against the model instance.
This field is provided to allow any plugins to store and retrieve arbitrary data against any item in the database.
!!! tip "External Use Only"
It is important to note that the `metadata` field of each model instance is not used for any internal functionality. Any data stored against this field is only for use by external plugins.
## Accessing Metadata
### Plugin Access
The `metadata` field can be accessed and updated directly from custom plugin code, as follows:
```python
from part.models import Part
# Show metadata value against a particular Part instance
part = Part.objects.get(pk=100)
print(part.metadata)
> {'foo': 'bar'}
part.metadata['hello'] = 'world'
print(part.metadata)
> {'foo': 'bar', 'hello': 'world'}
```
### API Access
For models which provide this metadata field, access is also provided via the API. Append `/metadata/` to the detail endpoint for a particular model instance to access.
For example:
{% with id="metadata_api", url="plugin/model_metadata_api.png", description="Access model metadata via API", maxheight="400px" %}
{% include 'img.html' %}
{% endwith %}
#### PUT vs PATCH
An important note with regard to metadata access via the API is the behaviour of a `PUT` request vs a `PATCH` request. As demonstrated in the comparison below, a `PUT` request will *overwrite* existing data, while a `PATCH` request will *merge* with existing data.
**Initial Data:**
```json
{"foo": "bar", "hello": "world"}
```
**Payload:**
```json
{"xyz": "XYZ"}
```
**Result of PUT request:**
```json
{"xyz: XYZ"}
```
**Result of PATCH request:**
```json
{"foo": "bar", "hello": "world", "xyz": "XYZ"}
```
!!! danger "Take Care"
Take care when updating metadata via the API, especially when using a PUT request.
### Python API Access
The [Python API library](../api/python/index.md) provides similar support for accessing model metadata. Use the `setMetadata` method to retrieve metadata information from the server:
```python
from inventree.api import InvenTreeAPI
from inventree.part import Part
api = InvenTreeAPI("http://localhost:8000", username="admin", password="inventree")
part = Part(api, pk=100)
print(part.getMetadata())
> {'foo': 'bar', 'hello': 'world'}
```
Metadata can be added directly here using the `setMetadata` method:
```python
part.setMetadata("abc", "xyz")
print(part.getMetadata())
> {'abc': 'xyz', 'foo': 'bar', 'hello': 'world'}
```
!!! tip "Merge vs Overwrite"
By default setting a metadata `key:value` pair will *merge* data in with existing data, by using a [PATCH request](#put-vs-patch).
To *overwrite* existing metadata, use the `overwrite=True` flag:
```python
part.setMetadata({"aaa": "ABC"}, overwrite=True)
print(part.getMetadata())
> {'aaa': 'ABC'}
```
## Considerations
### Data Keys
There is no guarantee that the data added to a particular model will *not* be overwritten by a different plugin. Your plugin should at least ensure that the data keys used are unique to the plugin, to ensure that they do not conflict with other plugins
### Structured Data
If you need to store data which is more "structured" than JSON objects, consider using the (more complex) [app mixin](./mixins/app.md) to develop custom database tables for your data.
+28
View File
@@ -0,0 +1,28 @@
---
title: Action Plugins
---
## ActionMixin
Arbitrary "actions" can be called by POSTing data to the `/api/action/` endpoint. The POST request must include the name of the action to be performed, and a matching ActionPlugin plugin must be loaded by the server. Arbitrary data can also be provided to the action plugin via the POST data:
```
POST {
action: "MyCustomAction",
data: {
foo: "bar",
}
}
```
### Sample Plugin
A sample action plugin is provided in the `InvenTree` source code, which can be used as a template for creating custom action plugins:
::: plugin.samples.integration.simpleactionplugin.SimpleActionPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+19
View File
@@ -0,0 +1,19 @@
---
title: Schedule Mixin
---
## APICallMixin
The APICallMixin class provides basic functionality for integration with an external API.
### Sample Plugin
The following example demonstrates how to use the `APICallMixin` class to make a simple API call:
::: plugin.samples.integration.api_caller.SampleApiCallerPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+10
View File
@@ -0,0 +1,10 @@
---
title: App Mixin
---
## AppMixin
If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration.
!!! warning "Danger Zone"
Only use this mixin if you have an understanding of Django's [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of Django to make sure your code does not cause unwanted behaviour in InvenTree before releasing.
+92
View File
@@ -0,0 +1,92 @@
---
title: Barcode Mixin
---
## Barcode Plugins
InvenTree supports decoding of arbitrary barcode data and generation of internal barcode formats via a **Barcode Plugin** interface. Barcode data POSTed to the `/api/barcode/` endpoint will be supplied to all loaded barcode plugins, and the first plugin to successfully interpret the barcode data will return a response to the client.
InvenTree can generate native QR codes to represent database objects (e.g. a single StockItem). This barcode can then be used to perform quick lookup of a stock item or location in the database. A client application (for example the InvenTree mobile app) scans a barcode, and sends the barcode data to the InvenTree server. The server then uses the **InvenTreeBarcodePlugin** (found at `src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py`) to decode the supplied barcode data.
Any third-party barcodes can be decoded by writing a matching plugin to decode the barcode data. These plugins could then perform a server-side action or render a JSON response back to the client for further action.
Some examples of possible uses for barcode integration:
- Stock lookup by scanning a barcode on a box of items
- Receiving goods against a PurchaseOrder by scanning a supplier barcode
- Perform a stock adjustment action (e.g. take 10 parts from stock whenever a barcode is scanned)
Barcode data are POSTed to the server as follows:
```
POST {
barcode_data: "[(>someBarcodeDataWhichThePluginKnowsHowToDealWith"
}
```
### Builtin Plugin
The InvenTree server includes a builtin barcode plugin which can generate and decode the QR codes. This plugin is enabled by default.
::: plugin.builtin.barcodes.inventree_barcode.InvenTreeInternalBarcodePlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Example Plugin
Please find below a very simple example that is used to return a part if the barcode starts with `PART-`
```python
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin
from part.models import Part
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
NAME = "MyBarcode"
TITLE = "My Barcodes"
DESCRIPTION = "support for barcodes"
VERSION = "0.0.1"
AUTHOR = "Michael"
def scan(self, barcode_data):
if barcode_data.startswith("PART-"):
try:
pk = int(barcode_data.split("PART-")[1])
instance = Part.objects.get(pk=pk)
label = Part.barcode_model_type()
return {label: instance.format_matched_response()}
except Part.DoesNotExist:
pass
```
To try it just copy the file to src/InvenTree/plugins and restart the server. Open the scan barcode window and start to scan codes or type in text manually. Each time the timeout is hit the plugin will execute and printout the result. The timeout can be changed in `Settings->Barcode Support->Barcode Input Delay`.
### Custom Internal Format
To implement a custom internal barcode format, the `generate(...)` method from the Barcode Mixin needs to be overridden. Then the plugin can be selected at `System Settings > Barcodes > Barcode Generation Plugin`.
```python
from InvenTree.models import InvenTreeBarcodeMixin
from plugin import InvenTreePlugin
from plugin.mixins import BarcodeMixin
class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
NAME = "MyInternalBarcode"
TITLE = "My Internal Barcodes"
DESCRIPTION = "support for custom internal barcodes"
VERSION = "0.0.1"
AUTHOR = "InvenTree contributors"
def generate(self, model_instance: InvenTreeBarcodeMixin):
return f'{model_instance.barcode_model_type()}: {model_instance.pk}'
```
!!! info "Scanning implementation required"
The parsing of the custom format needs to be implemented too, so that the scanning of the generated QR codes resolves to the correct part.
+62
View File
@@ -0,0 +1,62 @@
---
title: Currency Exchange Mixin
---
## CurrencyExchangeMixin
The `CurrencyExchangeMixin` class enabled plugins to provide custom backends for updating currency exchange rate information.
Any implementing classes must provide the `update_exchange_rates` method.
### Builtin Plugin
The default builtin plugin for handling currency exchange rates is the `InvenTreeCurrencyExchangePlugin` class.
::: plugin.builtin.integration.currency_exchange.InvenTreeCurrencyExchange
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Sample Plugin
A simple example is shown below (with fake data).
```python
from plugin import InvenTreePlugin
from plugin.mixins import CurrencyExchangeMixin
class MyFirstCurrencyExchangePlugin(CurrencyExchangeMixin, InvenTreePlugin):
"""Sample currency exchange plugin"""
...
def update_exchange_rates(self, base_currency: str, symbols: list[str]) -> dict:
"""Update currency exchange rates.
This method *must* be implemented by the plugin class.
Arguments:
base_currency: The base currency to use for exchange rates
symbols: A list of currency symbols to retrieve exchange rates for
Returns:
A dictionary of exchange rates, or None if the update failed
Raises:
Can raise any exception if the update fails
"""
rates = {
'base_currency': 1.00
}
for sym in symbols:
rates[sym] = random.randrange(5, 15) * 0.1
return rates
```
+187
View File
@@ -0,0 +1,187 @@
---
title: Event Mixin
---
## EventMixin
The `EventMixin` class enables plugins to respond to certain triggered events.
When a certain (server-side) event occurs, the background worker passes the event information to any plugins which inherit from the `EventMixin` base class.
!!! tip "Enable Event Integration"
The *Enable Event Integration* option must first be enabled to allow plugins to respond to events.
{% with id="events", url="plugin/enable_events.png", description="Enable event integration" %}
{% include 'img.html' %}
{% endwith %}
## Events
Events are passed through using a string identifier, e.g. `build.completed`
The arguments (and keyword arguments) passed to the receiving function depend entirely on the type of event.
!!! info "Read the Code"
Implementing a response to a particular event requires a working knowledge of the InvenTree code base, especially related to that event being received. While the *available* events are documented here, to implement a response to a particular event you will need to read the code to understand what data is passed to the event handler.
## Generic Events
There are a number of *generic* events which are generated on certain database actions. Whenever a database object is created, updated, or deleted, a corresponding event is generated.
#### Object Created
When a new object is created in the database, an event is generated with the following event name: `<app>_<model>.created`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).
The event is called with the following keywords arguments:
- `model`: The model class of the object that was created
- `id`: The primary key of the object that was created
**Example:**
A new `Part` object is created with primary key `123`, resulting in the following event being generated:
```python
trigger_event('part_part.created', model='part', id=123)
```
### Object Updated
When an object is updated in the database, an event is generated with the following event name: `<app>_<model>.saved`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).
The event is called with the following keywords arguments:
- `model`: The model class of the object that was updated
- `id`: The primary key of the object that was updated
**Example:**
A `Part` object with primary key `123` is updated, resulting in the following event being generated:
```python
trigger_event('part_part.saved', model='part', id=123)
```
### Object Deleted
When an object is deleted from the database, an event is generated with the following event name: `<app>_<model>.deleted`, where `<model>` is the name of the model class (e.g. `part`, `stockitem`, etc).
The event is called with the following keywords arguments:
- `model`: The model class of the object that was deleted
- `id`: The primary key of the object that was deleted (if available)
**Example:**
A `Part` object with primary key `123` is deleted, resulting in the following event being generated:
```python
trigger_event('part_part.deleted', model='part', id=123)
```
!!! warning "Object Deleted"
Note that the event is triggered *after* the object has been deleted from the database, so the object itself is no longer available.
## Specific Events
In addition to the *generic* events listed above, there are a number of other events which are triggered by *specific* actions within the InvenTree codebase.
The available events are provided in the enumerations listed below. Note that while the names of the events are documented here, the exact arguments passed to the event handler will depend on the specific event being triggered.
### Build Events
::: build.events.BuildEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Part Events
::: part.events.PartEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Stock Events
::: stock.events.StockEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Purchase Order Events
::: order.events.PurchaseOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Sales Order Events
::: order.events.SalesOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Return Order Events
::: order.events.ReturnOrderEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Plugin Events
::: plugin.events.PluginEvents
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
## Samples
### Sample Plugin - All events
Implementing classes must at least provide a `process_event` function:
::: plugin.samples.event.event_sample.EventPluginSample
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Sample Plugin - Specific Events
If you want to process just some specific events, you can also implement the `wants_process_event` function to decide if you want to process this event or not. This function will be executed synchronously, so be aware that it should contain simple logic.
Overall this function can reduce the workload on the background workers significantly since less events are queued to be processed.
::: plugin.samples.event.filtered_event_sample.FilteredEventPluginSample
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+122
View File
@@ -0,0 +1,122 @@
---
title: Data Export Mixin
---
## DataExportMixin
The `DataExportMixin` class provides a plugin with the ability to customize the data export process. The [InvenTree API](../../api/index.md) provides an integrated method to export a dataset to a tabulated file. The default export process is generic, and simply exports the data presented via the API in a tabulated file format.
Custom data export plugins allow this process to be adjusted:
- Data columns can be added or removed
- Rows can be removed or added
- Custom calculations or annotations can be performed.
### Supported Export Types
Each plugin can dictate which datasets are supported using the `supports_export` method. This allows a plugin to dynamically specify whether it can be selected by the user for a given export session.
::: plugin.base.integration.DataExport.DataExportMixin.supports_export
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
The default implementation returns `True` for all data types.
### Filename Generation
The `generate_filename` method constructs a filename for the exported file.
::: plugin.base.integration.DataExport.DataExportMixin.generate_filename
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Adjust Columns
The `update_headers` method allows the plugin to adjust the columns selected to be exported to the file.
::: plugin.base.integration.DataExport.DataExportMixin.update_headers
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Queryset Filtering
The `filter_queryset` method allows the plugin to provide custom filtering to the database query, before it is exported.
::: plugin.base.integration.DataExport.DataExportMixin.filter_queryset
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Export Data
The `export_data` method performs the step of transforming a [Django QuerySet]({% include "django.html" %}/ref/models/querysets/) into a dataset which can be processed by the [tablib](https://tablib.readthedocs.io/en/stable/) library.
::: plugin.base.integration.DataExport.DataExportMixin.export_data
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
Note that the default implementation simply uses the builtin tabulation functionality of the provided serializer class. In most cases, this will be sufficient.
## Custom Export Options
To provide the user with custom options to control the behavior of the export process *at the time of export*, the plugin can define a custom serializer class.
To enable this feature, define an `ExportOptionsSerializer` attribute on the plugin class which points to a DRF serializer class. Refer to the examples below for more information.
### Builtin Exporter Classes
InvenTree provides the following builtin data exporter classes.
### InvenTreeExporter
A generic exporter class which simply serializes the API output into a data file.
::: plugin.builtin.exporter.inventree_exporter.InvenTreeExporter
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### BOM Exporter
A custom exporter which only supports [bill of materials](../../manufacturing/bom.md) exporting.
::: plugin.builtin.exporter.bom_exporter.BomExporterPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
## Source Code
The full source code of the `DataExportMixin` class:
{{ includefile("src/backend/InvenTree/plugin/base/integration/DataExport.py", title="DataExportMixin") }}
+19
View File
@@ -0,0 +1,19 @@
---
title: Icon Pack Mixin
---
## IconPackMixin
The IconPackMixin class provides basic functionality for letting plugins expose custom icon packs that are available in the InvenTree UI. This is especially useful to provide a custom crafted icon pack with icons for different location types, e.g. different sizes and styles of drawers, bags, ESD bags, ... which are not available in the standard tabler icons library.
### Sample Plugin
The following example demonstrates how to use the `IconPackMixin` class to add a custom icon pack:
::: plugin.samples.icons.icon_sample.SampleIconPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+186
View File
@@ -0,0 +1,186 @@
---
title: Label Mixin
---
## LabelPrintingMixin
The `LabelPrintingMixin` class allows plugins to provide custom label printing functionality. The specific implementation of a label printing plugin is quite flexible, allowing for the following functions (as a starting point):
- Printing a single label to a file, and allowing user to download
- Combining multiple labels onto a single page
- Supporting proprietary label sheet formats
- Offloading label printing to an external printer
### Entry Point
When printing labels against a particular plugin, the entry point is the `print_labels` method. The default implementation of this method iterates over each of the provided items, renders a PDF, and calls the `print_label` method for each item, providing the rendered PDF data.
Both the `print_labels` and `print_label` methods may be overridden by a plugin, allowing for complex functionality to be achieved.
For example, the `print_labels` method could be reimplemented to merge all labels into a single larger page, and return a single page for printing.
### Return Type
The `print_labels` method *must* return a JsonResponse object. If the method does not return such a response, an error will be raised by the server.
### File Generation
If the label printing plugin generates a real file, it should be stored as a `LabelOutput` instance in the database, and returned in the JsonResponse result under the 'file' key.
For example, the built-in `InvenTreeLabelPlugin` plugin generates a PDF file which contains all the provided labels concatenated together. A snippet of the code is shown below (refer to the source code for full details):
```python
# Save the generated file to the database
output = LabelOutput.objects.create(
label=output_file,
user=request.user
)
return JsonResponse({
'file': output.label.url,
'success': True,
'message': f'{len(items)} labels generated'
})
```
### Background Printing
For some label printing processes (such as offloading printing to an external networked printer) it may be preferable to utilize the background worker process, and not block the front-end server.
The plugin provides an easy method to offload printing to the background thread.
Simply override the class attribute `BLOCKING_PRINT` as follows:
```python
class MyPrinterPlugin(LabelPrintingMixin, InvenTreePlugin):
BLOCKING_PRINT = False
```
If the `print_labels` method is not changed, this will run the `print_label` method in a background worker thread.
!!! info "Example Plugin"
Check out the [inventree-brother-plugin](https://github.com/inventree/inventree-brother-plugin) which provides native support for the Brother QL and PT series of networked label printers
!!! tip "Custom Code"
If your plugin overrides the `print_labels` method, you will have to ensure that the label printing is correctly offloaded to the background worker. Look at the `offload_label` method of the plugin mixin class for how this can be achieved.
### Printing options
A printing plugin can define custom options as a serializer class called `PrintingOptionsSerializer` that get shown on the printing screen and get passed to the `print_labels`/`print_label` function as a kwarg called `printing_options`. This can be used to e.g. let the user dynamically select the orientation of the label, the color mode, ... for each print job.
The following simple example shows how to implement an orientation select. For more information about how to define fields, refer to the django rest framework (DRF) [documentation](https://www.django-rest-framework.org/api-guide/fields/).
```py
from rest_framework import serializers
class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
...
class PrintingOptionsSerializer(serializers.Serializer):
orientation = serializers.ChoiceField(choices=[
("landscape", "Landscape"),
("portrait", "Portrait"),
])
def print_label(self, **kwargs):
print(kwargs["printing_options"]) # -> {"orientation": "landscape"}
...
```
!!! tip "Dynamically return a serializer instance"
If your plugin wants to dynamically expose options based on the request, you can implement the `get_printing_options_serializer` function which by default returns an instance
of the `PrintingOptionsSerializer` class if defined.
### Helper Methods
The plugin class provides a number of additional helper methods which may be useful for generating labels:
| Method | Description |
| --- | --- |
| render_to_pdf | Render label template to an in-memory PDF object |
| render_to_html | Render label template to a raw HTML string |
| render_to_png | Convert PDF data to an in-memory PNG image |
!!! info "Use the Source"
These methods are available for more complex implementations - refer to the source code for more information!
### Merging Labels
To merge (combine) multiple labels into a single output (for example printing multiple labels on a single sheet of paper), the plugin must override the `print_labels` method and implement the required functionality.
## Integration
### Web Integration
If label printing plugins are enabled, they are able to be used directly from the InvenTree web interface:
{% with id="label_print", url="plugin/print_label_select_plugin.png", description="Print label via plugin" %}
{% include 'img.html' %}
{% endwith %}
### App Integration
Label printing plugins also allow direct printing of labels via the [mobile app](../../app/stock.md#print-label)
## Implementation
Plugins which implement the `LabelPrintingMixin` mixin class can be implemented by simply providing a `print_label` method.
### Simple Example
```python
from dummy_printer import printer_backend
class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
"""
A simple example plugin which provides support for a dummy printer.
A more complex plugin would communicate with an actual printer!
"""
NAME = "MyLabelPrinter"
SLUG = "mylabel"
TITLE = "A dummy printer"
# Set BLOCKING_PRINT to false to return immediately
BLOCKING_PRINT = False
def print_label(self, **kwargs):
"""
Send the label to the printer
kwargs:
pdf_data: Raw PDF data of the rendered label
filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method
item_instance: The instance of the database model against which the label is printed
user: The user who triggered this print job
width: The expected width of the label (in mm)
height: The expected height of the label (in mm)
printing_options: The printing options set for this print job defined in the PrintingOptionsSerializer
"""
width = kwargs['width']
height = kwargs['height']
# This dummy printer supports printing of raw image files
printer_backend.print(png_file, w=width, h=height)
```
### Default Plugin
InvenTree supplies the `InvenTreeLabelPlugin` out of the box, which generates a PDF file which is then available for immediate download by the user.
The default plugin also features a *DEBUG* mode which generates a raw HTML output, rather than PDF. This can be handy for tracking down any template rendering errors in your labels.
::: plugin.builtin.labels.inventree_label.InvenTreeLabelPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### Available Data
The *label* data are supplied to the plugin in both `PDF` and `PNG` formats. This provides compatibility with a great range of label printers "out of the box". Conversion to other formats, if required, is left as an exercise for the plugin developer.
Other arguments provided to the `print_label` function are documented in the code sample above.
+43
View File
@@ -0,0 +1,43 @@
---
title: Locate Mixin
---
## LocateMixin
The `LocateMixin` class enables plugins to "locate" stock items (or stock locations) via an entirely custom method.
For example, a warehouse could be arranged with each individual 'parts bin' having an audio-visual indicator (e.g. RGB LED and buzzer). "Locating" a particular stock item causes the LED to flash and the buzzer to sound.
Another example might be a parts retrieval system, where "locating" a stock item causes the stock item to be "delivered" to the user via a conveyor.
The possibilities are endless!
### Web Integration
{% with id="web_locate", url="plugin/web_locate.png", description="Locate stock item from web interface", maxheight="400px" %}
{% include 'img.html' %}
{% endwith %}
### App Integration
If a locate plugin is installed and activated, the [InvenTree mobile app](../../app/index.md) displays a button for locating a StockItem or StockLocation (see below):
{% with id="app_locate", url="plugin/app_locate.png", description="Locate stock item from app", maxheight="400px" %}
{% include 'img.html' %}
{% endwith %}
### Implementation
Refer to the [InvenTree source code]({{ sourcefile("src/backend/InvenTree/plugin/samples/locate/locate_sample.py") }}) for a simple implementation example.
### Sample Plugin
A simple example is provided in the InvenTree code base:
::: plugin.samples.locate.locate_sample.SampleLocatePlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+23
View File
@@ -0,0 +1,23 @@
---
title: Navigation Mixin
---
## NavigationMixin
Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header.
The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon.
``` python
class MyNavigationPlugin(NavigationMixin, InvenTreePlugin):
NAME = "NavigationPlugin"
NAVIGATION = [
{'name': 'SampleIntegration', 'link': 'plugin:sample:hi', 'icon': 'ti ti-box'},
]
NAVIGATION_TAB_NAME = "Sample Nav"
NAVIGATION_TAB_ICON = 'ti ti-plus-circle'
```
The optional class constants `NAVIGATION_TAB_NAME` and `NAVIGATION_TAB_ICON` can be used to change the name and icon for the parent navigation node.
+27
View File
@@ -0,0 +1,27 @@
---
title: Report Mixin
---
## ReportMixin
The `ReportMixin` class provides a plugin with the ability to extend the functionality of custom [report templates](../../report/report.md). A plugin which implements the ReportMixin mixin class can add custom context data to a report template for rendering.
### Add Report Context
A plugin which implements the ReportMixin mixin can define the `add_report_context` method, allowing custom context data to be added to a report template at time of printing.
### Add Label Context
Additionally the `add_label_context` method, allowing custom context data to be added to a label template at time of printing.
### Sample Plugin
A sample plugin which provides additional context data to the report templates is available:
::: plugin.samples.integration.report_plugin_sample.SampleReportPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+31
View File
@@ -0,0 +1,31 @@
---
title: Schedule Mixin
---
## ScheduleMixin
The ScheduleMixin class provides a plugin with the ability to call functions at regular intervals.
- Functions are registered with the InvenTree worker which runs as a background process.
- Scheduled functions do not accept any arguments
- Plugin member functions can be called
- Global functions can be specified using dotted notation
!!! tip "Enable Schedule Integration"
The *Enable Schedule Integration* option but be enabled, for scheduled plugin events to be activated.
{% with id="schedule", url="plugin/enable_schedule.png", description="Enable schedule integration" %}
{% include 'img.html' %}
{% endwith %}
### SamplePlugin
An example of a plugin which supports scheduled tasks:
::: plugin.samples.integration.scheduled_task.ScheduledTaskPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+96
View File
@@ -0,0 +1,96 @@
---
title: Settings Mixin
---
## SettingsMixin
The *SettingsMixin* allows the plugin to save and load persistent settings to the database.
- Plugin settings are stored against the individual plugin, and thus do not have to be unique
- Plugin settings are stored using a "key:value" pair
Use the class constant `SETTINGS` for a dict of settings that should be added as global database settings.
The dict must be formatted similar to the following sample that shows how to use validator choices and default.
Take a look at the settings defined in `InvenTree.common.models.InvenTreeSetting` for all possible parameters.
### Example Plugin
Below is a simple example of how a plugin can implement settings:
``` python
class PluginWithSettings(SettingsMixin, InvenTreePlugin):
NAME = "PluginWithSettings"
SETTINGS = {
'API_ENABLE': {
'name': 'API Functionality',
'description': 'Enable remote API queries',
'validator': bool,
'default': True,
},
'API_KEY': {
'name': 'API Key',
'description': 'Security key for accessing remote API',
'default': '',
'required': True,
},
'API_URL': {
'name': _('API URL'),
'description': _('Base URL for remote server'),
'default': 'http://remote.url/api',
},
'CONNECTION': {
'name': _('Printer Interface'),
'description': _('Select local or network printer'),
'choices': [('local','Local printer e.g. USB'),('network','Network printer with IP address')],
'default': 'local',
},
'NUMBER': {
'name': _('A Name'),
'description': _('Describe me here'),
'default': 6,
'validator': [
int,
MinValueValidator(2),
MaxValueValidator(25)
]
},
'ASSEMBLY': {
'name': _('Assembled Part'),
'description': _('Settings can point to internal database models'),
'model': 'part.part',
'model_filters': {
'active': True,
'assembly': True
}
},
'GROUP': {
'name': _('User Group'),
'description': _('Select a group of users'),
'model': 'auth.group'
},
'HIDDEN_SETTING': {
'name': _('Hidden Setting'),
'description': _('This setting is hidden from the automatically generated plugin settings page'),
'hidden': True,
}
}
```
!!! info "More Info"
For more information on any of the methods described below, refer to the InvenTree source code.
!!! tip "Hidden Settings"
Plugin settings can be hidden from the settings page by marking them as 'hidden'
This mixin defines the helper functions `plugin.get_setting`, `plugin.set_setting` and `plugin.check_settings` to access all plugin specific settings. The `plugin.check_settings` function can be used to check if all settings marked with `'required': True` are defined and not equal to `''`. Note that these methods cannot be used in the `__init__` function of your plugin.
```python
api_url = self.get_setting('API_URL', cache = False)
self.set_setting('API_URL', 'some value')
is_valid, missing_settings = self.check_settings()
```
`get_setting` has an additional parameter which lets control if the value is taken directly from the database or from the cache. If it is left away `False` is the default that means the value is taken directly from the database.
+263
View File
@@ -0,0 +1,263 @@
---
title: User Interface Mixin
---
## User Interface Mixin
The `UserInterfaceMixin` class provides a set of methods to implement custom functionality for the InvenTree web interface.
### Enable User Interface Mixin
To enable user interface plugins, the global setting `ENABLE_PLUGINS_INTERFACE` must be enabled, in the [plugin settings](../../settings/global.md#plugin-settings).
## Custom UI Features
The InvenTree user interface functionality can be extended in various ways using plugins. Multiple types of user interface *features* can be added to the InvenTree user interface.
The entrypoint for user interface plugins is the `UserInterfaceMixin` class, which provides a number of methods which can be overridden to provide custom functionality. The `get_ui_features` method is used to extract available user interface features from the plugin:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_features
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
Note here that the `get_ui_features` calls other methods to extract the available features from the plugin, based on the requested feature type. These methods can be overridden to provide custom functionality.
!!! info "Implementation"
Your custom plugin does not need to override the `get_ui_features` method. Instead, override one of the other methods to provide custom functionality.
### UIFeature Return Type
The `get_ui_features` method should return a list of `UIFeature` objects, which define the available user interface features for the plugin. The `UIFeature` class is defined as follows:
::: plugin.base.ui.mixins.UIFeature
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
Note that the *options* field contains fields which may be specific to a particular feature type - read the documentation below on each feature type for more information.
### Dynamic Feature Loading
Each of the provided feature types can be loaded dynamically by the plugin, based on the information provided in the API request. For example, the plugin can choose to show or hide a particular feature based on the user permissions, or the current state of the system.
For examples of this dynamic feature loading, refer to the [sample plugin](#sample-plugin) implementation which demonstrates how to dynamically load custom panels based on the provided context.
### Javascript Source Files
The rendering function for the custom user interface features expect that the plugin provides a Javascript source file which contains the necessary code to render the custom content. The path to this file should be provided in the `source` field of the `UIFeature` object.
Note that the `source` field can include the name of the function to be called (if this differs from the expected default function name).
For example:
```
"source": "/static/plugins/my_plugin/my_plugin.js:my_custom_function"
```
## Available UI Feature Types
The following user interface feature types are available:
### Dashboard Items
The InvenTree dashboard is a collection of "items" which are displayed on the main dashboard page. Custom dashboard items can be added to the dashboard by implementing the `get_ui_dashboard_items` method:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_dashboard_items
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Dashboard Item Options
The *options* field in the returned `UIFeature` object can contain the following properties:
::: plugin.base.ui.mixins.CustomDashboardItemOptions
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Source Function
The frontend code expects a path to a javascript file containing a function named `renderDashboardItem` which will be called to render the custom dashboard item. Note that this function name can be overridden by appending the function name in the `source` field of the `UIFeature` object.
#### Example
Refer to the [sample plugin](#sample-plugin) for an example of how to implement server side rendering for custom panels.
### Panels
Many of the pages in the InvenTree web interface are built using a series of "panels" which are displayed on the page. Custom panels can be added to these pages, by implementing the `get_ui_panels` method:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_panels
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Panel Options
The *options* field in the returned `UIFeature` object can contain the following properties:
::: plugin.base.ui.mixins.CustomPanelOptions
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Source Function
The frontend code expects a path to a javascript file containing a function named `renderPanel` which will be called to render the custom panel. Note that this function name can be overridden by appending the function name in the `source` field of the `UIFeature` object.
#### Example
Refer to the [sample plugin](#sample-plugin) for an example of how to implement server side rendering for custom panels.
### Template Editors
The `get_ui_template_editors` feature type can be used to provide custom template editors.
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_template_editors
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Template previews
The `get_ui_template_previews` feature type can be used to provide custom template previews:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_template_previews
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
## Plugin Context
When rendering certain content in the user interface, the rendering functions are passed a `context` object which contains information about the current page being rendered. The type of the `context` object is defined in the `PluginContext` file:
{{ includefile("src/frontend/src/components/plugins/PluginContext.tsx", title="Plugin Context", fmt="javascript") }}
This context data can be used to provide additional information to the rendering functions, and can be used to dynamically render content based on the current state of the system.
### Additional Context
Note that additional context can be passed to the rendering functions by adding additional key-value pairs to the `context` field in the `UIFeature` return type (provided by the backend via the API). This field is optional, and can be used at the discretion of the plugin developer.
## File Distribution
When distributing a custom UI plugin, the plugin should include the necessary frontend code to render the custom content. This frontend code should be included in the plugin package, and should be made available to the InvenTree frontend when the plugin is installed.
The simplest (and recommended) way to achieve this is to distribute the compiled javascript files with the plugin package, in a top-level `static` directory. This directory will be automatically collected by InvenTree when the plugin is installed, and the files will be copied to the appropriate location.
Read more about [static plugin files](../index.md#static-files) for more information.
## Sample Plugin
A (very simple) sample plugin which implements custom user interface functionality is provided in the InvenTree source code, which provides a full working example of how to implement custom user interface functionality.
::: plugin.samples.integration.user_interface_sample.SampleUserInterfacePlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
### More Examples
Some more complex examples of user interface plugins can be found on the InvenTree GitHub repository:
- [inventree-test-statistics](https://github.com/inventree/inventree-test-statistics)
- [inventree-order-history](https://github.com/inventree/inventree-order-history)
## Consistent Theming
When developing a custom UI plugin for InvenTree, the plugin should aim to match the existing InvenTree theme as closely as possible. This will help to ensure that the custom content fits seamlessly into the existing user interface.
To achieve this, we strongly recommend that you use the same framework as the InvenTree frontend - which is built using [React](https://react.dev) on top of the [Mantine](https://mantine.dev) UI component library.
### Mantine
The Mantine UI component library is used throughout the InvenTree frontend, and provides a consistent look and feel to the user interface. By using Mantine components in your custom UI plugin, you can ensure that your custom content fits seamlessly into the existing InvenTree theme.
### InvenTree Component Library
We are working to develop and distribute a library of custom InvenTree components which can be used to build custom UI plugins. This library will be made available to plugin developers in the near future.
### Examples
Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration.
## Building a User Interface Plugin
The technology stack which allows custom plugins to hook into the InvenTree user interface utilizes the following components:
- [React](https://react.dev)
- [Mantine](https://mantine.dev)
- [TypeScript](https://www.typescriptlang.org/)
- [Vite](https://vitejs.dev/)
While you don't need to be an expert in all of these technologies, it is recommended that you have a basic understanding of how they work together to build the InvenTree user interface. To get started, you should familiarize yourself with the frontend code (at `./src/frontend/`) as well as the vite configuration for the [InvenTree plugin creator](httsps://github.com/inventree/plugin-creator).
### Bundled with InvenTree
If a plugin is bundled with a separate copy of React libraries, issues may arise either due to version mismatches or because the React context is not shared between the InvenTree core and the plugin. This can lead to issues with rendering components, as the React context may not be shared between the two libraries.
To avoid issues, the InvenTree UI provides globally accessible components, which can be used as external modules by the plugin. This allows the plugin to use the same React context as the InvenTree core, and ensures that the plugin is compatible with the InvenTree user interface.
The following modules are provided as global objects at runtime:
- `React`
- `ReactDOM`
- `ReactDOMClient`
Additionally, for the Mantine library, the following modules are provided as global objects at runtime:
- `@mantine/core`
- `@mantine/hooks`
- `@mantine/notifications`
To use these modules in your plugin, they must be correctly *externalized* in the Vite configuration. Getting this right is crucial to ensure that the plugin is compatible with the InvenTree user interface. The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides a good starting point for this configuration, and can be used to generate a new plugin with the correct configuration.
!!! info "Bundled Version"
Keep in mind that the version of React and Mantine used in the InvenTree core may differ from the version used in your plugin. It is recommended to use the same version as the InvenTree core to avoid compatibility issues.
### Plugin Creator
The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides an out-of-the-box setup for creating InvenTree plugins which integrate into the user interface. This includes a pre-configured Vite setup, which allows you to quickly get started with building your own custom UI plugins.
Using the plugin creator tool is the recommended way to get started with building custom UI plugins for InvenTree, as it provides a solid foundation to build upon. It is also the only method which is officially supported by the InvenTree development team!
### DIY
Of course, you can also build your own custom UI plugins from scratch. This is a more advanced option, and requires a good understanding of the InvenTree codebase, as well as the technologies used to build the user interface. You are free to use other web technologies, however if you choose to do this, don't expect any support from the InvenTree development team. We will only provide support for plugins which are built using the recommended stack, and which follow the guidelines outlined in this documentation.
+87
View File
@@ -0,0 +1,87 @@
---
title: URLs Mixin
---
## UrlsMixin
Use the class constant `URLS` for a array of URLs that should be added to InvenTrees URL paths or override the `plugin.setup_urls` function.
The array has to contain valid URL patterns as defined in the [django documentation]({% include "django.html" %}/topics/http/urls/).
``` python
class MyUrlsPlugin(UrlsMixin, InvenTreePlugin):
NAME = "UrlsMixin"
URLS = [
re_path(r'increase/(?P<location>\d+)/(?P<pk>\d+)/', self.view_increase, name='increase-level'),
]
```
The URLs get exposed under `/plugin/{plugin.slug}/*` and get exposed to the template engine with the prefix `plugin:{plugin.slug}:` (for usage with the [url tag]({% include "django.html" %}/ref/templates/builtins/#url)).
!!! info "Note"
In this example, when an HTTP request is made to `/plugin/{plugin.slug}/increase/.../...` the function `self.view_increase` is called and returns the view to be displayed (step 4 in the Django documentation)
### Views
If your plugin will implement and host another webpage, familiarize yourself with Django views. Implementation is exactly the same.
A good place to start is the [django documentation]({% include "django.html" %}/topics/http/views/). Additional InvenTree-specific information is below.
### Rendering Views
Rendering templated views is also supported. Templated HTML files should be placed inside your plugin folder in a sub folder called `templates`.
Placed here, the template can be called using the file name and the render command.
Example in context (inside the main plugin python file):
``` py
def view_test(self, request):
return render(request, 'test.html', context)
def setup_urls(self):
return [
path('test/', self.view_test, name='test')
]
```
#### Panels
InvenTree uses bootstrap panels to display the page's content. These panels are locate inside the block `page_content`.
Example:
```html
{% raw %}
<div class='panel panel-hidden' id='panel-loans'>
<div class='panel-heading'>
<div class='d-flex flex-wrap'>
<h4>{% trans "Loaning Information" %}</h4>
</div>
</div>
<div class='panel-content'>
...
</div>
</div>
{% endraw %}
```
Notice that this example has the panel initially hidden.
This is where the `enableSidebar('...');'` function comes back into play. Panels are enabled according to the labels of items in the sidebar. Each sidebar item must declare a label corresponding to a panel. An example of a sidebar item within with the label `loans` is below.
```html
{% raw %}
{% trans "Loaning" as text %}
{% include "sidebar_item.html" with label='loans' text=text icon="fa-sitemap" %}
{% endraw %}
```
Note: This code is assumed to be placed within the `sidebar` block.
The `enableSidebar('...');'` function will un-hide the panel with the label `panel-...` (for this example, `panel-loans`) and hide all other panels. This allows you to have multiple panels on a page, but only show the panel corresponding to the current selected sidebar item.
Whenever you click a sidebar item, it will automatically enable the panel with the corresponding label and hide all other panels.
Additionally, when a panel is loaded, the function `onPanelLoad(...)` will be called for the associated panel.
If you would like to add javascript functionality to a panel after it loads, add the function within the `{% raw %}{% block js_ready %}{% endraw %}` block of your template file.
Example:
```js
onPanelLoad('loans', function() {
...
});;
```
+282
View File
@@ -0,0 +1,282 @@
---
title: Validation Mixin
---
## ValidationMixin
The `ValidationMixin` class enables plugins to perform custom validation of objects within the database.
Any of the methods described below can be implemented in a custom plugin to provide functionality as required.
!!! info "More Info"
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point]({{ sourcefile("src/backend/InvenTree/plugin/samples/integration/validation_sample.py") }}).
!!! info "Multi Plugin Support"
It is possible to have multiple plugins loaded simultaneously which support validation methods. For example when validating a field, if one plugin returns a null value (`None`) then the *next* plugin (if available) will be queried.
## Model Deletion
Any model which inherits the `PluginValidationMixin` class is exposed to the plugin system for custom deletion validation. Before the model is deleted from the database, it is first passed to the plugin ecosystem to check if it really should be deleted.
A custom plugin may implement the `validate_model_deletion` method to perform custom validation on the model instance before it is deleted.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_model_deletion
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
## Model Validation
Any model which inherits the `PluginValidationMixin` mixin class is exposed to the plugin system for custom validation. Before the model is saved to the database (either when created, or updated), it is first passed to the plugin ecosystem for validation.
Any plugin which inherits the `ValidationMixin` can implement the `validate_model_instance` method, and run a custom validation routine.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_model_instance
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Error Messages
Any error messages must be raised as a `ValidationError`. The `ValidationMixin` class provides the `raise_error` method, which is a simple wrapper method which raises a `ValidationError`
#### Instance Errors
To indicate an *instance* validation error (i.e. the validation error applies to the entire model instance), the body of the error should be either a string, or a list of strings.
#### Field Errors
To indicate a *field* validation error (i.e. the validation error applies only to a single field on the model instance), the body of the error should be a dict, where the key(s) of the dict correspond to the model fields.
Note that an error can be which corresponds to multiple model instance fields.
### Example Plugin
Presented below is a simple working example for a plugin which implements the `validate_model_instance` method:
```python
from plugin import InvenTreePlugin
from plugin.mixins import ValidationMixin
import part.models
class MyValidationMixin(ValidationMixin, InvenTreePlugin):
"""Custom validation plugin."""
def validate_model_instance(self, instance, deltas=None):
"""Custom model validation example.
- A part name and category name must have the same starting letter
- A PartCategory description field cannot be shortened after it has been created
"""
if isinstance(instance, part.models.Part):
if category := instance.category:
if category.name[0] != part.name[0]:
self.raise_error({
"name": "Part name and category name must start with the same letter"
})
if isinstance(instance, part.models.PartCategory):
if deltas and 'description' in deltas:
d_new = deltas['description']['new']
d_old = deltas['description']['old']
if len(d_new) < len(d_old):
self.raise_error({
"description": "Description cannot be shortened"
})
```
## Field Validation
In addition to the general purpose model instance validation routine provided above, the following fields support custom validation routines:
### Part Name
By default, part names are not subject to any particular naming conventions or requirements. However if custom validation is required, the `validate_part_name` method can be implemented to ensure that a part name conforms to a required convention.
If the custom method determines that the part name is *objectionable*, it should throw a `ValidationError` which will be handled upstream by parent calling methods.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_name
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Part IPN
Validation of the Part IPN (Internal Part Number) field is exposed to custom plugins via the `validate_part_ipn` method. Any plugins which extend the `ValidationMixin` class can implement this method, and raise a `ValidationError` if the IPN value does not match a required convention.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_ipn
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Part Parameter Values
[Part parameters](../../part/parameter.md) can also have custom validation rules applied, by implementing the `validate_part_parameter` method. A plugin which implements this method should raise a `ValidationError` with an appropriate message if the part parameter value does not match a required convention.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_part_parameter
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Batch Codes
[Batch codes](../../stock/tracking.md#batch-codes) can be generated and/or validated by custom plugins.
#### Validate Batch Code
The `validate_batch_code` method allows plugins to raise an error if a batch code input by the user does not meet a particular pattern.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_batch_code
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Generate Batch Code
The `generate_batch_code` method can be implemented to generate a new batch code, based on a set of provided information.
::: plugin.base.integration.ValidationMixin.ValidationMixin.generate_batch_code
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Serial Numbers
Requirements for serial numbers can vary greatly depending on the application. Rather than attempting to provide a "one size fits all" serial number implementation, InvenTree allows custom serial number schemes to be implemented via plugins.
The default InvenTree [serial numbering system](../../stock/tracking.md#serial-numbers) uses a simple algorithm to validate and increment serial numbers. More complex behaviors can be implemented using the `ValidationMixin` plugin class and the following custom methods:
#### Serial Number Validation
Custom serial number validation can be implemented using the `validate_serial_number` method. A *proposed* serial number is passed to this method, which then has the opportunity to raise a `ValidationError` to indicate that the serial number is not valid.
::: plugin.base.integration.ValidationMixin.ValidationMixin.validate_serial_number
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "Stock Item"
If the `stock_item` argument is provided, then this stock item has already been assigned with the provided serial number. This stock item should be excluded from any subsequent checks for *uniqueness*. The `stock_item` parameter is optional, and may be `None` if the serial number is being validated in a context where no stock item is available.
##### Example
A plugin which requires all serial numbers to be valid hexadecimal values may implement this method as follows:
```python
def validate_serial_number(self, serial: str, part: Part, stock_item: StockItem = None):
"""Validate the supplied serial number
Arguments:
serial: The proposed serial number (string)
part: The Part instance for which this serial number is being validated
stock_item: The StockItem instance for which this serial number is being validated
"""
try:
# Attempt integer conversion
int(serial, 16)
except ValueError:
raise ValidationError("Serial number must be a valid hex value")
```
#### Serial Number Sorting
While InvenTree supports arbitrary text values in the serial number fields, behind the scenes it attempts to "coerce" these values into an integer representation for more efficient sorting.
A custom plugin can implement the `convert_serial_to_int` method to determine how a particular serial number is converted to an integer representation.
::: plugin.base.integration.ValidationMixin.ValidationMixin.convert_serial_to_int
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "Not Required"
If this method is not implemented, or the serial number cannot be converted to an integer, then the sorting algorithm falls back to the text (string) value
#### Serial Number Incrementing
A core component of the InvenTree serial number system is the ability to *increment* serial numbers - to determine the *next* serial number value in a sequence.
For custom serial number schemes, it is important to provide a method to generate the *next* serial number given a current value. The `increment_serial_number` method can be implemented by a plugin to achieve this.
::: plugin.base.integration.ValidationMixin.ValidationMixin.increment_serial_number
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
!!! info "Invalid Increment"
If the provided number cannot be incremented (or an error occurs) the method should return `None`
##### Example
Continuing with the hexadecimal example as above, the method may be implemented as follows:
```python
def increment_serial_number(self, serial: str):
"""Provide the next hexadecimal number in sequence"""
try:
val = int(serial, 16) + 1
val = hex(val).upper()[2:]
except ValueError:
val = None
return val
```
## Sample Plugin
A sample plugin which implements custom validation routines is provided in the InvenTree source code:
::: plugin.samples.integration.validation_sample.SampleValidatorPlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []
+64
View File
@@ -0,0 +1,64 @@
---
title: Item Tags
---
## Tags
Several models in InvenTree can be tagged with arbitrary tags. Tags are useful for grouping items together. This can be used to mark items with a plugin or to group items together for a particular theme. Tags are meant to be used by programs and are not visible to the end user.
Tags are shared between all models that can be tagged.
The following models can be tagged:
- [Parts](../part/index.md)
- [Supplier Parts](../purchasing/supplier.md#supplier-parts)
- [Manufacturer Part](../purchasing/manufacturer.md#manufacturer-parts)
- [Stock Items](../stock/index.md#stock-item)
- [Stock Location](../stock/index.md#stock-location)
## Accessing Tags
### Plugin Access
The `tags` field can be accessed and updated directly from custom plugin code, as follows:
```python
from part.models import Part
# Show tags for a particular Part instance
part = Part.objects.get(pk=100)
print(part.tags)
> {['Tag1', 'Another Tag']}
# Tags can also be accessed via tags.all()
print(part.tags.all())
> {['Tag1', 'Another Tag']}
# Add tag
part.tags.add('Tag 2')
print(part.tags)
> {['Tag1', 'Tag 2', 'Another Tag']}
# Remove tag
part.tags.remove('Tag1')
print(part.tags)
> {['Tag 2', 'Another Tag']}
# Filter by tags
Part.objects.filter(tags__name__in=["Tag1", "Tag 2"]).distinct()
```
### API Access
For models which provide tags, access is also provided via the API. The tags are exposed via the detail endpoint for the models starting from version 111.
Tags can be cached via PATCH or POST requests. The tags are provided as a json formatted list of strings. The tags are note case sensitive and must be unique across the instance - else the existing tag gets assigned. The tags are not sorted and the order is not guaranteed.
```json
{
"tags": '["foo", "bar"]'
}
```
+174
View File
@@ -0,0 +1,174 @@
---
Title: Unit Tests
---
## Unit Tests
For complicated plugins it makes sense to add unit tests the code to ensure
that plugins work correctly and are compatible with future versions too.
You can run these tests as part of your ci against the current stable and
latest tag to get notified when something breaks before it gets released as
part of stable. InvenTree offers a framework for testing. Please refer
to [Unit Tests](../develop/contributing.md) for more information.
### Prerequisites
For plugin testing the following environment variables must be set to True:
| Name | Function | Value |
| --- | --- | --- |
| INVENTREE_PLUGINS_ENABLED | Enables the use of 3rd party plugins | True |
| INVENTREE_PLUGIN_TESTING | Enables enables all plugins no matter of their active state in the db or built-in flag | True |
| INVENTREE_PLUGIN_TESTING_SETUP | Enables the url mixin | True |
### Test program
A file called test_plugin_name.py should be added to the plugin directory. It can have the
following structure:
```
# Basic unit tests for the plugin
from InvenTree.unit_test import InvenTreeTestCase
class TestMyPlugin(InvenTreeTestCase):
def test_my_function(self):
do some work here...
```
The test can be executed using invoke:
```
invoke dev.test -r module.file.class
```
Plugins are usually installed outside of the InventTree directory, e.g. in .local/lib/...
I that case module must be omitted.
```
invoke dev.test -r plugin_directory.test_plugin_name.TestMyPlugin
```
### do some work here... A simple Example
A simple example is shown here. Assume the plugin has a function that converts a price string
that comes from a supplier API to a float value. The price might have the form "1.456,34 €".
It can be different based on country and local settings.
The function in the plugin will convert it to a float 1456.34. It is in the class MySupplier
and has the following structure:
```
class MySupplier():
def reformat_price(self, string_price):
...
return float_price
```
This function needs to be tested. The test can look like this:
```
from .myplugin import MySupplier
def test_reformat_price(self):
self.assertEqual(MySupplier.reformat_price(self, '1.456,34 €'), 1456.34)
self.assertEqual(MySupplier.reformat_price(self, '1,45645 €'), 1.45645)
self.assertEqual(MySupplier.reformat_price(self, '1,56 $'), 1.56)
self.assertEqual(MySupplier.reformat_price(self, ''), 0)
self.assertEqual(MySupplier.reformat_price(self, 'Mumpitz'), 0)
```
The function assertEqual flags an error in case the two arguments are not equal. In equal case
no error is flagged and the test passes. The test function tests five different
input variations. More might be added based on the requirements.
### Involve the database
Now we test a function that uses InvenTree database objects. The function checks if a part
should be updated with latest data from a supplier. Parts that are not purchasable or inactive
should not be updated. The function in the plugin has the following form:
```
class MySupplier():
def should_be_updated(self, my_part):
...
return True/False
```
To test this function, parts are needed in the database. The test framework creates
a dummy database for each run which is empty. Parts for testing need to be added.
This is done in the test function which looks like:
```
from part.models import Part, PartCategory
def test_should_be_updated(self):
test_cat = PartCategory.objects.create(name='test_cat')
active_part = Part.objects.create(
name='Part1',
IPN='IPN1',
category=test_cat,
active=True,
purchaseable=True,
component=True,
virtual=False)
inactive_part = Part.objects.create(
name='Part2',
IPN='IPN2',
category=test_cat,
active=False,
purchaseable=True,
component=True,
virtual=False)
non_purchasable_part = Part.objects.create(
name='Part3',
IPN='IPN3',
category=test_cat,
active=True,
purchaseable=False,
component=True,
virtual=False)
self.assertEqual(MySupplier.should_be_updated(self, active_part, True, 'Active part')
self.assertEqual(MySupplier.should_be_updated(self, inactive_part, False, 'Inactive part')
self.assertEqual(MySupplier.should_be_updated(self, non_purchasable_part, False, 'Non purchasable part')
```
A category and three parts are created. One part is active, one is inactive and one is not
purchasable. The function should_be_updated is tested with all
three parts. The first test should return True, the others False. A message was added to the assert
function for better clarity of test results.
The dummy database is completely separate from the one that you might use for development
and it is deleted after the test. There is no danger for your development database.
In case everything is OK, the result looks like:
```
----------------------------------------------------------------------
Ran 1 tests in 0.809s
OK
Destroying test database for alias 'default'...
```
In case of a problem you will see something like:
```
======================================================================
FAIL: test_should_be_updated (inventree_supplier_sync.test_supplier_sync.TestSyncPlugin)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/michael/.local/lib/python3.10/site-packages/inventree_supplier_sync/test_supplier_sync.py", line 73, in test_should_be_updated
self.assertEqual(SupplierSyncPlugin.should_be_updated(self, non_purchasable_part,), False, 'Non purchasable part')
AssertionError: True != False : Non purchasable part
----------------------------------------------------------------------
Ran 3 tests in 0.679s
FAILED (failures=1)
Destroying test database for alias 'default'...
```
In the AssertionError the message appears that was added to the assertEqual function.