mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Event enum (#8573)
* Add enumeration for stock events * Update function def * Refactor build events * Plugin events * Update order events * Overdue order events * Add documentation * Revert mkdocs.yml * Stringify event name * Enum cleanup - Support python < 3.11 - Custom __str__ * Add unit tests * Fix duplicated code * Update unit tests * Bump query limit * Use proper enums in unit tests
This commit is contained in:
parent
390828d166
commit
dd9a6a8a2d
@ -15,6 +15,151 @@ When a certain (server-side) event occurs, the background worker passes the even
|
||||
{% 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:
|
||||
@ -40,12 +185,3 @@ Overall this function can reduce the workload on the background workers signific
|
||||
show_root_toc_entry: False
|
||||
show_source: True
|
||||
members: []
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
Implementing a response to a particular event requires a working knowledge of the InvenTree code base, especially related to that event being received.
|
||||
|
@ -212,6 +212,7 @@ PLUGIN_TESTING_SETUP = get_setting(
|
||||
) # Load plugins from setup hooks in testing?
|
||||
|
||||
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
||||
PLUGIN_TESTING_EVENTS_ASYNC = False # Flag if events are tested asynchronously
|
||||
|
||||
PLUGIN_RETRY = get_setting(
|
||||
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
|
||||
|
@ -94,6 +94,73 @@ def getNewestMigrationFile(app, exclude_extension=True):
|
||||
return newest_file
|
||||
|
||||
|
||||
def findOffloadedTask(
|
||||
task_name: str,
|
||||
clear_after: bool = False,
|
||||
reverse: bool = False,
|
||||
matching_args=None,
|
||||
matching_kwargs=None,
|
||||
):
|
||||
"""Find an offloaded tasks in the background worker queue.
|
||||
|
||||
Arguments:
|
||||
task_name: The name of the task to search for
|
||||
clear_after: Clear the task queue after searching
|
||||
reverse: Search in reverse order (most recent first)
|
||||
matching_args: List of argument names to match against
|
||||
matching_kwargs: List of keyword argument names to match against
|
||||
"""
|
||||
from django_q.models import OrmQ
|
||||
|
||||
tasks = OrmQ.objects.all()
|
||||
|
||||
if reverse:
|
||||
tasks = tasks.order_by('-pk')
|
||||
|
||||
task = None
|
||||
|
||||
for t in tasks:
|
||||
if t.func() == task_name:
|
||||
found = True
|
||||
|
||||
if matching_args:
|
||||
for arg in matching_args:
|
||||
if arg not in t.args():
|
||||
found = False
|
||||
break
|
||||
|
||||
if matching_kwargs:
|
||||
for kwarg in matching_kwargs:
|
||||
if kwarg not in t.kwargs():
|
||||
found = False
|
||||
break
|
||||
|
||||
if found:
|
||||
task = t
|
||||
break
|
||||
|
||||
if clear_after:
|
||||
OrmQ.objects.all().delete()
|
||||
|
||||
return task
|
||||
|
||||
|
||||
def findOffloadedEvent(
|
||||
event_name: str,
|
||||
clear_after: bool = False,
|
||||
reverse: bool = False,
|
||||
matching_kwargs=None,
|
||||
):
|
||||
"""Find an offloaded event in the background worker queue."""
|
||||
return findOffloadedTask(
|
||||
'plugin.base.event.events.register_event',
|
||||
matching_args=[str(event_name)],
|
||||
matching_kwargs=matching_kwargs,
|
||||
clear_after=clear_after,
|
||||
reverse=reverse,
|
||||
)
|
||||
|
||||
|
||||
class UserMixin:
|
||||
"""Mixin to setup a user and login for tests.
|
||||
|
||||
|
18
src/backend/InvenTree/build/events.py
Normal file
18
src/backend/InvenTree/build/events.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Event definitions and triggers for the build app."""
|
||||
|
||||
from generic.events import BaseEventEnum
|
||||
|
||||
|
||||
class BuildEvents(BaseEventEnum):
|
||||
"""Event enumeration for the Build app."""
|
||||
|
||||
# Build order events
|
||||
HOLD = 'build.hold'
|
||||
ISSUED = 'build.issued'
|
||||
CANCELLED = 'build.cancelled'
|
||||
COMPLETED = 'build.completed'
|
||||
OVERDUE = 'build.overdue_build_order'
|
||||
|
||||
# Build output events
|
||||
OUTPUT_CREATED = 'buildoutput.created'
|
||||
OUTPUT_COMPLETED = 'buildoutput.completed'
|
@ -24,6 +24,7 @@ from rest_framework import serializers
|
||||
from build.status_codes import BuildStatus, BuildStatusGroups
|
||||
from stock.status_codes import StockStatus, StockHistoryCode
|
||||
|
||||
from build.events import BuildEvents
|
||||
from build.filters import annotate_allocated_quantity
|
||||
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||
from generic.states import StateTransitionMixin
|
||||
@ -651,7 +652,7 @@ class Build(
|
||||
raise ValidationError(_("Failed to offload task to complete build allocations"))
|
||||
|
||||
# Register an event
|
||||
trigger_event('build.completed', id=self.pk)
|
||||
trigger_event(BuildEvents.COMPLETED, id=self.pk)
|
||||
|
||||
# Notify users that this build has been completed
|
||||
targets = [
|
||||
@ -718,7 +719,7 @@ class Build(
|
||||
self.status = BuildStatus.PRODUCTION.value
|
||||
self.save()
|
||||
|
||||
trigger_event('build.issued', id=self.pk)
|
||||
trigger_event(BuildEvents.ISSUED, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_build(self):
|
||||
@ -743,7 +744,7 @@ class Build(
|
||||
self.status = BuildStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('build.hold', id=self.pk)
|
||||
trigger_event(BuildEvents.HOLD, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_build(self, user, **kwargs):
|
||||
@ -802,7 +803,7 @@ class Build(
|
||||
content=InvenTreeNotificationBodies.OrderCanceled
|
||||
)
|
||||
|
||||
trigger_event('build.cancelled', id=self.pk)
|
||||
trigger_event(BuildEvents.CANCELLED, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def deallocate_stock(self, build_line=None, output=None):
|
||||
@ -1157,6 +1158,12 @@ class Build(
|
||||
deltas=deltas
|
||||
)
|
||||
|
||||
trigger_event(
|
||||
BuildEvents.OUTPUT_COMPLETED,
|
||||
id=output.pk,
|
||||
build_id=self.pk,
|
||||
)
|
||||
|
||||
# Increase the completed quantity for this build
|
||||
self.completed += output.quantity
|
||||
|
||||
|
@ -21,6 +21,7 @@ import InvenTree.helpers_email
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
import part.models as part_models
|
||||
from build.events import BuildEvents
|
||||
from build.status_codes import BuildStatusGroups
|
||||
from InvenTree.ready import isImportingData
|
||||
from plugin.events import trigger_event
|
||||
@ -272,7 +273,7 @@ def notify_overdue_build_order(bo: build_models.Build):
|
||||
}
|
||||
}
|
||||
|
||||
event_name = 'build.overdue_build_order'
|
||||
event_name = BuildEvents.OVERDUE
|
||||
|
||||
# Send a notification to the appropriate users
|
||||
common.notifications.trigger_notification(
|
||||
|
@ -45,7 +45,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
# Filter query by build status
|
||||
response = self.get(url, {'status': 40}, expected_code=200)
|
||||
response = self.get(url, {'status': BuildStatus.COMPLETE.value}, expected_code=200)
|
||||
|
||||
self.assertEqual(len(response.data), 4)
|
||||
|
||||
@ -221,10 +221,10 @@ class BuildTest(BuildAPITest):
|
||||
{
|
||||
"outputs": [{"output": output.pk} for output in outputs],
|
||||
"location": 1,
|
||||
"status": 50, # Item requires attention
|
||||
"status": StockStatus.ATTENTION.value,
|
||||
},
|
||||
expected_code=201,
|
||||
max_query_count=450, # TODO: Try to optimize this
|
||||
max_query_count=600, # TODO: Try to optimize this
|
||||
)
|
||||
|
||||
self.assertEqual(self.build.incomplete_outputs.count(), 0)
|
||||
|
@ -4,12 +4,15 @@ from datetime import datetime, timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Sum
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from InvenTree import status_codes as status
|
||||
from InvenTree.unit_test import findOffloadedEvent
|
||||
|
||||
import common.models
|
||||
from common.settings import set_global_setting
|
||||
@ -722,6 +725,59 @@ class BuildTest(BuildTestBase):
|
||||
|
||||
self.assertTrue(messages.filter(user__pk=4).exists())
|
||||
|
||||
@override_settings(
|
||||
TESTING_TABLE_EVENTS=True,
|
||||
PLUGIN_TESTING_EVENTS=True,
|
||||
PLUGIN_TESTING_EVENTS_ASYNC=True
|
||||
)
|
||||
def test_events(self):
|
||||
"""Test that build events are triggered correctly."""
|
||||
|
||||
from django_q.models import OrmQ
|
||||
from build.events import BuildEvents
|
||||
|
||||
set_global_setting('ENABLE_PLUGINS_EVENTS', True)
|
||||
|
||||
OrmQ.objects.all().delete()
|
||||
|
||||
# Create a new build
|
||||
build = Build.objects.create(
|
||||
reference='BO-9999',
|
||||
title='Some new build',
|
||||
part=self.assembly,
|
||||
quantity=5,
|
||||
issued_by=get_user_model().objects.get(pk=2),
|
||||
responsible=Owner.create(obj=Group.objects.get(pk=3))
|
||||
)
|
||||
|
||||
# Check that the 'build.created' event was triggered
|
||||
task = findOffloadedEvent(
|
||||
'build_build.created',
|
||||
matching_kwargs=['id', 'model'],
|
||||
reverse=True,
|
||||
clear_after=True,
|
||||
)
|
||||
|
||||
# Assert that the task was found
|
||||
self.assertIsNotNone(task)
|
||||
|
||||
# Check that the Build ID matches
|
||||
self.assertEqual(task.kwargs()['id'], build.pk)
|
||||
|
||||
# Issue the build
|
||||
build.issue_build()
|
||||
|
||||
# Check that the 'build.issued' event was triggered
|
||||
task = findOffloadedEvent(
|
||||
BuildEvents.ISSUED,
|
||||
matching_kwargs=['id'],
|
||||
clear_after=True,
|
||||
)
|
||||
|
||||
self.assertIsNotNone(task)
|
||||
|
||||
set_global_setting('ENABLE_PLUGINS_EVENTS', False)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Unit tests for the metadata field."""
|
||||
# Make sure a BuildItem exists before trying to run this test
|
||||
|
11
src/backend/InvenTree/generic/events.py
Normal file
11
src/backend/InvenTree/generic/events.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Generic event enumerations for InevnTree."""
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class BaseEventEnum(str, enum.Enum):
|
||||
"""Base class for representing a set of 'events'."""
|
||||
|
||||
def __str__(self):
|
||||
"""Return the string representation of the event."""
|
||||
return str(self.value)
|
37
src/backend/InvenTree/order/events.py
Normal file
37
src/backend/InvenTree/order/events.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Event definitions and triggers for the order app."""
|
||||
|
||||
from generic.events import BaseEventEnum
|
||||
|
||||
|
||||
class PurchaseOrderEvents(BaseEventEnum):
|
||||
"""Event enumeration for the PurchaseOrder models."""
|
||||
|
||||
PLACED = 'purchaseorder.placed'
|
||||
COMPLETED = 'purchaseorder.completed'
|
||||
CANCELLED = 'purchaseorder.cancelled'
|
||||
HOLD = 'purchaseorder.hold'
|
||||
|
||||
OVERDUE = 'order.overdue_purchase_order'
|
||||
|
||||
|
||||
class SalesOrderEvents(BaseEventEnum):
|
||||
"""Event enumeration for the SalesOrder models."""
|
||||
|
||||
ISSUED = 'salesorder.issued'
|
||||
HOLD = 'salesorder.onhold'
|
||||
COMPLETED = 'salesorder.completed'
|
||||
CANCELLED = 'salesorder.cancelled'
|
||||
|
||||
OVERDUE = 'order.overdue_sales_order'
|
||||
|
||||
SHIPMENT_COMPLETE = 'salesordershipment.completed'
|
||||
|
||||
|
||||
class ReturnOrderEvents(BaseEventEnum):
|
||||
"""Event enumeration for the Return models."""
|
||||
|
||||
ISSUED = 'returnorder.issued'
|
||||
RECEIVED = 'returnorder.received'
|
||||
COMPLETED = 'returnorder.completed'
|
||||
CANCELLED = 'returnorder.cancelled'
|
||||
HOLD = 'returnorder.hold'
|
@ -45,6 +45,7 @@ from InvenTree.fields import (
|
||||
)
|
||||
from InvenTree.helpers import decimal2string, pui_url
|
||||
from InvenTree.helpers_model import notify_responsible
|
||||
from order.events import PurchaseOrderEvents, ReturnOrderEvents, SalesOrderEvents
|
||||
from order.status_codes import (
|
||||
PurchaseOrderStatus,
|
||||
PurchaseOrderStatusGroups,
|
||||
@ -635,7 +636,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('purchaseorder.placed', id=self.pk)
|
||||
trigger_event(PurchaseOrderEvents.PLACED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been placed
|
||||
notify_responsible(
|
||||
@ -661,7 +662,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
if line.part and line.part.part:
|
||||
line.part.part.schedule_pricing_update(create=True)
|
||||
|
||||
trigger_event('purchaseorder.completed', id=self.pk)
|
||||
trigger_event(PurchaseOrderEvents.COMPLETED, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def issue_order(self):
|
||||
@ -729,7 +730,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
self.status = PurchaseOrderStatus.CANCELLED.value
|
||||
self.save()
|
||||
|
||||
trigger_event('purchaseorder.cancelled', id=self.pk)
|
||||
trigger_event(PurchaseOrderEvents.CANCELLED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been canceled
|
||||
notify_responsible(
|
||||
@ -753,7 +754,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
self.status = PurchaseOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('purchaseorder.hold', id=self.pk)
|
||||
trigger_event(PurchaseOrderEvents.HOLD, id=self.pk)
|
||||
|
||||
# endregion
|
||||
|
||||
@ -1143,7 +1144,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('salesorder.issued', id=self.pk)
|
||||
trigger_event(SalesOrderEvents.ISSUED, id=self.pk)
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
@ -1159,7 +1160,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
self.status = SalesOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('salesorder.onhold', id=self.pk)
|
||||
trigger_event(SalesOrderEvents.HOLD, id=self.pk)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Mark this order as "complete."""
|
||||
@ -1188,7 +1189,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
if line.part:
|
||||
line.part.schedule_pricing_update(create=True)
|
||||
|
||||
trigger_event('salesorder.completed', id=self.pk)
|
||||
trigger_event(SalesOrderEvents.COMPLETED, id=self.pk)
|
||||
|
||||
return True
|
||||
|
||||
@ -1214,7 +1215,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
for allocation in line.allocations.all():
|
||||
allocation.delete()
|
||||
|
||||
trigger_event('salesorder.cancelled', id=self.pk)
|
||||
trigger_event(SalesOrderEvents.CANCELLED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been canceled
|
||||
notify_responsible(
|
||||
@ -1956,7 +1957,7 @@ class SalesOrderShipment(
|
||||
group='sales_order',
|
||||
)
|
||||
|
||||
trigger_event('salesordershipment.completed', id=self.pk)
|
||||
trigger_event(SalesOrderEvents.SHIPMENT_COMPLETE, id=self.pk)
|
||||
|
||||
|
||||
class SalesOrderExtraLine(OrderExtraLine):
|
||||
@ -2281,7 +2282,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
self.status = ReturnOrderStatus.ON_HOLD.value
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.hold', id=self.pk)
|
||||
trigger_event(ReturnOrderEvents.HOLD, id=self.pk)
|
||||
|
||||
@property
|
||||
def can_cancel(self):
|
||||
@ -2294,7 +2295,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
self.status = ReturnOrderStatus.CANCELLED.value
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.cancelled', id=self.pk)
|
||||
trigger_event(ReturnOrderEvents.CANCELLED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been canceled
|
||||
notify_responsible(
|
||||
@ -2311,7 +2312,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
self.complete_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.completed', id=self.pk)
|
||||
trigger_event(ReturnOrderEvents.COMPLETED, id=self.pk)
|
||||
|
||||
def place_order(self):
|
||||
"""Deprecated version of 'issue_order."""
|
||||
@ -2332,7 +2333,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
self.issue_date = InvenTree.helpers.current_date()
|
||||
self.save()
|
||||
|
||||
trigger_event('returnorder.issued', id=self.pk)
|
||||
trigger_event(ReturnOrderEvents.ISSUED, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
@ -2422,7 +2423,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
line.received_date = InvenTree.helpers.current_date()
|
||||
line.save()
|
||||
|
||||
trigger_event('returnorder.received', id=self.pk)
|
||||
trigger_event(ReturnOrderEvents.RECEIVED, id=self.pk)
|
||||
|
||||
# Notify responsible users
|
||||
notify_responsible(
|
||||
|
@ -11,6 +11,7 @@ import common.notifications
|
||||
import InvenTree.helpers_model
|
||||
import order.models
|
||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||
from order.events import PurchaseOrderEvents, SalesOrderEvents
|
||||
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
||||
from plugin.events import trigger_event
|
||||
|
||||
@ -37,7 +38,7 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
'template': {'html': 'email/overdue_purchase_order.html', 'subject': name},
|
||||
}
|
||||
|
||||
event_name = 'order.overdue_purchase_order'
|
||||
event_name = PurchaseOrderEvents.OVERDUE
|
||||
|
||||
# Send a notification to the appropriate users
|
||||
common.notifications.trigger_notification(
|
||||
@ -87,7 +88,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
'template': {'html': 'email/overdue_sales_order.html', 'subject': name},
|
||||
}
|
||||
|
||||
event_name = 'order.overdue_sales_order'
|
||||
event_name = SalesOrderEvents.OVERDUE
|
||||
|
||||
# Send a notification to the appropriate users
|
||||
common.notifications.trigger_notification(
|
||||
|
@ -133,8 +133,8 @@ class PurchaseOrderTest(OrderTest):
|
||||
self.filter({'outstanding': False}, 2)
|
||||
|
||||
# Filter by "status"
|
||||
self.filter({'status': 10}, 3)
|
||||
self.filter({'status': 40}, 1)
|
||||
self.filter({'status': PurchaseOrderStatus.PENDING.value}, 3)
|
||||
self.filter({'status': PurchaseOrderStatus.CANCELLED.value}, 1)
|
||||
|
||||
# Filter by "reference"
|
||||
self.filter({'reference': 'PO-0001'}, 1)
|
||||
@ -1264,8 +1264,8 @@ class SalesOrderTest(OrderTest):
|
||||
self.filter({'outstanding': False}, 2)
|
||||
|
||||
# Filter by status
|
||||
self.filter({'status': 10}, 3) # PENDING
|
||||
self.filter({'status': 20}, 1) # SHIPPED
|
||||
self.filter({'status': SalesOrderStatus.PENDING.value}, 3) # PENDING
|
||||
self.filter({'status': SalesOrderStatus.SHIPPED.value}, 1) # SHIPPED
|
||||
self.filter({'status': 99}, 0) # Invalid
|
||||
|
||||
# Filter by "reference"
|
||||
@ -2229,7 +2229,9 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
self.assertEqual(result['customer'], cmp_id)
|
||||
|
||||
# Filter by status
|
||||
data = self.get(url, {'status': 20}, expected_code=200).data
|
||||
data = self.get(
|
||||
url, {'status': ReturnOrderStatus.IN_PROGRESS.value}, expected_code=200
|
||||
).data
|
||||
|
||||
self.assertEqual(len(data), 2)
|
||||
|
||||
|
7
src/backend/InvenTree/part/events.py
Normal file
7
src/backend/InvenTree/part/events.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""Event definitions and triggers for the part app."""
|
||||
|
||||
from generic.events import BaseEventEnum
|
||||
|
||||
|
||||
class PartEvents(BaseEventEnum):
|
||||
"""Event enumeration for the Part models."""
|
@ -16,16 +16,23 @@ from plugin.registry import registry
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def trigger_event(event, *args, **kwargs):
|
||||
def trigger_event(event: str, *args, **kwargs) -> None:
|
||||
"""Trigger an event with optional arguments.
|
||||
|
||||
This event will be stored in the database,
|
||||
and the worker will respond to it later on.
|
||||
Arguments:
|
||||
event: The event to trigger
|
||||
*args: Additional arguments to pass to the event handler
|
||||
**kwargs: Additional keyword arguments to pass to the event handler
|
||||
|
||||
This event will be stored in the database, and the worker will respond to it later on.
|
||||
"""
|
||||
if not get_global_setting('ENABLE_PLUGINS_EVENTS', False):
|
||||
# Do nothing if plugin events are not enabled
|
||||
return
|
||||
|
||||
# Ensure event name is stringified
|
||||
event = str(event).strip()
|
||||
|
||||
# Make sure the database can be accessed and is not being tested rn
|
||||
if (
|
||||
not canAppAccessDatabase(allow_shell=True)
|
||||
@ -36,9 +43,13 @@ def trigger_event(event, *args, **kwargs):
|
||||
|
||||
logger.debug("Event triggered: '%s'", event)
|
||||
|
||||
# By default, force the event to be processed asynchronously
|
||||
if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS:
|
||||
kwargs['force_async'] = True
|
||||
force_async = kwargs.pop('force_async', True)
|
||||
|
||||
# If we are running in testing mode, we can enable or disable async processing
|
||||
if settings.PLUGIN_TESTING_EVENTS:
|
||||
force_async = settings.PLUGIN_TESTING_EVENTS_ASYNC
|
||||
|
||||
kwargs['force_async'] = force_async
|
||||
|
||||
offload_task(register_event, event, *args, group='plugin', **kwargs)
|
||||
|
||||
@ -179,4 +190,9 @@ def after_delete(sender, instance, **kwargs):
|
||||
if not allow_table_event(table):
|
||||
return
|
||||
|
||||
trigger_event(f'{table}.deleted', model=sender.__name__)
|
||||
instance_id = None
|
||||
|
||||
if instance:
|
||||
instance_id = getattr(instance, 'id', None)
|
||||
|
||||
trigger_event(f'{table}.deleted', model=sender.__name__, id=instance_id)
|
||||
|
@ -1,5 +1,14 @@
|
||||
"""Import helper for events."""
|
||||
|
||||
from generic.events import BaseEventEnum
|
||||
from plugin.base.event.events import process_event, register_event, trigger_event
|
||||
|
||||
__all__ = ['process_event', 'register_event', 'trigger_event']
|
||||
|
||||
class PluginEvents(BaseEventEnum):
|
||||
"""Event enumeration for the Plugin app."""
|
||||
|
||||
PLUGINS_LOADED = 'plugins_loaded'
|
||||
PLUGIN_ACTIVATED = 'plugin_activated'
|
||||
|
||||
|
||||
__all__ = ['PluginEvents', 'process_event', 'register_event', 'trigger_event']
|
||||
|
@ -14,6 +14,7 @@ import common.models
|
||||
import InvenTree.models
|
||||
import plugin.staticfiles
|
||||
from plugin import InvenTreePlugin, registry
|
||||
from plugin.events import PluginEvents, trigger_event
|
||||
|
||||
|
||||
class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||
@ -234,6 +235,8 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||
self.active = active
|
||||
self.save()
|
||||
|
||||
trigger_event(PluginEvents.PLUGIN_ACTIVATED, slug=self.key, active=active)
|
||||
|
||||
if active:
|
||||
offload_task(check_for_migrations)
|
||||
offload_task(
|
||||
|
@ -15,7 +15,7 @@ from collections import OrderedDict
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
@ -104,7 +104,7 @@ class PluginsRegistry:
|
||||
|
||||
return plg
|
||||
|
||||
def get_plugin_config(self, slug: str, name: [str, None] = None):
|
||||
def get_plugin_config(self, slug: str, name: Union[str, None] = None):
|
||||
"""Return the matching PluginConfig instance for a given plugin.
|
||||
|
||||
Args:
|
||||
@ -237,9 +237,9 @@ class PluginsRegistry:
|
||||
|
||||
# Trigger plugins_loaded event
|
||||
if canAppAccessDatabase():
|
||||
from plugin.events import trigger_event
|
||||
from plugin.events import PluginEvents, trigger_event
|
||||
|
||||
trigger_event('plugins_loaded')
|
||||
trigger_event(PluginEvents.PLUGINS_LOADED)
|
||||
|
||||
def _unload_plugins(self, force_reload: bool = False):
|
||||
"""Unload and deactivate all IntegrationPlugins.
|
||||
|
16
src/backend/InvenTree/stock/events.py
Normal file
16
src/backend/InvenTree/stock/events.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Event definitions and triggers for the stock app."""
|
||||
|
||||
from generic.events import BaseEventEnum
|
||||
|
||||
|
||||
class StockEvents(BaseEventEnum):
|
||||
"""Event enumeration for the Stock app."""
|
||||
|
||||
# StockItem events
|
||||
ITEM_ASSIGNED_TO_CUSTOMER = 'stockitem.assignedtocustomer'
|
||||
ITEM_RETURNED_FROM_CUSTOMER = 'stockitem.returnedfromcustomer'
|
||||
ITEM_SPLIT = 'stockitem.split'
|
||||
ITEM_MOVED = 'stockitem.moved'
|
||||
ITEM_COUNTED = 'stockitem.counted'
|
||||
ITEM_QUANTITY_UPDATED = 'stockitem.quantityupdated'
|
||||
ITEM_INSTALLED_INTO_ASSEMBLY = 'stockitem.installed'
|
@ -47,6 +47,7 @@ from InvenTree.status_codes import (
|
||||
)
|
||||
from part import models as PartModels
|
||||
from plugin.events import trigger_event
|
||||
from stock.events import StockEvents
|
||||
from stock.generators import generate_batch_code
|
||||
from users.models import Owner
|
||||
|
||||
@ -1205,7 +1206,7 @@ class StockItem(
|
||||
item.add_tracking_entry(code, user, deltas, notes=notes)
|
||||
|
||||
trigger_event(
|
||||
'stockitem.assignedtocustomer',
|
||||
StockEvents.ITEM_ASSIGNED_TO_CUSTOMER,
|
||||
id=self.id,
|
||||
customer=customer.id if customer else None,
|
||||
)
|
||||
@ -1241,12 +1242,15 @@ class StockItem(
|
||||
self.belongs_to = None
|
||||
self.sales_order = None
|
||||
self.location = location
|
||||
self.clearAllocations()
|
||||
|
||||
if status := kwargs.get('status'):
|
||||
self.status = status
|
||||
tracking_info['status'] = status
|
||||
|
||||
self.save()
|
||||
|
||||
self.clearAllocations()
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.RETURNED_FROM_CUSTOMER,
|
||||
user,
|
||||
@ -1255,7 +1259,7 @@ class StockItem(
|
||||
location=location,
|
||||
)
|
||||
|
||||
trigger_event('stockitem.returnedfromcustomer', id=self.id)
|
||||
trigger_event(StockEvents.ITEM_RETURNED_FROM_CUSTOMER, id=self.id)
|
||||
|
||||
"""If new location is the same as the parent location, merge this stock back in the parent"""
|
||||
if self.parent and self.location == self.parent.location:
|
||||
@ -1414,7 +1418,10 @@ class StockItem(
|
||||
|
||||
# Assign the other stock item into this one
|
||||
stock_item.belongs_to = self
|
||||
stock_item.consumed_by = build
|
||||
|
||||
if build is not None:
|
||||
stock_item.consumed_by = build
|
||||
|
||||
stock_item.location = None
|
||||
stock_item.save(add_note=False)
|
||||
|
||||
@ -1436,6 +1443,12 @@ class StockItem(
|
||||
deltas={'stockitem': stock_item.pk},
|
||||
)
|
||||
|
||||
trigger_event(
|
||||
StockEvents.ITEM_INSTALLED_INTO_ASSEMBLY,
|
||||
id=stock_item.pk,
|
||||
assembly_id=self.pk,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def uninstall_into_location(self, location, user, notes):
|
||||
"""Uninstall this stock item from another item, into a location.
|
||||
@ -2042,7 +2055,7 @@ class StockItem(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
trigger_event('stockitem.split', id=new_stock.id, parent=self.id)
|
||||
trigger_event(StockEvents.ITEM_SPLIT, id=new_stock.id, parent=self.id)
|
||||
|
||||
# Return a copy of the "new" stock item
|
||||
return new_stock
|
||||
@ -2129,7 +2142,7 @@ class StockItem(
|
||||
|
||||
# Trigger event for the plugin system
|
||||
trigger_event(
|
||||
'stockitem.moved',
|
||||
StockEvents.ITEM_MOVED,
|
||||
id=self.id,
|
||||
old_location=current_location.id if current_location else None,
|
||||
new_location=location.id if location else None,
|
||||
@ -2167,6 +2180,11 @@ class StockItem(
|
||||
return False
|
||||
|
||||
self.save()
|
||||
|
||||
trigger_event(
|
||||
StockEvents.ITEM_QUANTITY_UPDATED, id=self.id, quantity=float(self.quantity)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@transaction.atomic
|
||||
@ -2210,6 +2228,13 @@ class StockItem(
|
||||
deltas=tracking_info,
|
||||
)
|
||||
|
||||
trigger_event(
|
||||
StockEvents.ITEM_COUNTED,
|
||||
'stockitem.counted',
|
||||
id=self.id,
|
||||
quantity=float(self.quantity),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
from users.models import ApiToken
|
||||
import users.models
|
||||
|
||||
|
||||
class ApiTokenAuthentication(TokenAuthentication):
|
||||
@ -18,7 +18,7 @@ class ApiTokenAuthentication(TokenAuthentication):
|
||||
- Tokens can expire
|
||||
"""
|
||||
|
||||
model = ApiToken
|
||||
model = users.models.ApiToken
|
||||
|
||||
def authenticate_credentials(self, key):
|
||||
"""Adds additional checks to the default token authentication method."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user