mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 12:05:53 +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:
@ -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."""
|
||||
|
Reference in New Issue
Block a user