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