mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 10:18:42 +00:00
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:
@@ -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: []
|
||||
@@ -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: []
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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: []
|
||||
@@ -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") }}
|
||||
@@ -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: []
|
||||
@@ -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.
|
||||
@@ -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: []
|
||||
@@ -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.
|
||||
@@ -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: []
|
||||
@@ -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: []
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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() {
|
||||
...
|
||||
});;
|
||||
```
|
||||
@@ -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: []
|
||||
Reference in New Issue
Block a user