diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index 6f34847529df..75c4e4b4f072 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -306,47 +306,6 @@ def post(self, request, *args, **kwargs):
return Response(results)
-class StatusView(APIView):
- """Generic API endpoint for discovering information on 'status codes' for a particular model.
-
- This class should be implemented as a subclass for each type of status.
- For example, the API endpoint /stock/status/ will have information about
- all available 'StockStatus' codes
- """
-
- permission_classes = [
- permissions.IsAuthenticated,
- ]
-
- # Override status_class for implementing subclass
- MODEL_REF = 'statusmodel'
-
- def get_status_model(self, *args, **kwargs):
- """Return the StatusCode moedl based on extra parameters passed to the view"""
-
- status_model = self.kwargs.get(self.MODEL_REF, None)
-
- if status_model is None:
- raise ValidationError(f"StatusView view called without '{self.MODEL_REF}' parameter")
-
- return status_model
-
- def get(self, request, *args, **kwargs):
- """Perform a GET request to learn information about status codes"""
-
- status_class = self.get_status_model()
-
- if not status_class:
- raise NotImplementedError("status_class not defined for this endpoint")
-
- data = {
- 'class': status_class.__name__,
- 'values': status_class.dict(),
- }
-
- return Response(data)
-
-
class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model"""
diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py
index 380389501061..4518d2b9518d 100644
--- a/InvenTree/InvenTree/context.py
+++ b/InvenTree/InvenTree/context.py
@@ -4,10 +4,8 @@
import InvenTree.email
import InvenTree.status
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
- ReturnOrderLineStatus, ReturnOrderStatus,
- SalesOrderStatus, StockHistoryCode,
- StockStatus)
+from generic.states import StatusCode
+from InvenTree.helpers import inheritors
from users.models import RuleSet, check_user_role
@@ -57,17 +55,7 @@ def status_codes(request):
return {}
request._inventree_status_codes = True
-
- return {
- # Expose the StatusCode classes to the templates
- 'ReturnOrderStatus': ReturnOrderStatus,
- 'ReturnOrderLineStatus': ReturnOrderLineStatus,
- 'SalesOrderStatus': SalesOrderStatus,
- 'PurchaseOrderStatus': PurchaseOrderStatus,
- 'BuildStatus': BuildStatus,
- 'StockStatus': StockStatus,
- 'StockHistoryCode': StockHistoryCode,
- }
+ return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
def user_roles(request):
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 32fb5b4689d4..d5faf1fbbeda 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -196,6 +196,7 @@
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'plugin.apps.PluginAppConfig',
+ 'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py
index 124100a5dc54..956773a9e679 100644
--- a/InvenTree/InvenTree/status_codes.py
+++ b/InvenTree/InvenTree/status_codes.py
@@ -2,377 +2,161 @@
from django.utils.translation import gettext_lazy as _
-
-class StatusCode:
- """Base class for representing a set of StatusCodes.
-
- This is used to map a set of integer values to text.
- """
-
- colors = {}
-
- @classmethod
- def render(cls, key, large=False):
- """Render the value as a HTML label."""
- # If the key cannot be found, pass it back
- if key not in cls.options.keys():
- return key
-
- value = cls.options.get(key, key)
- color = cls.colors.get(key, 'secondary')
-
- span_class = f'badge rounded-pill bg-{color}'
-
- return "{value}".format(
- cl=span_class,
- value=value
- )
-
- @classmethod
- def list(cls):
- """Return the StatusCode options as a list of mapped key / value items."""
- return list(cls.dict().values())
-
- @classmethod
- def text(cls, key):
- """Text for supplied status code."""
- return cls.options.get(key, None)
-
- @classmethod
- def items(cls):
- """All status code items."""
- return cls.options.items()
-
- @classmethod
- def keys(cls):
- """All status code keys."""
- return cls.options.keys()
-
- @classmethod
- def labels(cls):
- """All status code labels."""
- return cls.options.values()
-
- @classmethod
- def names(cls):
- """Return a map of all 'names' of status codes in this class
-
- Will return a dict object, with the attribute name indexed to the integer value.
-
- e.g.
- {
- 'PENDING': 10,
- 'IN_PROGRESS': 20,
- }
- """
- keys = cls.keys()
- status_names = {}
-
- for d in dir(cls):
- if d.startswith('_'):
- continue
- if d != d.upper():
- continue
-
- value = getattr(cls, d, None)
-
- if value is None:
- continue
- if callable(value):
- continue
- if type(value) != int:
- continue
- if value not in keys:
- continue
-
- status_names[d] = value
-
- return status_names
-
- @classmethod
- def dict(cls):
- """Return a dict representation containing all required information"""
- values = {}
-
- for name, value, in cls.names().items():
- entry = {
- 'key': value,
- 'name': name,
- 'label': cls.label(value),
- }
-
- if hasattr(cls, 'colors'):
- if color := cls.colors.get(value, None):
- entry['color'] = color
-
- values[name] = entry
-
- return values
-
- @classmethod
- def label(cls, value):
- """Return the status code label associated with the provided value."""
- return cls.options.get(value, value)
-
- @classmethod
- def value(cls, label):
- """Return the value associated with the provided label."""
- for k in cls.options.keys():
- if cls.options[k].lower() == label.lower():
- return k
-
- raise ValueError("Label not found")
+from generic.states import StatusCode
class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder."""
# Order status codes
- PENDING = 10 # Order is pending (not yet placed)
- PLACED = 20 # Order has been placed with supplier
- COMPLETE = 30 # Order has been completed
- CANCELLED = 40 # Order was cancelled
- LOST = 50 # Order was lost
- RETURNED = 60 # Order was returned
-
- options = {
- PENDING: _("Pending"),
- PLACED: _("Placed"),
- COMPLETE: _("Complete"),
- CANCELLED: _("Cancelled"),
- LOST: _("Lost"),
- RETURNED: _("Returned"),
- }
-
- colors = {
- PENDING: 'secondary',
- PLACED: 'primary',
- COMPLETE: 'success',
- CANCELLED: 'danger',
- LOST: 'warning',
- RETURNED: 'warning',
- }
+ PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed)
+ PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier
+ COMPLETE = 30, _("Complete"), 'success' # Order has been completed
+ CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled
+ LOST = 50, _("Lost"), 'warning' # Order was lost
+ RETURNED = 60, _("Returned"), 'warning' # Order was returned
+
+
+class PurchaseOrderStatusGroups:
+ """Groups for PurchaseOrderStatus codes."""
# Open orders
OPEN = [
- PENDING,
- PLACED,
+ PurchaseOrderStatus.PENDING.value,
+ PurchaseOrderStatus.PLACED.value,
]
# Failed orders
FAILED = [
- CANCELLED,
- LOST,
- RETURNED
+ PurchaseOrderStatus.CANCELLED.value,
+ PurchaseOrderStatus.LOST.value,
+ PurchaseOrderStatus.RETURNED.value
]
class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder."""
- PENDING = 10 # Order is pending
- IN_PROGRESS = 15 # Order has been issued, and is in progress
- SHIPPED = 20 # Order has been shipped to customer
- CANCELLED = 40 # Order has been cancelled
- LOST = 50 # Order was lost
- RETURNED = 60 # Order was returned
-
- options = {
- PENDING: _("Pending"),
- IN_PROGRESS: _("In Progress"),
- SHIPPED: _("Shipped"),
- CANCELLED: _("Cancelled"),
- LOST: _("Lost"),
- RETURNED: _("Returned"),
- }
-
- colors = {
- PENDING: 'secondary',
- IN_PROGRESS: 'primary',
- SHIPPED: 'success',
- CANCELLED: 'danger',
- LOST: 'warning',
- RETURNED: 'warning',
- }
+ PENDING = 10, _("Pending"), 'secondary' # Order is pending
+ IN_PROGRESS = 15, _("In Progress"), 'primary' # Order has been issued, and is in progress
+ SHIPPED = 20, _("Shipped"), 'success' # Order has been shipped to customer
+ CANCELLED = 40, _("Cancelled"), 'danger' # Order has been cancelled
+ LOST = 50, _("Lost"), 'warning' # Order was lost
+ RETURNED = 60, _("Returned"), 'warning' # Order was returned
+
+
+class SalesOrderStatusGroups:
+ """Groups for SalesOrderStatus codes."""
# Open orders
OPEN = [
- PENDING,
- IN_PROGRESS,
+ SalesOrderStatus.PENDING.value,
+ SalesOrderStatus.IN_PROGRESS.value,
]
# Completed orders
COMPLETE = [
- SHIPPED,
+ SalesOrderStatus.SHIPPED.value,
]
class StockStatus(StatusCode):
"""Status codes for Stock."""
- OK = 10 # Item is OK
- ATTENTION = 50 # Item requires attention
- DAMAGED = 55 # Item is damaged
- DESTROYED = 60 # Item is destroyed
- REJECTED = 65 # Item is rejected
- LOST = 70 # Item has been lost
- QUARANTINED = 75 # Item has been quarantined and is unavailable
- RETURNED = 85 # Item has been returned from a customer
-
- options = {
- OK: _("OK"),
- ATTENTION: _("Attention needed"),
- DAMAGED: _("Damaged"),
- DESTROYED: _("Destroyed"),
- LOST: _("Lost"),
- REJECTED: _("Rejected"),
- QUARANTINED: _("Quarantined"),
- }
-
- colors = {
- OK: 'success',
- ATTENTION: 'warning',
- DAMAGED: 'warning',
- DESTROYED: 'danger',
- LOST: 'dark',
- REJECTED: 'danger',
- QUARANTINED: 'info'
- }
+ OK = 10, _("OK"), 'success' # Item is OK
+ ATTENTION = 50, _("Attention needed"), 'warning' # Item requires attention
+ DAMAGED = 55, _("Damaged"), 'warning' # Item is damaged
+ DESTROYED = 60, _("Destroyed"), 'danger' # Item is destroyed
+ REJECTED = 65, _("Rejected"), 'danger' # Item is rejected
+ LOST = 70, _("Lost"), 'dark' # Item has been lost
+ QUARANTINED = 75, _("Quarantined"), 'info' # Item has been quarantined and is unavailable
+ RETURNED = 85, _("Returned"), 'warning' # Item has been returned from a customer
+
+
+class StockStatusGroups:
+ """Groups for StockStatus codes."""
# The following codes correspond to parts that are 'available' or 'in stock'
AVAILABLE_CODES = [
- OK,
- ATTENTION,
- DAMAGED,
- RETURNED,
+ StockStatus.OK.value,
+ StockStatus.ATTENTION.value,
+ StockStatus.DAMAGED.value,
+ StockStatus.RETURNED.value,
]
class StockHistoryCode(StatusCode):
"""Status codes for StockHistory."""
- LEGACY = 0
+ LEGACY = 0, _('Legacy stock tracking entry')
- CREATED = 1
+ CREATED = 1, _('Stock item created')
# Manual editing operations
- EDITED = 5
- ASSIGNED_SERIAL = 6
+ EDITED = 5, _('Edited stock item')
+ ASSIGNED_SERIAL = 6, _('Assigned serial number')
# Manual stock operations
- STOCK_COUNT = 10
- STOCK_ADD = 11
- STOCK_REMOVE = 12
+ STOCK_COUNT = 10, _('Stock counted')
+ STOCK_ADD = 11, _('Stock manually added')
+ STOCK_REMOVE = 12, _('Stock manually removed')
# Location operations
- STOCK_MOVE = 20
- STOCK_UPDATE = 25
+ STOCK_MOVE = 20, _('Location changed')
+ STOCK_UPDATE = 25, _('Stock updated')
# Installation operations
- INSTALLED_INTO_ASSEMBLY = 30
- REMOVED_FROM_ASSEMBLY = 31
+ INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
+ REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
- INSTALLED_CHILD_ITEM = 35
- REMOVED_CHILD_ITEM = 36
+ INSTALLED_CHILD_ITEM = 35, _('Installed component item')
+ REMOVED_CHILD_ITEM = 36, _('Removed component item')
# Stock splitting operations
- SPLIT_FROM_PARENT = 40
- SPLIT_CHILD_ITEM = 42
+ SPLIT_FROM_PARENT = 40, _('Split from parent item')
+ SPLIT_CHILD_ITEM = 42, _('Split child item')
# Stock merging operations
- MERGED_STOCK_ITEMS = 45
+ MERGED_STOCK_ITEMS = 45, _('Merged stock items')
# Convert stock item to variant
- CONVERTED_TO_VARIANT = 48
+ CONVERTED_TO_VARIANT = 48, _('Converted to variant')
# Build order codes
- BUILD_OUTPUT_CREATED = 50
- BUILD_OUTPUT_COMPLETED = 55
- BUILD_OUTPUT_REJECTED = 56
- BUILD_CONSUMED = 57
+ BUILD_OUTPUT_CREATED = 50, _('Build order output created')
+ BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
+ BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
+ BUILD_CONSUMED = 57, _('Consumed by build order')
# Sales order codes
- SHIPPED_AGAINST_SALES_ORDER = 60
+ SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order")
# Purchase order codes
- RECEIVED_AGAINST_PURCHASE_ORDER = 70
+ RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
# Return order codes
- RETURNED_AGAINST_RETURN_ORDER = 80
+ RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
# Customer actions
- SENT_TO_CUSTOMER = 100
- RETURNED_FROM_CUSTOMER = 105
-
- options = {
- LEGACY: _('Legacy stock tracking entry'),
-
- CREATED: _('Stock item created'),
-
- EDITED: _('Edited stock item'),
- ASSIGNED_SERIAL: _('Assigned serial number'),
-
- STOCK_COUNT: _('Stock counted'),
- STOCK_ADD: _('Stock manually added'),
- STOCK_REMOVE: _('Stock manually removed'),
-
- STOCK_MOVE: _('Location changed'),
- STOCK_UPDATE: _('Stock updated'),
-
- INSTALLED_INTO_ASSEMBLY: _('Installed into assembly'),
- REMOVED_FROM_ASSEMBLY: _('Removed from assembly'),
-
- INSTALLED_CHILD_ITEM: _('Installed component item'),
- REMOVED_CHILD_ITEM: _('Removed component item'),
-
- SPLIT_FROM_PARENT: _('Split from parent item'),
- SPLIT_CHILD_ITEM: _('Split child item'),
-
- MERGED_STOCK_ITEMS: _('Merged stock items'),
-
- CONVERTED_TO_VARIANT: _('Converted to variant'),
-
- SENT_TO_CUSTOMER: _('Sent to customer'),
- RETURNED_FROM_CUSTOMER: _('Returned from customer'),
-
- BUILD_OUTPUT_CREATED: _('Build order output created'),
- BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
- BUILD_OUTPUT_REJECTED: _('Build order output rejected'),
- BUILD_CONSUMED: _('Consumed by build order'),
-
- SHIPPED_AGAINST_SALES_ORDER: _("Shipped against Sales Order"),
-
- RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against Purchase Order'),
-
- RETURNED_AGAINST_RETURN_ORDER: _('Returned against Return Order'),
- }
+ SENT_TO_CUSTOMER = 100, _('Sent to customer')
+ RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
class BuildStatus(StatusCode):
"""Build status codes."""
- PENDING = 10 # Build is pending / active
- PRODUCTION = 20 # BuildOrder is in production
- CANCELLED = 30 # Build was cancelled
- COMPLETE = 40 # Build is complete
-
- options = {
- PENDING: _("Pending"),
- PRODUCTION: _("Production"),
- CANCELLED: _("Cancelled"),
- COMPLETE: _("Complete"),
- }
-
- colors = {
- PENDING: 'secondary',
- PRODUCTION: 'primary',
- COMPLETE: 'success',
- CANCELLED: 'danger',
- }
+ PENDING = 10, _("Pending"), 'secondary' # Build is pending / active
+ PRODUCTION = 20, _("Production"), 'primary' # BuildOrder is in production
+ CANCELLED = 30, _("Cancelled"), 'danger' # Build was cancelled
+ COMPLETE = 40, _("Complete"), 'success' # Build is complete
+
+
+class BuildStatusGroups:
+ """Groups for BuildStatus codes."""
ACTIVE_CODES = [
- PENDING,
- PRODUCTION,
+ BuildStatus.PENDING.value,
+ BuildStatus.PRODUCTION.value,
]
@@ -380,68 +164,40 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder"""
# Order is pending, waiting for receipt of items
- PENDING = 10
+ PENDING = 10, _("Pending"), 'secondary'
# Items have been received, and are being inspected
- IN_PROGRESS = 20
+ IN_PROGRESS = 20, _("In Progress"), 'primary'
- COMPLETE = 30
- CANCELLED = 40
+ COMPLETE = 30, _("Complete"), 'success'
+ CANCELLED = 40, _("Cancelled"), 'danger'
- OPEN = [
- PENDING,
- IN_PROGRESS,
- ]
- options = {
- PENDING: _("Pending"),
- IN_PROGRESS: _("In Progress"),
- COMPLETE: _("Complete"),
- CANCELLED: _("Cancelled"),
- }
+class ReturnOrderStatusGroups:
+ """Groups for ReturnOrderStatus codes."""
- colors = {
- PENDING: 'secondary',
- IN_PROGRESS: 'primary',
- COMPLETE: 'success',
- CANCELLED: 'danger',
- }
+ OPEN = [
+ ReturnOrderStatus.PENDING.value,
+ ReturnOrderStatus.IN_PROGRESS.value,
+ ]
class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem"""
- PENDING = 10
+ PENDING = 10, _("Pending"), 'secondary'
# Item is to be returned to customer, no other action
- RETURN = 20
+ RETURN = 20, _("Return"), 'success'
# Item is to be repaired, and returned to customer
- REPAIR = 30
+ REPAIR = 30, _("Repair"), 'primary'
# Item is to be replaced (new item shipped)
- REPLACE = 40
+ REPLACE = 40, _("Replace"), 'warning'
# Item is to be refunded (cannot be repaired)
- REFUND = 50
+ REFUND = 50, _("Refund"), 'info'
# Item is rejected
- REJECT = 60
-
- options = {
- PENDING: _('Pending'),
- RETURN: _('Return'),
- REPAIR: _('Repair'),
- REFUND: _('Refund'),
- REPLACE: _('Replace'),
- REJECT: _('Reject')
- }
-
- colors = {
- PENDING: 'secondary',
- RETURN: 'success',
- REPAIR: 'primary',
- REFUND: 'info',
- REPLACE: 'warning',
- REJECT: 'danger',
- }
+ REJECT = 60, _("Reject"), 'danger'
diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index b4d5853b41a9..e0f9a4f8bbee 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -9,9 +9,10 @@
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
-from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView, StatusView
+from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
+from generic.states import StatusView
from InvenTree.helpers import str2bool, isNull, DownloadFile
-from InvenTree.status_codes import BuildStatus
+from InvenTree.status_codes import BuildStatus, BuildStatusGroups
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import build.admin
@@ -41,9 +42,9 @@ class Meta:
def filter_active(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are active."""
if str2bool(value):
- return queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
+ return queryset.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
else:
- return queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
+ return queryset.exclude(status__in=BuildStatusGroups.ACTIVE_CODES)
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index af2e3cf63011..726d99187d48 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -21,7 +21,7 @@
from rest_framework import serializers
-from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
+from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode, BuildStatusGroups
from build.validators import generate_next_build_reference, validate_build_order_reference
@@ -69,7 +69,7 @@ class Meta:
verbose_name = _("Build Order")
verbose_name_plural = _("Build Orders")
- OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
+ OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
# Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
@@ -129,10 +129,10 @@ def filterByDate(queryset, min_date, max_date):
return queryset
# Order was completed within the specified range
- completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
+ completed = Q(status=BuildStatus.COMPLETE.value) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
# Order target date falls within specified range
- pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
+ pending = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
# TODO - Construct a queryset for "overdue" orders
@@ -231,7 +231,7 @@ def get_absolute_url(self):
status = models.PositiveIntegerField(
verbose_name=_('Build Status'),
- default=BuildStatus.PENDING,
+ default=BuildStatus.PENDING.value,
choices=BuildStatus.items(),
validators=[MinValueValidator(0)],
help_text=_('Build status code')
@@ -331,7 +331,7 @@ def is_overdue(self):
@property
def active(self):
"""Return True if this build is active."""
- return self.status in BuildStatus.ACTIVE_CODES
+ return self.status in BuildStatusGroups.ACTIVE_CODES
@property
def bom_items(self):
@@ -503,7 +503,7 @@ def complete_build(self, user):
self.completion_date = datetime.now().date()
self.completed_by = user
- self.status = BuildStatus.COMPLETE
+ self.status = BuildStatus.COMPLETE.value
self.save()
# Remove untracked allocated stock
@@ -585,7 +585,7 @@ def cancel_build(self, user, **kwargs):
self.completion_date = datetime.now().date()
self.completed_by = user
- self.status = BuildStatus.CANCELLED
+ self.status = BuildStatus.CANCELLED.value
self.save()
trigger_event('build.cancelled', id=self.pk)
@@ -729,7 +729,7 @@ def _add_tracking_entry(output, user):
_add_tracking_entry(output, user)
if self.status == BuildStatus.PENDING:
- self.status = BuildStatus.PRODUCTION
+ self.status = BuildStatus.PRODUCTION.value
self.save()
@transaction.atomic
@@ -831,7 +831,7 @@ def scrap_build_output(self, output, quantity, location, **kwargs):
# Update build output item
output.is_building = False
- output.status = StockStatus.REJECTED
+ output.status = StockStatus.REJECTED.value
output.location = location
output.save(add_note=False)
@@ -851,7 +851,7 @@ def scrap_build_output(self, output, quantity, location, **kwargs):
notes=notes,
deltas={
'location': location.pk,
- 'status': StockStatus.REJECTED,
+ 'status': StockStatus.REJECTED.value,
'buildorder': self.pk,
}
)
@@ -865,7 +865,7 @@ def complete_build_output(self, output, user, **kwargs):
"""
# Select the location for the build output
location = kwargs.get('location', self.destination)
- status = kwargs.get('status', StockStatus.OK)
+ status = kwargs.get('status', StockStatus.OK.value)
notes = kwargs.get('notes', '')
# List the allocated BuildItem objects for the given output
@@ -1187,7 +1187,7 @@ def is_active(self):
- PENDING
- HOLDING
"""
- return self.status in BuildStatus.ACTIVE_CODES
+ return self.status in BuildStatusGroups.ACTIVE_CODES
@property
def is_complete(self):
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 6ee194fcb8b0..193b9f621722 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -490,8 +490,8 @@ class Meta:
)
status = serializers.ChoiceField(
- choices=list(StockStatus.items()),
- default=StockStatus.OK,
+ choices=StockStatus.items(),
+ default=StockStatus.OK.value,
label=_("Status"),
)
diff --git a/InvenTree/build/tasks.py b/InvenTree/build/tasks.py
index 26ee2f1cda7b..6d63720e9685 100644
--- a/InvenTree/build/tasks.py
+++ b/InvenTree/build/tasks.py
@@ -15,7 +15,7 @@
import InvenTree.email
import InvenTree.helpers_model
import InvenTree.tasks
-from InvenTree.status_codes import BuildStatus
+from InvenTree.status_codes import BuildStatusGroups
from InvenTree.ready import isImportingData
import part.models as part_models
@@ -158,7 +158,7 @@ def check_overdue_build_orders():
overdue_orders = build.models.Build.objects.filter(
target_date=yesterday,
- status__in=BuildStatus.ACTIVE_CODES
+ status__in=BuildStatusGroups.ACTIVE_CODES
)
for bo in overdue_orders:
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index b0aed7ba38bd..57c5c2347034 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -2,7 +2,7 @@
{% load static %}
{% load i18n %}
-{% load status_codes %}
+{% load generic %}
{% load inventree_extras %}
{% block page_title %}
@@ -150,7 +150,7 @@
|
{% trans "Status" %} |
- {% build_status_label build.status %}
+ {% status_label 'build' build.status %}
|
{% if build.target_date %}
@@ -217,7 +217,7 @@
{% block page_data %}
- {% build_status_label build.status large=True %}
+ {% status_label 'build' build.status large=True %}
{% if build.is_overdue %}
{% trans "Overdue" %}
{% endif %}
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index 95ae46c8eb0c..ea870d8c4433 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -2,7 +2,7 @@
{% load static %}
{% load i18n %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% block sidebar %}
{% include "build/sidebar.html" %}
@@ -60,7 +60,7 @@ {% trans "Build Details" %}
|
{% trans "Status" %} |
- {% build_status_label build.status %} |
+ {% status_label 'build' build.status %} |
|
diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py
index 98b59ce8bd96..b998ad3cae37 100644
--- a/InvenTree/build/test_api.py
+++ b/InvenTree/build/test_api.py
@@ -298,7 +298,7 @@ def test_delete(self):
expected_code=400,
)
- bo.status = BuildStatus.CANCELLED
+ bo.status = BuildStatus.CANCELLED.value
bo.save()
# Now, we should be able to delete
@@ -843,7 +843,7 @@ def test_get_all_builds(self):
builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1)
- builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
+ builds = self.get(self.url, data={'status': BuildStatus.COMPLETE.value})
self.assertEqual(len(builds.data), 4)
builds = self.get(self.url, data={'overdue': False})
@@ -863,7 +863,7 @@ def test_overdue(self):
reference="BO-0006",
quantity=10,
title='Just some thing',
- status=BuildStatus.PRODUCTION,
+ status=BuildStatus.PRODUCTION.value,
target_date=in_the_past
)
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index ad40a4125517..c41fa39a6f92 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -30,7 +30,7 @@
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, MetadataMixin)
-from InvenTree.status_codes import PurchaseOrderStatus
+from InvenTree.status_codes import PurchaseOrderStatusGroups
def rename_company_image(instance, filename):
@@ -697,7 +697,7 @@ def add_price_break(self, quantity, price) -> None:
def open_orders(self):
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
- return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN)
+ return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
def on_order(self):
"""Return the total quantity of items currently on order.
diff --git a/InvenTree/generic/__init__.py b/InvenTree/generic/__init__.py
new file mode 100644
index 000000000000..188874354b62
--- /dev/null
+++ b/InvenTree/generic/__init__.py
@@ -0,0 +1,4 @@
+"""The generic module provides high-level functionality that is used in multiple places.
+
+The generic module is split into sub-modules. Each sub-module provides a specific set of functionality. Each sub-module should be 100% tested within the sub-module.
+"""
diff --git a/InvenTree/generic/states/__init__.py b/InvenTree/generic/states/__init__.py
new file mode 100644
index 000000000000..5137b7c3f460
--- /dev/null
+++ b/InvenTree/generic/states/__init__.py
@@ -0,0 +1,15 @@
+"""States are used to track the logical state of an object.
+
+The logic value of a state is stored in the database as an integer. The logic value is used for business logic and should not be easily changed therefore.
+There is a rendered state for each state value. The rendered state is used for display purposes and can be changed easily.
+
+States can be extended with custom options for each InvenTree instance - those options are stored in the database and need to link back to state values.
+"""
+
+from .api import StatusView
+from .states import StatusCode
+
+__all__ = [
+ StatusView,
+ StatusCode,
+]
diff --git a/InvenTree/generic/states/api.py b/InvenTree/generic/states/api.py
new file mode 100644
index 000000000000..70e6256232cc
--- /dev/null
+++ b/InvenTree/generic/states/api.py
@@ -0,0 +1,54 @@
+"""Generic implementation of status api functions for InvenTree models."""
+
+import inspect
+
+from rest_framework import permissions
+from rest_framework.response import Response
+from rest_framework.serializers import ValidationError
+from rest_framework.views import APIView
+
+from .states import StatusCode
+
+
+class StatusView(APIView):
+ """Generic API endpoint for discovering information on 'status codes' for a particular model.
+
+ This class should be implemented as a subclass for each type of status.
+ For example, the API endpoint /stock/status/ will have information about
+ all available 'StockStatus' codes
+ """
+
+ permission_classes = [
+ permissions.IsAuthenticated,
+ ]
+
+ # Override status_class for implementing subclass
+ MODEL_REF = 'statusmodel'
+
+ def get_status_model(self, *args, **kwargs):
+ """Return the StatusCode moedl based on extra parameters passed to the view"""
+
+ status_model = self.kwargs.get(self.MODEL_REF, None)
+
+ if status_model is None:
+ raise ValidationError(f"StatusView view called without '{self.MODEL_REF}' parameter")
+
+ return status_model
+
+ def get(self, request, *args, **kwargs):
+ """Perform a GET request to learn information about status codes"""
+
+ status_class = self.get_status_model()
+
+ if not inspect.isclass(status_class):
+ raise NotImplementedError("`status_class` not a class")
+
+ if not issubclass(status_class, StatusCode):
+ raise NotImplementedError("`status_class` not a valid StatusCode class")
+
+ data = {
+ 'class': status_class.__name__,
+ 'values': status_class.dict(),
+ }
+
+ return Response(data)
diff --git a/InvenTree/generic/states/states.py b/InvenTree/generic/states/states.py
new file mode 100644
index 000000000000..f53b008ea14b
--- /dev/null
+++ b/InvenTree/generic/states/states.py
@@ -0,0 +1,170 @@
+"""Generic implementation of status for InvenTree models."""
+import enum
+import re
+
+
+class BaseEnum(enum.IntEnum):
+ """An `Enum` capabile of having its members have docstrings.
+
+ Based on https://stackoverflow.com/questions/19330460/how-do-i-put-docstrings-on-enums
+ """
+
+ def __new__(cls, *args):
+ """Assign values on creation."""
+ obj = object.__new__(cls)
+ obj._value_ = args[0]
+ return obj
+
+ def __eq__(self, obj):
+ """Override equality operator to allow comparison with int."""
+ if type(self) == type(obj):
+ return super().__eq__(obj)
+ return self.value == obj
+
+ def __ne__(self, obj):
+ """Override inequality operator to allow comparison with int."""
+ if type(self) == type(obj):
+ return super().__ne__(obj)
+ return self.value != obj
+
+
+class StatusCode(BaseEnum):
+ """Base class for representing a set of StatusCodes.
+
+ Use enum syntax to define the status codes, e.g.
+ ```python
+ PENDING = 10, _("Pending"), 'secondary'
+ ```
+
+ The values of the status can be accessed with `StatusCode.PENDING.value`.
+
+ Additionally there are helpers to access all additional attributes `text`, `label`, `color`.
+ """
+
+ def __new__(cls, *args):
+ """Define object out of args."""
+ obj = int.__new__(cls)
+ obj._value_ = args[0]
+
+ # Normal item definition
+ if len(args) == 1:
+ obj.label = args[0]
+ obj.color = 'secondary'
+ else:
+ obj.label = args[1]
+ obj.color = args[2] if len(args) > 2 else 'secondary'
+
+ return obj
+
+ @classmethod
+ def _is_element(cls, d):
+ """Check if the supplied value is a valid status code."""
+ if d.startswith('_'):
+ return False
+ if d != d.upper():
+ return False
+
+ value = getattr(cls, d, None)
+
+ if value is None:
+ return False
+ if callable(value):
+ return False
+ if type(value.value) != int:
+ return False
+ return True
+
+ @classmethod
+ def values(cls, key=None):
+ """Return a dict representation containing all required information"""
+ elements = [itm for itm in cls if cls._is_element(itm.name)]
+ if key is None:
+ return elements
+
+ ret = [itm for itm in elements if itm.value == key]
+ if ret:
+ return ret[0]
+ return None
+
+ @classmethod
+ def render(cls, key, large=False):
+ """Render the value as a HTML label."""
+ # If the key cannot be found, pass it back
+ item = cls.values(key)
+ if item is None:
+ return key
+
+ return f"{item.label}"
+
+ @classmethod
+ def tag(cls):
+ """Return tag for this status code."""
+ # Return the tag if it is defined
+ if hasattr(cls, '_TAG') and bool(cls._TAG):
+ return cls._TAG.value
+
+ # Try to find a default tag
+ # Remove `Status` from the class name
+ ref_name = cls.__name__.removesuffix('Status')
+ # Convert to snake case
+ return re.sub(r'(?Pending")
+ self.assertEqual(GeneralStatus.render(20), "Placed")
+ # render with invalid key
+ self.assertEqual(GeneralStatus.render(100), 100)
+
+ # list
+ self.assertEqual(GeneralStatus.list(), [{'color': 'secondary', 'key': 10, 'label': 'Pending', 'name': 'PENDING'}, {'color': 'primary', 'key': 20, 'label': 'Placed', 'name': 'PLACED'}, {'color': 'success', 'key': 30, 'label': 'Complete', 'name': 'COMPLETE'}])
+
+ # text
+ self.assertEqual(GeneralStatus.text(10), 'Pending')
+ self.assertEqual(GeneralStatus.text(20), 'Placed')
+
+ # items
+ self.assertEqual(list(GeneralStatus.items()), [(10, 'Pending'), (20, 'Placed'), (30, 'Complete')])
+
+ # keys
+ self.assertEqual(list(GeneralStatus.keys()), ([10, 20, 30]))
+
+ # labels
+ self.assertEqual(list(GeneralStatus.labels()), ['Pending', 'Placed', 'Complete'])
+
+ # names
+ self.assertEqual(GeneralStatus.names(), {'PENDING': 10, 'PLACED': 20, 'COMPLETE': 30})
+
+ # dict
+ self.assertEqual(GeneralStatus.dict(), {
+ 'PENDING': {'key': 10, 'name': 'PENDING', 'label': 'Pending', 'color': 'secondary'},
+ 'PLACED': {'key': 20, 'name': 'PLACED', 'label': 'Placed', 'color': 'primary'},
+ 'COMPLETE': {'key': 30, 'name': 'COMPLETE', 'label': 'Complete', 'color': 'success'},
+ })
+
+ # label
+ self.assertEqual(GeneralStatus.label(10), 'Pending')
+
+ def test_tag_function(self):
+ """Test that the status code tag functions."""
+ from .tags import status_label
+
+ self.assertEqual(status_label('general', 10), "Pending")
+
+ # invalid type
+ with self.assertRaises(ValueError) as e:
+ status_label('invalid', 10)
+ self.assertEqual(str(e.exception), "Unknown status type 'invalid'")
+
+ # Test non-existent key
+ self.assertEqual(status_label('general', 100), '100')
+
+ def test_api(self):
+ """Test StatusView API view."""
+ view = StatusView.as_view()
+ rqst = RequestFactory().get('status/',)
+ force_authenticate(rqst, user=self.user)
+
+ # Correct call
+ resp = view(rqst, **{StatusView.MODEL_REF: GeneralStatus})
+ self.assertEqual(resp.data, {'class': 'GeneralStatus', 'values': {'COMPLETE': {'key': 30, 'name': 'COMPLETE', 'label': 'Complete', 'color': 'success'}, 'PENDING': {'key': 10, 'name': 'PENDING', 'label': 'Pending', 'color': 'secondary'}, 'PLACED': {'key': 20, 'name': 'PLACED', 'label': 'Placed', 'color': 'primary'}}})
+
+ # No status defined
+ resp = view(rqst, **{StatusView.MODEL_REF: None})
+ self.assertEqual(resp.status_code, 400)
+ self.assertEqual(str(resp.rendered_content, 'utf-8'), '["StatusView view called without \'statusmodel\' parameter"]')
+
+ # Invalid call - not a class
+ with self.assertRaises(NotImplementedError) as e:
+ resp = view(rqst, **{StatusView.MODEL_REF: 'invalid'})
+ self.assertEqual(str(e.exception), "`status_class` not a class")
+
+ # Invalid call - not the right class
+ with self.assertRaises(NotImplementedError) as e:
+ resp = view(rqst, **{StatusView.MODEL_REF: object})
+ self.assertEqual(str(e.exception), "`status_class` not a valid StatusCode class")
diff --git a/InvenTree/generic/templatetags/__init__.py b/InvenTree/generic/templatetags/__init__.py
new file mode 100644
index 000000000000..829285424760
--- /dev/null
+++ b/InvenTree/generic/templatetags/__init__.py
@@ -0,0 +1 @@
+"""Template tags for generic *things*."""
diff --git a/InvenTree/generic/templatetags/generic.py b/InvenTree/generic/templatetags/generic.py
new file mode 100644
index 000000000000..af33e9d36b24
--- /dev/null
+++ b/InvenTree/generic/templatetags/generic.py
@@ -0,0 +1,10 @@
+"""Template tags for generic *things*."""
+
+from django import template
+
+register = template.Library()
+from generic.states.tags import status_label # noqa: E402
+
+__all__ = [
+ status_label,
+]
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index 322ec71ab863..0e6071ebabb8 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -16,15 +16,18 @@
from common.models import InvenTreeSetting, ProjectCode
from common.settings import settings
from company.models import SupplierPart
+from generic.states import StatusView
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
- ListCreateDestroyAPIView, MetadataView, StatusView)
+ ListCreateDestroyAPIView, MetadataView)
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
RetrieveUpdateDestroyAPI)
-from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
- ReturnOrderStatus, SalesOrderStatus)
+from InvenTree.status_codes import (PurchaseOrderStatus,
+ PurchaseOrderStatusGroups,
+ ReturnOrderLineStatus, ReturnOrderStatus,
+ SalesOrderStatus, SalesOrderStatusGroups)
from order import models, serializers
from order.admin import (PurchaseOrderExtraLineResource,
PurchaseOrderLineItemResource, PurchaseOrderResource,
@@ -431,9 +434,9 @@ def filter_pending(self, queryset, name, value):
"""Filter by "pending" status (order status = pending)"""
if str2bool(value):
- return queryset.filter(order__status__in=PurchaseOrderStatus.OPEN)
+ return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
else:
- return queryset.exclude(order__status__in=PurchaseOrderStatus.OPEN)
+ return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN)
received = rest_filters.BooleanFilter(label='received', method='filter_received')
@@ -448,7 +451,7 @@ def filter_received(self, queryset, name, value):
return queryset.filter(q)
else:
# Only count "pending" orders
- return queryset.exclude(q).filter(order__status__in=PurchaseOrderStatus.OPEN)
+ return queryset.exclude(q).filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
class PurchaseOrderLineItemMixin:
@@ -984,12 +987,12 @@ def filter_queryset(self, queryset):
# Filter only "open" orders
# Filter only allocations which have *not* shipped
queryset = queryset.filter(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None,
)
else:
queryset = queryset.exclude(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None
)
@@ -1471,21 +1474,21 @@ def items(self, obj):
if obj['include_completed'] is False:
# Do not include completed orders from list in this case
# Completed status = 30
- outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE)
+ outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE.value)
else:
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'sales-order':
if obj['include_completed'] is False:
# Do not include completed (=shipped) orders from list in this case
# Shipped status = 20
- outlist = models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED)
+ outlist = models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED.value)
else:
outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'return-order':
if obj['include_completed'] is False:
# Do not include completed orders from list in this case
# Complete status = 30
- outlist = models.ReturnOrder.objects.filter(target_date__isnull=False).filter(status__lt=ReturnOrderStatus.COMPLETE)
+ outlist = models.ReturnOrder.objects.filter(target_date__isnull=False).filter(status__lt=ReturnOrderStatus.COMPLETE.value)
else:
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False)
else:
diff --git a/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py b/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py
new file mode 100644
index 000000000000..ac958308074c
--- /dev/null
+++ b/InvenTree/order/migrations/0096_alter_returnorderlineitem_outcome.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.19 on 2023-06-04 17:43
+
+from django.db import migrations, models
+import InvenTree.status_codes
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('order', '0095_salesordershipment_delivery_date'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='returnorderlineitem',
+ name='outcome',
+ field=models.PositiveIntegerField(choices=InvenTree.status_codes.ReturnOrderLineStatus.items(), default=10, help_text='Outcome for this line item', verbose_name='Outcome'),
+ ),
+ ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index 34cb95156477..f7301e289495 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -41,9 +41,12 @@
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, MetadataMixin,
ReferenceIndexingMixin)
-from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
- ReturnOrderStatus, SalesOrderStatus,
- StockHistoryCode, StockStatus)
+from InvenTree.status_codes import (PurchaseOrderStatus,
+ PurchaseOrderStatusGroups,
+ ReturnOrderLineStatus, ReturnOrderStatus,
+ ReturnOrderStatusGroups, SalesOrderStatus,
+ SalesOrderStatusGroups, StockHistoryCode,
+ StockStatus)
from part import models as PartModels
from plugin.events import trigger_event
@@ -294,7 +297,7 @@ def get_api_url():
@classmethod
def get_status_class(cls):
"""Return the PurchasOrderStatus class"""
- return PurchaseOrderStatus
+ return PurchaseOrderStatusGroups
@classmethod
def api_defaults(cls, request):
@@ -333,10 +336,10 @@ def filterByDate(queryset, min_date, max_date):
return queryset
# Construct a queryset for "received" orders within the range
- received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
+ received = Q(status=PurchaseOrderStatus.COMPLETE.value) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
# Construct a queryset for "pending" orders within the range
- pending = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
+ pending = Q(status__in=PurchaseOrderStatusGroups.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
# TODO - Construct a queryset for "overdue" orders within the range
@@ -361,7 +364,7 @@ def __str__(self):
]
)
- status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
+ status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING.value, choices=PurchaseOrderStatus.items(),
help_text=_('Purchase order status'))
@property
@@ -479,7 +482,7 @@ def place_order(self):
Order must be currently PENDING.
"""
if self.status == PurchaseOrderStatus.PENDING:
- self.status = PurchaseOrderStatus.PLACED
+ self.status = PurchaseOrderStatus.PLACED.value
self.issue_date = datetime.now().date()
self.save()
@@ -500,7 +503,7 @@ def complete_order(self):
Order must be currently PLACED.
"""
if self.status == PurchaseOrderStatus.PLACED:
- self.status = PurchaseOrderStatus.COMPLETE
+ self.status = PurchaseOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date()
self.save()
@@ -520,7 +523,7 @@ def is_pending(self):
@property
def is_open(self):
"""Return True if the PurchaseOrder is 'open'"""
- return self.status in PurchaseOrderStatus.OPEN
+ return self.status in PurchaseOrderStatusGroups.OPEN
def can_cancel(self):
"""A PurchaseOrder can only be cancelled under the following circumstances.
@@ -537,7 +540,7 @@ def can_cancel(self):
def cancel_order(self):
"""Marks the PurchaseOrder as CANCELLED."""
if self.can_cancel():
- self.status = PurchaseOrderStatus.CANCELLED
+ self.status = PurchaseOrderStatus.CANCELLED.value
self.save()
trigger_event('purchaseorder.cancelled', id=self.pk)
@@ -574,7 +577,7 @@ def is_complete(self):
return self.lines.count() > 0 and self.pending_line_items().count() == 0
@transaction.atomic
- def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
+ def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK.value, **kwargs):
"""Receive a line item (or partial line item) against this PurchaseOrder."""
# Extract optional batch code for the new stock item
batch_code = kwargs.get('batch_code', '')
@@ -701,7 +704,7 @@ def get_api_url():
@classmethod
def get_status_class(cls):
"""Return the SalesOrderStatus class"""
- return SalesOrderStatus
+ return SalesOrderStatusGroups
@classmethod
def api_defaults(cls, request):
@@ -739,10 +742,10 @@ def filterByDate(queryset, min_date, max_date):
return queryset
# Construct a queryset for "completed" orders within the range
- completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
+ completed = Q(status__in=SalesOrderStatusGroups.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
# Construct a queryset for "pending" orders within the range
- pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
+ pending = Q(status__in=SalesOrderStatusGroups.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
# TODO: Construct a queryset for "overdue" orders within the range
@@ -783,7 +786,7 @@ def company(self):
return self.customer
status = models.PositiveIntegerField(
- default=SalesOrderStatus.PENDING,
+ default=SalesOrderStatus.PENDING.value,
choices=SalesOrderStatus.items(),
verbose_name=_('Status'), help_text=_('Purchase order status')
)
@@ -813,7 +816,7 @@ def is_pending(self):
@property
def is_open(self):
"""Return True if this order is 'open' (either 'pending' or 'in_progress')"""
- return self.status in SalesOrderStatus.OPEN
+ return self.status in SalesOrderStatusGroups.OPEN
@property
def stock_allocations(self):
@@ -881,7 +884,7 @@ def issue_order(self):
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
if self.status == SalesOrderStatus.PENDING:
- self.status = SalesOrderStatus.IN_PROGRESS
+ self.status = SalesOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date()
self.save()
@@ -892,7 +895,7 @@ def complete_order(self, user, **kwargs):
if not self.can_complete(**kwargs):
return False
- self.status = SalesOrderStatus.SHIPPED
+ self.status = SalesOrderStatus.SHIPPED.value
self.shipped_by = user
self.shipment_date = datetime.now()
@@ -921,7 +924,7 @@ def cancel_order(self):
if not self.can_cancel():
return False
- self.status = SalesOrderStatus.CANCELLED
+ self.status = SalesOrderStatus.CANCELLED.value
self.save()
for line in self.lines.all():
@@ -1696,7 +1699,7 @@ def get_api_url():
@classmethod
def get_status_class(cls):
"""Return the ReturnOrderStatus class"""
- return ReturnOrderStatus
+ return ReturnOrderStatusGroups
@classmethod
def api_defaults(cls, request):
@@ -1742,7 +1745,7 @@ def company(self):
return self.customer
status = models.PositiveIntegerField(
- default=ReturnOrderStatus.PENDING,
+ default=ReturnOrderStatus.PENDING.value,
choices=ReturnOrderStatus.items(),
verbose_name=_('Status'), help_text=_('Return order status')
)
@@ -1773,7 +1776,7 @@ def is_pending(self):
@property
def is_open(self):
"""Return True if this order is outstanding"""
- return self.status in ReturnOrderStatus.OPEN
+ return self.status in ReturnOrderStatusGroups.OPEN
@property
def is_received(self):
@@ -1784,7 +1787,7 @@ def is_received(self):
def cancel_order(self):
"""Cancel this ReturnOrder (if not already cancelled)"""
if self.status != ReturnOrderStatus.CANCELLED:
- self.status = ReturnOrderStatus.CANCELLED
+ self.status = ReturnOrderStatus.CANCELLED.value
self.save()
trigger_event('returnorder.cancelled', id=self.pk)
@@ -1794,7 +1797,7 @@ def complete_order(self):
"""Complete this ReturnOrder (if not already completed)"""
if self.status == ReturnOrderStatus.IN_PROGRESS:
- self.status = ReturnOrderStatus.COMPLETE
+ self.status = ReturnOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date()
self.save()
@@ -1809,7 +1812,7 @@ def issue_order(self):
"""Issue this ReturnOrder (if currently pending)"""
if self.status == ReturnOrderStatus.PENDING:
- self.status = ReturnOrderStatus.IN_PROGRESS
+ self.status = ReturnOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date()
self.save()
@@ -1833,7 +1836,7 @@ def receive_line_item(self, line, location, user, note=''):
stock_item = line.item
deltas = {
- 'status': StockStatus.QUARANTINED,
+ 'status': StockStatus.QUARANTINED.value,
'returnorder': self.pk,
'location': location.pk,
}
@@ -1842,7 +1845,7 @@ def receive_line_item(self, line, location, user, note=''):
deltas['customer'] = stock_item.customer.pk
# Update the StockItem
- stock_item.status = StockStatus.QUARANTINED
+ stock_item.status = StockStatus.QUARANTINED.value
stock_item.location = location
stock_item.customer = None
stock_item.sales_order = None
@@ -1926,7 +1929,7 @@ def received(self):
return self.received_date is not None
outcome = models.PositiveIntegerField(
- default=ReturnOrderLineStatus.PENDING,
+ default=ReturnOrderLineStatus.PENDING.value,
choices=ReturnOrderLineStatus.items(),
verbose_name=_('Outcome'), help_text=_('Outcome for this line item')
)
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index f8c3c0dcc4f4..d0dee4740bb3 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -27,8 +27,9 @@
InvenTreeDecimalField,
InvenTreeModelSerializer,
InvenTreeMoneySerializer)
-from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderStatus,
- SalesOrderStatus, StockStatus)
+from InvenTree.status_codes import (PurchaseOrderStatusGroups,
+ ReturnOrderStatus, SalesOrderStatusGroups,
+ StockStatus)
from part.serializers import PartBriefSerializer
from users.serializers import OwnerSerializer
@@ -381,7 +382,7 @@ def validate_quantity(self, quantity):
def validate_purchase_order(self, purchase_order):
"""Validation for the 'purchase_order' field"""
- if purchase_order.status not in PurchaseOrderStatus.OPEN:
+ if purchase_order.status not in PurchaseOrderStatusGroups.OPEN:
raise ValidationError(_('Order is not open'))
return purchase_order
@@ -518,8 +519,8 @@ def validate_quantity(self, quantity):
)
status = serializers.ChoiceField(
- choices=list(StockStatus.items()),
- default=StockStatus.OK,
+ choices=StockStatus.items(),
+ default=StockStatus.OK.value,
label=_('Status'),
)
@@ -906,7 +907,7 @@ def annotate_queryset(queryset):
queryset = queryset.annotate(
overdue=Case(
When(
- Q(order__status__in=SalesOrderStatus.OPEN) & order.models.SalesOrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
+ Q(order__status__in=SalesOrderStatusGroups.OPEN) & order.models.SalesOrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField()),
)
diff --git a/InvenTree/order/tasks.py b/InvenTree/order/tasks.py
index e04906bd002c..225c3a7976ac 100644
--- a/InvenTree/order/tasks.py
+++ b/InvenTree/order/tasks.py
@@ -7,7 +7,8 @@
import common.notifications
import InvenTree.helpers_model
import order.models
-from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
+from InvenTree.status_codes import (PurchaseOrderStatusGroups,
+ SalesOrderStatusGroups)
from InvenTree.tasks import ScheduledTask, scheduled_task
from plugin.events import trigger_event
@@ -68,7 +69,7 @@ def check_overdue_purchase_orders():
overdue_orders = order.models.PurchaseOrder.objects.filter(
target_date=yesterday,
- status__in=PurchaseOrderStatus.OPEN
+ status__in=PurchaseOrderStatusGroups.OPEN,
)
for po in overdue_orders:
@@ -131,7 +132,7 @@ def check_overdue_sales_orders():
overdue_orders = order.models.SalesOrder.objects.filter(
target_date=yesterday,
- status__in=SalesOrderStatus.OPEN
+ status__in=SalesOrderStatusGroups.OPEN,
)
for po in overdue_orders:
diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html
index 4eb6c5565920..436636b8f821 100644
--- a/InvenTree/order/templates/order/order_base.html
+++ b/InvenTree/order/templates/order/order_base.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% load static %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% block page_title %}
{% inventree_title %} | {% trans "Purchase Order" %}
@@ -121,7 +121,7 @@
|
{% trans "Order Status" %} |
- {% purchase_order_status_label order.status %}
+ {% status_label 'purchase_order' order.status %}
{% if order.is_overdue %}
{% trans "Overdue" %}
{% endif %}
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index e484b245c4ac..1994be41da80 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -1,7 +1,7 @@
{% extends "order/order_base.html" %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% load i18n %}
{% load static %}
diff --git a/InvenTree/order/templates/order/return_order_base.html b/InvenTree/order/templates/order/return_order_base.html
index 9288e41f373c..d701112b0bf7 100644
--- a/InvenTree/order/templates/order/return_order_base.html
+++ b/InvenTree/order/templates/order/return_order_base.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% load static %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% block page_title %}
{% inventree_title %} | {% trans "Return Order" %}
@@ -113,7 +113,7 @@
| |
{% trans "Order Status" %} |
- {% return_order_status_label order.status %}
+ {% status_label 'return_order' order.status %}
{% if order.is_overdue %}
{% trans "Overdue" %}
{% endif %}
diff --git a/InvenTree/order/templates/order/return_order_detail.html b/InvenTree/order/templates/order/return_order_detail.html
index 56a943308f73..b0542ba18804 100644
--- a/InvenTree/order/templates/order/return_order_detail.html
+++ b/InvenTree/order/templates/order/return_order_detail.html
@@ -1,7 +1,7 @@
{% extends "order/return_order_base.html" %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% load i18n %}
{% load static %}
diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html
index be8dac2bab75..b149a440eb85 100644
--- a/InvenTree/order/templates/order/sales_order_base.html
+++ b/InvenTree/order/templates/order/sales_order_base.html
@@ -3,7 +3,7 @@
{% load i18n %}
{% load static %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% block page_title %}
{% inventree_title %} | {% trans "Sales Order" %}
@@ -118,7 +118,7 @@
| |
{% trans "Order Status" %} |
- {% sales_order_status_label order.status %}
+ {% status_label 'sales_order' order.status %}
{% if order.is_overdue %}
{% trans "Overdue" %}
{% endif %}
diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html
index ede1e30711fc..8a43c3ee1bd6 100644
--- a/InvenTree/order/templates/order/sales_order_detail.html
+++ b/InvenTree/order/templates/order/sales_order_detail.html
@@ -1,7 +1,7 @@
{% extends "order/sales_order_base.html" %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% load i18n %}
{% load static %}
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 436b21c938d4..99ee7bc8940b 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -17,7 +17,7 @@
from company.models import Company
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
ReturnOrderStatus, SalesOrderStatus,
- StockStatus)
+ SalesOrderStatusGroups, StockStatus)
from InvenTree.unit_test import InvenTreeAPITestCase
from order import models
from part.models import Part
@@ -562,7 +562,7 @@ def test_po_calendar(self):
# Test without completed orders
response = self.get(url, expected_code=200, format=None)
- number_orders = len(models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE))
+ number_orders = len(models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE.value))
# Transform content to a Calendar object
calendar = Calendar.from_ical(response.content)
@@ -743,7 +743,7 @@ def setUp(self):
# Mark the order as "placed" so we can receive line items
order = models.PurchaseOrder.objects.get(pk=1)
- order.status = PurchaseOrderStatus.PLACED
+ order.status = PurchaseOrderStatus.PLACED.value
order.save()
def test_empty(self):
@@ -944,7 +944,7 @@ def test_valid(self):
# Before posting "valid" data, we will mark the purchase order as "pending"
# In this case we do expect an error!
order = models.PurchaseOrder.objects.get(pk=1)
- order.status = PurchaseOrderStatus.PENDING
+ order.status = PurchaseOrderStatus.PENDING.value
order.save()
response = self.post(
@@ -956,7 +956,7 @@ def test_valid(self):
self.assertIn('can only be received against', str(response.data))
# Now, set the PurchaseOrder back to "PLACED" so the items can be received
- order.status = PurchaseOrderStatus.PLACED
+ order.status = PurchaseOrderStatus.PLACED.value
order.save()
# Receive two separate line items against this order
@@ -1388,7 +1388,7 @@ def test_so_calendar(self):
# Test without completed orders
response = self.get(url, expected_code=200, format=None)
- number_orders = len(models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED))
+ number_orders = len(models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED.value))
# Transform content to a Calendar object
calendar = Calendar.from_ical(response.content)
@@ -1621,7 +1621,7 @@ def test_download_csv(self):
file,
required_cols=required_cols,
excluded_cols=excluded_cols,
- required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(),
+ required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatusGroups.OPEN).count(),
delimiter='\t',
)
diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py
index a45f09fc4897..8bcdbeae8eab 100644
--- a/InvenTree/order/test_migrations.py
+++ b/InvenTree/order/test_migrations.py
@@ -109,7 +109,7 @@ def prepare(self):
reference=f'SO{ii}',
customer=customer,
description='A sales order for stuffs',
- status=SalesOrderStatus.PENDING,
+ status=SalesOrderStatus.PENDING.value,
)
order.save()
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index d18ac8ea20ef..4af7ed98dd05 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -29,8 +29,9 @@
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
UpdateAPI)
from InvenTree.permissions import RolePermission
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
- SalesOrderStatus)
+from InvenTree.status_codes import (BuildStatusGroups,
+ PurchaseOrderStatusGroups,
+ SalesOrderStatusGroups)
from part.admin import PartCategoryResource, PartResource
from . import serializers as part_serializers
@@ -479,7 +480,7 @@ def add_schedule_entry(date, quantity, title, label, url, speculative_quantity=0
# Add purchase order (incoming stock) information
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part=part,
- order__status__in=PurchaseOrderStatus.OPEN,
+ order__status__in=PurchaseOrderStatusGroups.OPEN,
)
for line in po_lines:
@@ -502,7 +503,7 @@ def add_schedule_entry(date, quantity, title, label, url, speculative_quantity=0
# Add sales order (outgoing stock) information
so_lines = order.models.SalesOrderLineItem.objects.filter(
part=part,
- order__status__in=SalesOrderStatus.OPEN,
+ order__status__in=SalesOrderStatusGroups.OPEN,
)
for line in so_lines:
@@ -522,7 +523,7 @@ def add_schedule_entry(date, quantity, title, label, url, speculative_quantity=0
# Add build orders (incoming stock) information
build_orders = Build.objects.filter(
part=part,
- status__in=BuildStatus.ACTIVE_CODES
+ status__in=BuildStatusGroups.ACTIVE_CODES
)
for build in build_orders:
@@ -567,12 +568,12 @@ def add_schedule_entry(date, quantity, title, label, url, speculative_quantity=0
# An "inherited" BOM item filters down to variant parts also
children = bom_item.part.get_descendants(include_self=True)
builds = Build.objects.filter(
- status__in=BuildStatus.ACTIVE_CODES,
+ status__in=BuildStatusGroups.ACTIVE_CODES,
part__in=children,
)
else:
builds = Build.objects.filter(
- status__in=BuildStatus.ACTIVE_CODES,
+ status__in=BuildStatusGroups.ACTIVE_CODES,
part=bom_item.part,
)
@@ -1197,7 +1198,7 @@ def filter_queryset(self, queryset):
if stock_to_build is not None:
# Get active builds
- builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
+ builds = Build.objects.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
# Store parts with builds needing stock
parts_needed_to_complete_builds = []
# Filter required parts
diff --git a/InvenTree/part/filters.py b/InvenTree/part/filters.py
index aa0015ea94a1..70555a0a78c1 100644
--- a/InvenTree/part/filters.py
+++ b/InvenTree/part/filters.py
@@ -28,8 +28,9 @@
import part.models
import stock.models
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
- SalesOrderStatus)
+from InvenTree.status_codes import (BuildStatusGroups,
+ PurchaseOrderStatusGroups,
+ SalesOrderStatusGroups)
def annotate_on_order_quantity(reference: str = ''):
@@ -46,7 +47,7 @@ def annotate_on_order_quantity(reference: str = ''):
# Filter only 'active' purhase orders
# Filter only line with outstanding quantity
order_filter = Q(
- order__status__in=PurchaseOrderStatus.OPEN,
+ order__status__in=PurchaseOrderStatusGroups.OPEN,
quantity__gt=F('received'),
)
@@ -111,7 +112,7 @@ def annotate_build_order_allocations(reference: str = ''):
"""
# Build filter only returns 'active' build orders
- build_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
+ build_filter = Q(build__status__in=BuildStatusGroups.ACTIVE_CODES)
return Coalesce(
SubquerySum(
@@ -137,7 +138,7 @@ def annotate_sales_order_allocations(reference: str = ''):
# Order filter only returns incomplete shipments for open orders
order_filter = Q(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None,
)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 984016ede0be..b775960b705b 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -51,8 +51,9 @@
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
InvenTreeTree, MetadataMixin)
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
- SalesOrderStatus)
+from InvenTree.status_codes import (BuildStatusGroups, PurchaseOrderStatus,
+ PurchaseOrderStatusGroups,
+ SalesOrderStatus, SalesOrderStatusGroups)
from order import models as OrderModels
from stock import models as StockModels
@@ -1070,7 +1071,7 @@ def requiring_build_orders(self):
# Now, get a list of outstanding build orders which require this part
builds = BuildModels.Build.objects.filter(
part__in=self.get_used_in(),
- status__in=BuildStatus.ACTIVE_CODES
+ status__in=BuildStatusGroups.ACTIVE_CODES
)
return builds
@@ -1104,7 +1105,7 @@ def requiring_sales_orders(self):
# Get a list of line items for open orders which match this part
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
- order__status__in=SalesOrderStatus.OPEN,
+ order__status__in=SalesOrderStatusGroups.OPEN,
part=self
)
@@ -1117,7 +1118,7 @@ def required_sales_order_quantity(self):
"""Return the quantity of this part required for active sales orders."""
# Get a list of line items for open orders which match this part
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
- order__status__in=SalesOrderStatus.OPEN,
+ order__status__in=SalesOrderStatusGroups.OPEN,
part=self
)
@@ -1329,7 +1330,7 @@ def active_builds(self):
Builds marked as 'complete' or 'cancelled' are ignored
"""
- return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES)
+ return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
@property
def quantity_being_built(self):
@@ -1401,13 +1402,13 @@ def sales_order_allocations(self, **kwargs):
if pending is True:
# Look only for 'open' orders which have not shipped
queryset = queryset.filter(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None,
)
elif pending is False:
# Look only for 'closed' orders or orders which have shipped
queryset = queryset.exclude(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None,
)
@@ -2161,7 +2162,7 @@ def on_order(self):
# Look at any incomplete line item for open orders
lines = sp.purchase_order_line_items.filter(
- order__status__in=PurchaseOrderStatus.OPEN,
+ order__status__in=PurchaseOrderStatusGroups.OPEN,
quantity__gt=F('received'),
)
@@ -2559,7 +2560,7 @@ def update_purchase_cost(self, save=True):
# Find all line items for completed orders which reference this part
line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
- order__status=PurchaseOrderStatus.COMPLETE,
+ order__status=PurchaseOrderStatus.COMPLETE.value,
received__gt=0,
part__part=self.part,
)
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 97a1de33a076..2a209633d48f 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -25,7 +25,7 @@
import part.filters
import part.tasks
import stock.models
-from InvenTree.status_codes import BuildStatus
+from InvenTree.status_codes import BuildStatusGroups
from InvenTree.tasks import offload_task
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
@@ -532,7 +532,7 @@ def annotate_queryset(queryset):
# Filter to limit builds to "active"
build_filter = Q(
- status__in=BuildStatus.ACTIVE_CODES
+ status__in=BuildStatusGroups.ACTIVE_CODES
)
# Annotate with the total 'building' quantity
diff --git a/InvenTree/part/templatetags/status_codes.py b/InvenTree/part/templatetags/status_codes.py
deleted file mode 100644
index 9eef869678fa..000000000000
--- a/InvenTree/part/templatetags/status_codes.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Provide templates for the various model status codes."""
-
-from django import template
-from django.utils.safestring import mark_safe
-
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
- ReturnOrderStatus, SalesOrderStatus,
- StockStatus)
-
-register = template.Library()
-
-
-@register.simple_tag
-def purchase_order_status_label(key, *args, **kwargs):
- """Render a PurchaseOrder status label."""
- return mark_safe(PurchaseOrderStatus.render(key, large=kwargs.get('large', False)))
-
-
-@register.simple_tag
-def sales_order_status_label(key, *args, **kwargs):
- """Render a SalesOrder status label."""
- return mark_safe(SalesOrderStatus.render(key, large=kwargs.get('large', False)))
-
-
-@register.simple_tag
-def return_order_status_label(key, *args, **kwargs):
- """Render a ReturnOrder status label"""
- return mark_safe(ReturnOrderStatus.render(key, large=kwargs.get('large', False)))
-
-
-@register.simple_tag
-def stock_status_label(key, *args, **kwargs):
- """Render a StockItem status label."""
- return mark_safe(StockStatus.render(key, large=kwargs.get('large', False)))
-
-
-@register.simple_tag
-def stock_status_text(key, *args, **kwargs):
- """Render the text value of a StockItem status value"""
- return mark_safe(StockStatus.text(key))
-
-
-@register.simple_tag
-def build_status_label(key, *args, **kwargs):
- """Render a Build status label."""
- return mark_safe(BuildStatus.render(key, large=kwargs.get('large', False)))
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 7944961e1877..2d667703186f 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -18,7 +18,7 @@
import order.models
from common.models import InvenTreeSetting
from company.models import Company, SupplierPart
-from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
+from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatusGroups,
StockStatus)
from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import (BomItem, BomItemSubstitute, Part, PartCategory,
@@ -1628,7 +1628,7 @@ def test_details(self):
# How many parts are 'on order' for this part?
lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part__pk=1,
- order__status__in=PurchaseOrderStatus.OPEN,
+ order__status__in=PurchaseOrderStatusGroups.OPEN,
)
on_order = 0
@@ -1857,7 +1857,7 @@ def setUpTestData(cls):
StockItem.objects.create(part=cls.part, quantity=300)
# Now create another 400 units which are LOST
- StockItem.objects.create(part=cls.part, quantity=400, status=StockStatus.LOST)
+ StockItem.objects.create(part=cls.part, quantity=400, status=StockStatus.LOST.value)
def get_part_data(self):
"""Helper function for retrieving part data"""
@@ -1992,7 +1992,7 @@ def test_allocation_annotations(self):
quantity=10,
title='Making some assemblies',
reference='BO-9999',
- status=BuildStatus.PRODUCTION,
+ status=BuildStatus.PRODUCTION.value,
)
bom_item = BomItem.objects.get(pk=6)
@@ -2133,7 +2133,7 @@ def test_on_order(self):
for line_item in sp.purchase_order_line_items.all():
po = line_item.order
- if po.status in PurchaseOrderStatus.OPEN:
+ if po.status in PurchaseOrderStatusGroups.OPEN:
remaining = line_item.quantity - line_item.received
if remaining > 0:
diff --git a/InvenTree/part/test_pricing.py b/InvenTree/part/test_pricing.py
index 9c62cba9771c..18dc5c170a0b 100644
--- a/InvenTree/part/test_pricing.py
+++ b/InvenTree/part/test_pricing.py
@@ -345,7 +345,7 @@ def test_purchase_pricing(self):
self.assertIsNone(pricing.purchase_cost_min)
self.assertIsNone(pricing.purchase_cost_max)
- po.status = PurchaseOrderStatus.COMPLETE
+ po.status = PurchaseOrderStatus.COMPLETE.value
po.save()
pricing.update_purchase_cost()
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 06e4af3788b2..3a45debe9c1c 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -22,8 +22,9 @@
from build.serializers import BuildSerializer
from company.models import Company, SupplierPart
from company.serializers import CompanySerializer, SupplierPartSerializer
+from generic.states import StatusView
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
- ListCreateDestroyAPIView, MetadataView, StatusView)
+ ListCreateDestroyAPIView, MetadataView)
from InvenTree.filters import (ORDER_FILTER, SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS)
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
diff --git a/InvenTree/stock/migrations/0061_auto_20210511_0911.py b/InvenTree/stock/migrations/0061_auto_20210511_0911.py
index e26079cb6c8f..aacaf01edcd5 100644
--- a/InvenTree/stock/migrations/0061_auto_20210511_0911.py
+++ b/InvenTree/stock/migrations/0061_auto_20210511_0911.py
@@ -190,7 +190,7 @@ def update_history(apps, schema_editor):
tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER
if tracking_type is not None:
- entry.tracking_type = tracking_type
+ entry.tracking_type = tracking_type.value
updated = True
if updated:
diff --git a/InvenTree/stock/migrations/0096_auto_20230330_1121.py b/InvenTree/stock/migrations/0096_auto_20230330_1121.py
index 0e7c92cfc7c1..b2dd16cc03ee 100644
--- a/InvenTree/stock/migrations/0096_auto_20230330_1121.py
+++ b/InvenTree/stock/migrations/0096_auto_20230330_1121.py
@@ -43,7 +43,7 @@ def update_stock_history(apps, schema_editor):
history.deltas['salesorder'] = item.sales_order.pk
# Change the history type
- history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
+ history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER.value
history.save()
n += 1
diff --git a/InvenTree/stock/migrations/0102_alter_stockitem_status.py b/InvenTree/stock/migrations/0102_alter_stockitem_status.py
new file mode 100644
index 000000000000..7b8cf4d23969
--- /dev/null
+++ b/InvenTree/stock/migrations/0102_alter_stockitem_status.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.19 on 2023-06-04 17:43
+
+import django.core.validators
+from django.db import migrations, models
+import InvenTree.status_codes
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0101_stockitemtestresult_metadata'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='stockitem',
+ name='status',
+ field=models.PositiveIntegerField(choices=InvenTree.status_codes.StockStatus.items(), default=10, validators=[django.core.validators.MinValueValidator(0)]),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index a3c838504ed9..12092d3eba87 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -34,8 +34,8 @@
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, InvenTreeTree,
MetadataMixin, extract_int)
-from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode,
- StockStatus)
+from InvenTree.status_codes import (SalesOrderStatusGroups, StockHistoryCode,
+ StockStatus, StockStatusGroups)
from part import models as PartModels
from plugin.events import trigger_event
from users.models import Owner
@@ -334,7 +334,7 @@ def api_instance_filters(self):
customer=None,
consumed_by=None,
is_building=False,
- status__in=StockStatus.AVAILABLE_CODES
+ status__in=StockStatusGroups.AVAILABLE_CODES
)
# A query filter which can be used to filter StockItem objects which have expired
@@ -806,7 +806,7 @@ def get_part_name(self):
)
status = models.PositiveIntegerField(
- default=StockStatus.OK,
+ default=StockStatus.OK.value,
choices=StockStatus.items(),
validators=[MinValueValidator(0)])
@@ -1082,12 +1082,12 @@ def get_sales_order_allocations(self, active=True):
if active is True:
query = query.filter(
- line__order__status__in=SalesOrderStatus.OPEN,
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None
)
elif active is False:
query = query.exclude(
- line__order__status__in=SalesOrderStatus.OPEN
+ line__order__status__in=SalesOrderStatusGroups.OPEN,
).exclude(
shipment__shipment_date=None
)
@@ -1346,7 +1346,7 @@ def add_tracking_entry(self, entry_type: int, user: User, deltas: dict = None, n
entry = StockItemTracking.objects.create(
item=self,
- tracking_type=entry_type,
+ tracking_type=entry_type.value,
user=user,
date=datetime.now(),
notes=notes,
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index da04f739da0c..618413b562b1 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -2,7 +2,7 @@
{% load static %}
{% load plugin_extras %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
{% load i18n %}
{% load l10n %}
@@ -421,7 +421,7 @@ {% if item.quantity != available %}{% decimal available %} / {% endif %}{% d
|
{% trans "Status" %} |
- {% stock_status_label item.status %} |
+ {% status_label 'stock' item.status %} |
{% if item.expiry_date %}
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index cd513b1fb0fd..d8c0a9cf3525 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -385,11 +385,11 @@ def test_filter_by_in_stock(self):
def test_filter_by_status(self):
"""Filter StockItem by 'status' field."""
codes = {
- StockStatus.OK: 27,
- StockStatus.DESTROYED: 1,
- StockStatus.LOST: 1,
- StockStatus.DAMAGED: 0,
- StockStatus.REJECTED: 0,
+ StockStatus.OK.value: 27,
+ StockStatus.DESTROYED.value: 1,
+ StockStatus.LOST.value: 1,
+ StockStatus.DAMAGED.value: 0,
+ StockStatus.REJECTED.value: 0,
}
for code in codes.keys():
@@ -1465,7 +1465,7 @@ def test_invalid(self):
stock_item = StockItem.objects.create(
part=part.models.Part.objects.get(pk=1),
- status=StockStatus.DESTROYED,
+ status=StockStatus.DESTROYED.value,
quantity=5,
)
diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py
index 6177f0f0a183..be2c58733c46 100644
--- a/InvenTree/stock/test_views.py
+++ b/InvenTree/stock/test_views.py
@@ -112,7 +112,7 @@ def assert_api_change(self):
"""Helper function to get response to API change."""
return self.client.patch(
reverse('api-stock-detail', args=(self.test_item_id,)),
- {'status': StockStatus.DAMAGED},
+ {'status': StockStatus.DAMAGED.value},
content_type='application/json',
)
@@ -156,7 +156,7 @@ def test_ownership_functions(self):
# Check that user is allowed to change item
self.assertTrue(item.check_ownership(self.user)) # Owner is group -> True
self.assertTrue(location.check_ownership(self.user)) # Owner is group -> True
- self.assertContains(self.assert_api_change(), f'"status":{StockStatus.DAMAGED}', status_code=200)
+ self.assertContains(self.assert_api_change(), f'"status":{StockStatus.DAMAGED.value}', status_code=200)
# Change group
new_group = Group.objects.create(name='new_group')
diff --git a/InvenTree/templates/js/translated/status_codes.js b/InvenTree/templates/js/translated/status_codes.js
index 8702944730d7..63c2648f91dd 100644
--- a/InvenTree/templates/js/translated/status_codes.js
+++ b/InvenTree/templates/js/translated/status_codes.js
@@ -1,5 +1,5 @@
{% load i18n %}
-{% load status_codes %}
+{% load generic %}
{% load inventree_extras %}
/* globals
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index e2aa8d72a380..ffc66816bbb1 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -1,6 +1,6 @@
{% load i18n %}
{% load inventree_extras %}
-{% load status_codes %}
+{% load generic %}
/* globals
addCachedAlert,
diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js
index abeba38d00e2..6c422a3caed8 100644
--- a/InvenTree/templates/js/translated/table_filters.js
+++ b/InvenTree/templates/js/translated/table_filters.js
@@ -1,5 +1,5 @@
{% load i18n %}
-{% load status_codes %}
+{% load generic %}
{% load inventree_extras %}
/* globals
|