2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +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:
Oliver 2024-11-29 12:05:05 +11:00 committed by GitHub
parent 390828d166
commit dd9a6a8a2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 472 additions and 58 deletions

View File

@ -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.

View File

@ -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

View File

@ -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.

View 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'

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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

View 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)

View 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'

View File

@ -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(

View File

@ -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(

View File

@ -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)

View 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."""

View File

@ -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)

View File

@ -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']

View File

@ -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(

View File

@ -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.

View 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'

View File

@ -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

View File

@ -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."""