2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-02 13:28:49 +00:00

Refactor states/status (#4857)

* add file for states

* move general definition out

* add some tests and docs

* add tests for invalid definitions

* make status_label tag generic

* move templatetags

* remove unused tag

* rename test file

* make status label a lookup

* rename tags

* move import structure

* add missing tag

* collect states dynamically

* fix context function

* move api function out

* add tests for tags

* rename tests

* refactor imports

* Add test for API function

* improve errors and add tests for imporved errors

* make test calls simpler

* refactor definitions to use enums

* switch to enum

* refactor definitions to use enums

* fix lookup

* fix tag name

* make _TAG lookup a function

* cleanup BaseEnum

* make _TAG definition simpler

* restructure status codes to enum

* reduce LoC

* type status codes as int

* add specific function for template context

* Add definition for lookups

* fix filter lookup

* TEST: "fix" action lookup

* Add missing migrations

* Make all group code references explict

* change default on models to value

* switch to IntEnum

* move groups into a seperate class

* only request _TAG if it exsists

* use value and list

* use dedicated groups

* fix stock assigment

* fix order code

* more fixes

* fix borked change

* fix render lookup

* add group

* fix import

* fix syntax

* clenup

* fix migrations

* fix typo

* fix wrong value usage

* fix test

* remove group section

* remove group section

* add more test cases

* Add more docstring

* move choices out of migrations

* change import ordeR?

* last try before I revert

* Update part.migrations.0112

- Add custom migration class which handles errors

* Add unit test for migration

- Ensure that the new fields are added to the model

* Update reference to PR

---------

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair 2023-06-09 02:27:26 +02:00 committed by GitHub
parent 005c8341bf
commit 5d1d8ec889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 677 additions and 586 deletions

View File

@ -306,47 +306,6 @@ class APISearchView(APIView):
return Response(results) 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): class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model""" """Generic API endpoint for reading and editing metadata for a model"""

View File

@ -4,10 +4,8 @@
import InvenTree.email import InvenTree.email
import InvenTree.status import InvenTree.status
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, from generic.states import StatusCode
ReturnOrderLineStatus, ReturnOrderStatus, from InvenTree.helpers import inheritors
SalesOrderStatus, StockHistoryCode,
StockStatus)
from users.models import RuleSet, check_user_role from users.models import RuleSet, check_user_role
@ -57,17 +55,7 @@ def status_codes(request):
return {} return {}
request._inventree_status_codes = True request._inventree_status_codes = True
return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
return {
# Expose the StatusCode classes to the templates
'ReturnOrderStatus': ReturnOrderStatus,
'ReturnOrderLineStatus': ReturnOrderLineStatus,
'SalesOrderStatus': SalesOrderStatus,
'PurchaseOrderStatus': PurchaseOrderStatus,
'BuildStatus': BuildStatus,
'StockStatus': StockStatus,
'StockHistoryCode': StockHistoryCode,
}
def user_roles(request): def user_roles(request):

View File

@ -196,6 +196,7 @@ INSTALLED_APPS = [
'stock.apps.StockConfig', 'stock.apps.StockConfig',
'users.apps.UsersConfig', 'users.apps.UsersConfig',
'plugin.apps.PluginAppConfig', 'plugin.apps.PluginAppConfig',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules # Core django modules

View File

@ -2,377 +2,161 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from generic.states import StatusCode
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 "<span class='{cl}'>{value}</span>".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")
class PurchaseOrderStatus(StatusCode): class PurchaseOrderStatus(StatusCode):
"""Defines a set of status codes for a PurchaseOrder.""" """Defines a set of status codes for a PurchaseOrder."""
# Order status codes # Order status codes
PENDING = 10 # Order is pending (not yet placed) PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed)
PLACED = 20 # Order has been placed with supplier PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier
COMPLETE = 30 # Order has been completed COMPLETE = 30, _("Complete"), 'success' # Order has been completed
CANCELLED = 40 # Order was cancelled CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled
LOST = 50 # Order was lost LOST = 50, _("Lost"), 'warning' # Order was lost
RETURNED = 60 # Order was returned RETURNED = 60, _("Returned"), 'warning' # Order was returned
options = {
PENDING: _("Pending"),
PLACED: _("Placed"),
COMPLETE: _("Complete"),
CANCELLED: _("Cancelled"),
LOST: _("Lost"),
RETURNED: _("Returned"),
}
colors = { class PurchaseOrderStatusGroups:
PENDING: 'secondary', """Groups for PurchaseOrderStatus codes."""
PLACED: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
LOST: 'warning',
RETURNED: 'warning',
}
# Open orders # Open orders
OPEN = [ OPEN = [
PENDING, PurchaseOrderStatus.PENDING.value,
PLACED, PurchaseOrderStatus.PLACED.value,
] ]
# Failed orders # Failed orders
FAILED = [ FAILED = [
CANCELLED, PurchaseOrderStatus.CANCELLED.value,
LOST, PurchaseOrderStatus.LOST.value,
RETURNED PurchaseOrderStatus.RETURNED.value
] ]
class SalesOrderStatus(StatusCode): class SalesOrderStatus(StatusCode):
"""Defines a set of status codes for a SalesOrder.""" """Defines a set of status codes for a SalesOrder."""
PENDING = 10 # Order is pending PENDING = 10, _("Pending"), 'secondary' # Order is pending
IN_PROGRESS = 15 # Order has been issued, and is in progress IN_PROGRESS = 15, _("In Progress"), 'primary' # Order has been issued, and is in progress
SHIPPED = 20 # Order has been shipped to customer SHIPPED = 20, _("Shipped"), 'success' # Order has been shipped to customer
CANCELLED = 40 # Order has been cancelled CANCELLED = 40, _("Cancelled"), 'danger' # Order has been cancelled
LOST = 50 # Order was lost LOST = 50, _("Lost"), 'warning' # Order was lost
RETURNED = 60 # Order was returned RETURNED = 60, _("Returned"), 'warning' # Order was returned
options = {
PENDING: _("Pending"),
IN_PROGRESS: _("In Progress"),
SHIPPED: _("Shipped"),
CANCELLED: _("Cancelled"),
LOST: _("Lost"),
RETURNED: _("Returned"),
}
colors = { class SalesOrderStatusGroups:
PENDING: 'secondary', """Groups for SalesOrderStatus codes."""
IN_PROGRESS: 'primary',
SHIPPED: 'success',
CANCELLED: 'danger',
LOST: 'warning',
RETURNED: 'warning',
}
# Open orders # Open orders
OPEN = [ OPEN = [
PENDING, SalesOrderStatus.PENDING.value,
IN_PROGRESS, SalesOrderStatus.IN_PROGRESS.value,
] ]
# Completed orders # Completed orders
COMPLETE = [ COMPLETE = [
SHIPPED, SalesOrderStatus.SHIPPED.value,
] ]
class StockStatus(StatusCode): class StockStatus(StatusCode):
"""Status codes for Stock.""" """Status codes for Stock."""
OK = 10 # Item is OK OK = 10, _("OK"), 'success' # Item is OK
ATTENTION = 50 # Item requires attention ATTENTION = 50, _("Attention needed"), 'warning' # Item requires attention
DAMAGED = 55 # Item is damaged DAMAGED = 55, _("Damaged"), 'warning' # Item is damaged
DESTROYED = 60 # Item is destroyed DESTROYED = 60, _("Destroyed"), 'danger' # Item is destroyed
REJECTED = 65 # Item is rejected REJECTED = 65, _("Rejected"), 'danger' # Item is rejected
LOST = 70 # Item has been lost LOST = 70, _("Lost"), 'dark' # Item has been lost
QUARANTINED = 75 # Item has been quarantined and is unavailable QUARANTINED = 75, _("Quarantined"), 'info' # Item has been quarantined and is unavailable
RETURNED = 85 # Item has been returned from a customer RETURNED = 85, _("Returned"), 'warning' # Item has been returned from a customer
options = {
OK: _("OK"),
ATTENTION: _("Attention needed"),
DAMAGED: _("Damaged"),
DESTROYED: _("Destroyed"),
LOST: _("Lost"),
REJECTED: _("Rejected"),
QUARANTINED: _("Quarantined"),
}
colors = { class StockStatusGroups:
OK: 'success', """Groups for StockStatus codes."""
ATTENTION: 'warning',
DAMAGED: 'warning',
DESTROYED: 'danger',
LOST: 'dark',
REJECTED: 'danger',
QUARANTINED: 'info'
}
# The following codes correspond to parts that are 'available' or 'in stock' # The following codes correspond to parts that are 'available' or 'in stock'
AVAILABLE_CODES = [ AVAILABLE_CODES = [
OK, StockStatus.OK.value,
ATTENTION, StockStatus.ATTENTION.value,
DAMAGED, StockStatus.DAMAGED.value,
RETURNED, StockStatus.RETURNED.value,
] ]
class StockHistoryCode(StatusCode): class StockHistoryCode(StatusCode):
"""Status codes for StockHistory.""" """Status codes for StockHistory."""
LEGACY = 0 LEGACY = 0, _('Legacy stock tracking entry')
CREATED = 1 CREATED = 1, _('Stock item created')
# Manual editing operations # Manual editing operations
EDITED = 5 EDITED = 5, _('Edited stock item')
ASSIGNED_SERIAL = 6 ASSIGNED_SERIAL = 6, _('Assigned serial number')
# Manual stock operations # Manual stock operations
STOCK_COUNT = 10 STOCK_COUNT = 10, _('Stock counted')
STOCK_ADD = 11 STOCK_ADD = 11, _('Stock manually added')
STOCK_REMOVE = 12 STOCK_REMOVE = 12, _('Stock manually removed')
# Location operations # Location operations
STOCK_MOVE = 20 STOCK_MOVE = 20, _('Location changed')
STOCK_UPDATE = 25 STOCK_UPDATE = 25, _('Stock updated')
# Installation operations # Installation operations
INSTALLED_INTO_ASSEMBLY = 30 INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
REMOVED_FROM_ASSEMBLY = 31 REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
INSTALLED_CHILD_ITEM = 35 INSTALLED_CHILD_ITEM = 35, _('Installed component item')
REMOVED_CHILD_ITEM = 36 REMOVED_CHILD_ITEM = 36, _('Removed component item')
# Stock splitting operations # Stock splitting operations
SPLIT_FROM_PARENT = 40 SPLIT_FROM_PARENT = 40, _('Split from parent item')
SPLIT_CHILD_ITEM = 42 SPLIT_CHILD_ITEM = 42, _('Split child item')
# Stock merging operations # Stock merging operations
MERGED_STOCK_ITEMS = 45 MERGED_STOCK_ITEMS = 45, _('Merged stock items')
# Convert stock item to variant # Convert stock item to variant
CONVERTED_TO_VARIANT = 48 CONVERTED_TO_VARIANT = 48, _('Converted to variant')
# Build order codes # Build order codes
BUILD_OUTPUT_CREATED = 50 BUILD_OUTPUT_CREATED = 50, _('Build order output created')
BUILD_OUTPUT_COMPLETED = 55 BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
BUILD_OUTPUT_REJECTED = 56 BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
BUILD_CONSUMED = 57 BUILD_CONSUMED = 57, _('Consumed by build order')
# Sales order codes # Sales order codes
SHIPPED_AGAINST_SALES_ORDER = 60 SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order")
# Purchase order codes # Purchase order codes
RECEIVED_AGAINST_PURCHASE_ORDER = 70 RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
# Return order codes # Return order codes
RETURNED_AGAINST_RETURN_ORDER = 80 RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
# Customer actions # Customer actions
SENT_TO_CUSTOMER = 100 SENT_TO_CUSTOMER = 100, _('Sent to customer')
RETURNED_FROM_CUSTOMER = 105 RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
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'),
}
class BuildStatus(StatusCode): class BuildStatus(StatusCode):
"""Build status codes.""" """Build status codes."""
PENDING = 10 # Build is pending / active PENDING = 10, _("Pending"), 'secondary' # Build is pending / active
PRODUCTION = 20 # BuildOrder is in production PRODUCTION = 20, _("Production"), 'primary' # BuildOrder is in production
CANCELLED = 30 # Build was cancelled CANCELLED = 30, _("Cancelled"), 'danger' # Build was cancelled
COMPLETE = 40 # Build is complete COMPLETE = 40, _("Complete"), 'success' # Build is complete
options = {
PENDING: _("Pending"),
PRODUCTION: _("Production"),
CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"),
}
colors = { class BuildStatusGroups:
PENDING: 'secondary', """Groups for BuildStatus codes."""
PRODUCTION: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
}
ACTIVE_CODES = [ ACTIVE_CODES = [
PENDING, BuildStatus.PENDING.value,
PRODUCTION, BuildStatus.PRODUCTION.value,
] ]
@ -380,68 +164,40 @@ class ReturnOrderStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrder""" """Defines a set of status codes for a ReturnOrder"""
# Order is pending, waiting for receipt of items # Order is pending, waiting for receipt of items
PENDING = 10 PENDING = 10, _("Pending"), 'secondary'
# Items have been received, and are being inspected # Items have been received, and are being inspected
IN_PROGRESS = 20 IN_PROGRESS = 20, _("In Progress"), 'primary'
COMPLETE = 30 COMPLETE = 30, _("Complete"), 'success'
CANCELLED = 40 CANCELLED = 40, _("Cancelled"), 'danger'
class ReturnOrderStatusGroups:
"""Groups for ReturnOrderStatus codes."""
OPEN = [ OPEN = [
PENDING, ReturnOrderStatus.PENDING.value,
IN_PROGRESS, ReturnOrderStatus.IN_PROGRESS.value,
] ]
options = {
PENDING: _("Pending"),
IN_PROGRESS: _("In Progress"),
COMPLETE: _("Complete"),
CANCELLED: _("Cancelled"),
}
colors = {
PENDING: 'secondary',
IN_PROGRESS: 'primary',
COMPLETE: 'success',
CANCELLED: 'danger',
}
class ReturnOrderLineStatus(StatusCode): class ReturnOrderLineStatus(StatusCode):
"""Defines a set of status codes for a ReturnOrderLineItem""" """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 # 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 # Item is to be repaired, and returned to customer
REPAIR = 30 REPAIR = 30, _("Repair"), 'primary'
# Item is to be replaced (new item shipped) # Item is to be replaced (new item shipped)
REPLACE = 40 REPLACE = 40, _("Replace"), 'warning'
# Item is to be refunded (cannot be repaired) # Item is to be refunded (cannot be repaired)
REFUND = 50 REFUND = 50, _("Refund"), 'info'
# Item is rejected # Item is rejected
REJECT = 60 REJECT = 60, _("Reject"), 'danger'
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',
}

View File

@ -9,9 +9,10 @@ from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters 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.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 from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import build.admin import build.admin
@ -41,9 +42,9 @@ class BuildFilter(rest_filters.FilterSet):
def filter_active(self, queryset, name, value): def filter_active(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are active.""" """Filter the queryset to either include or exclude orders which are active."""
if str2bool(value): if str2bool(value):
return queryset.filter(status__in=BuildStatus.ACTIVE_CODES) return queryset.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
else: 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') overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')

View File

@ -21,7 +21,7 @@ from mptt.exceptions import InvalidMove
from rest_framework import serializers 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 from build.validators import generate_next_build_reference, validate_build_order_reference
@ -69,7 +69,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
verbose_name = _("Build Order") verbose_name = _("Build Order")
verbose_name_plural = _("Build Orders") 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 # Global setting for specifying reference pattern
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN' REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
@ -129,10 +129,10 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
return queryset return queryset
# Order was completed within the specified range # 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 # 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 # TODO - Construct a queryset for "overdue" orders
@ -231,7 +231,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
status = models.PositiveIntegerField( status = models.PositiveIntegerField(
verbose_name=_('Build Status'), verbose_name=_('Build Status'),
default=BuildStatus.PENDING, default=BuildStatus.PENDING.value,
choices=BuildStatus.items(), choices=BuildStatus.items(),
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
help_text=_('Build status code') help_text=_('Build status code')
@ -331,7 +331,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
@property @property
def active(self): def active(self):
"""Return True if this build is active.""" """Return True if this build is active."""
return self.status in BuildStatus.ACTIVE_CODES return self.status in BuildStatusGroups.ACTIVE_CODES
@property @property
def bom_items(self): def bom_items(self):
@ -503,7 +503,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
self.completion_date = datetime.now().date() self.completion_date = datetime.now().date()
self.completed_by = user self.completed_by = user
self.status = BuildStatus.COMPLETE self.status = BuildStatus.COMPLETE.value
self.save() self.save()
# Remove untracked allocated stock # Remove untracked allocated stock
@ -585,7 +585,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
self.completion_date = datetime.now().date() self.completion_date = datetime.now().date()
self.completed_by = user self.completed_by = user
self.status = BuildStatus.CANCELLED self.status = BuildStatus.CANCELLED.value
self.save() self.save()
trigger_event('build.cancelled', id=self.pk) trigger_event('build.cancelled', id=self.pk)
@ -729,7 +729,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
_add_tracking_entry(output, user) _add_tracking_entry(output, user)
if self.status == BuildStatus.PENDING: if self.status == BuildStatus.PENDING:
self.status = BuildStatus.PRODUCTION self.status = BuildStatus.PRODUCTION.value
self.save() self.save()
@transaction.atomic @transaction.atomic
@ -831,7 +831,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
# Update build output item # Update build output item
output.is_building = False output.is_building = False
output.status = StockStatus.REJECTED output.status = StockStatus.REJECTED.value
output.location = location output.location = location
output.save(add_note=False) output.save(add_note=False)
@ -851,7 +851,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
notes=notes, notes=notes,
deltas={ deltas={
'location': location.pk, 'location': location.pk,
'status': StockStatus.REJECTED, 'status': StockStatus.REJECTED.value,
'buildorder': self.pk, 'buildorder': self.pk,
} }
) )
@ -865,7 +865,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
""" """
# Select the location for the build output # Select the location for the build output
location = kwargs.get('location', self.destination) location = kwargs.get('location', self.destination)
status = kwargs.get('status', StockStatus.OK) status = kwargs.get('status', StockStatus.OK.value)
notes = kwargs.get('notes', '') notes = kwargs.get('notes', '')
# List the allocated BuildItem objects for the given output # List the allocated BuildItem objects for the given output
@ -1187,7 +1187,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
- PENDING - PENDING
- HOLDING - HOLDING
""" """
return self.status in BuildStatus.ACTIVE_CODES return self.status in BuildStatusGroups.ACTIVE_CODES
@property @property
def is_complete(self): def is_complete(self):

View File

@ -490,8 +490,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
) )
status = serializers.ChoiceField( status = serializers.ChoiceField(
choices=list(StockStatus.items()), choices=StockStatus.items(),
default=StockStatus.OK, default=StockStatus.OK.value,
label=_("Status"), label=_("Status"),
) )

View File

@ -15,7 +15,7 @@ import build.models
import InvenTree.email import InvenTree.email
import InvenTree.helpers_model import InvenTree.helpers_model
import InvenTree.tasks import InvenTree.tasks
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatusGroups
from InvenTree.ready import isImportingData from InvenTree.ready import isImportingData
import part.models as part_models import part.models as part_models
@ -158,7 +158,7 @@ def check_overdue_build_orders():
overdue_orders = build.models.Build.objects.filter( overdue_orders = build.models.Build.objects.filter(
target_date=yesterday, target_date=yesterday,
status__in=BuildStatus.ACTIVE_CODES status__in=BuildStatusGroups.ACTIVE_CODES
) )
for bo in overdue_orders: for bo in overdue_orders:

View File

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load status_codes %} {% load generic %}
{% load inventree_extras %} {% load inventree_extras %}
{% block page_title %} {% block page_title %}
@ -150,7 +150,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td> <td>{% trans "Status" %}</td>
<td> <td>
{% build_status_label build.status %} {% status_label 'build' build.status %}
</td> </td>
</tr> </tr>
{% if build.target_date %} {% if build.target_date %}
@ -217,7 +217,7 @@ src="{% static 'img/blank_image.png' %}"
{% block page_data %} {% block page_data %}
<h3> <h3>
{% build_status_label build.status large=True %} {% status_label 'build' build.status large=True %}
{% if build.is_overdue %} {% if build.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}

View File

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% block sidebar %} {% block sidebar %}
{% include "build/sidebar.html" %} {% include "build/sidebar.html" %}
@ -60,7 +60,7 @@
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td> <td>{% trans "Status" %}</td>
<td>{% build_status_label build.status %}</td> <td>{% status_label 'build' build.status %}</td>
</tr> </tr>
<tr> <tr>
<td><span class='fas fa-check-circle'></span></td> <td><span class='fas fa-check-circle'></span></td>

View File

@ -298,7 +298,7 @@ class BuildTest(BuildAPITest):
expected_code=400, expected_code=400,
) )
bo.status = BuildStatus.CANCELLED bo.status = BuildStatus.CANCELLED.value
bo.save() bo.save()
# Now, we should be able to delete # Now, we should be able to delete
@ -843,7 +843,7 @@ class BuildListTest(BuildAPITest):
builds = self.get(self.url, data={'active': True}) builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1) 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) self.assertEqual(len(builds.data), 4)
builds = self.get(self.url, data={'overdue': False}) builds = self.get(self.url, data={'overdue': False})
@ -863,7 +863,7 @@ class BuildListTest(BuildAPITest):
reference="BO-0006", reference="BO-0006",
quantity=10, quantity=10,
title='Just some thing', title='Just some thing',
status=BuildStatus.PRODUCTION, status=BuildStatus.PRODUCTION.value,
target_date=in_the_past target_date=in_the_past
) )

View File

@ -30,7 +30,7 @@ from common.settings import currency_code_default
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin, from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, MetadataMixin) InvenTreeNotesMixin, MetadataMixin)
from InvenTree.status_codes import PurchaseOrderStatus from InvenTree.status_codes import PurchaseOrderStatusGroups
def rename_company_image(instance, filename): def rename_company_image(instance, filename):
@ -697,7 +697,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
def open_orders(self): def open_orders(self):
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding.""" """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): def on_order(self):
"""Return the total quantity of items currently on order. """Return the total quantity of items currently on order.

View File

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

View File

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

View File

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

View File

@ -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"<span class='badge rounded-pill bg-{item.color}'>{item.label}</span>"
@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'(?<!^)(?=[A-Z])', '_', ref_name).lower()
@classmethod
def items(cls):
"""All status code items."""
return [(x.value, x.label) for x in cls.values()]
@classmethod
def keys(cls):
"""All status code keys."""
return [x.value for x in cls.values()]
@classmethod
def labels(cls):
"""All status code labels."""
return [x.label for x in cls.values()]
@classmethod
def names(cls):
"""Return a map of all 'names' of status codes in this class."""
return {x.name: x.value for x in cls.values()}
@classmethod
def text(cls, key):
"""Text for supplied status code."""
filtered = cls.values(key)
if filtered is None:
return key
return filtered.label
@classmethod
def label(cls, key):
"""Return the status code label associated with the provided value."""
filtered = cls.values(key)
if filtered is None:
return key
return filtered.label
@classmethod
def dict(cls, key=None):
"""Return a dict representation containing all required information"""
return {x.name: {
'color': x.color,
'key': x.value,
'label': x.label,
'name': x.name,
} for x in cls.values(key)}
@classmethod
def list(cls):
"""Return the StatusCode options as a list of mapped key / value items."""
return list(cls.dict().values())
@classmethod
def template_context(cls):
"""Return a dict representation containing all required information for templates."""
ret = {x.name: x.value for x in cls.values()}
ret['list'] = cls.list()
return ret

View File

@ -0,0 +1,17 @@
"""Provide templates for the various model status codes."""
from django.utils.safestring import mark_safe
from generic.templatetags.generic import register
from InvenTree.helpers import inheritors
from .states import StatusCode
@register.simple_tag
def status_label(typ: str, key: int, *args, **kwargs):
"""Render a status label."""
state = {cls.tag(): cls for cls in inheritors(StatusCode)}.get(typ, None)
if state:
return mark_safe(state.render(key, large=kwargs.get('large', False)))
raise ValueError(f"Unknown status type '{typ}'")

View File

@ -0,0 +1,110 @@
"""Tests for the generic states module."""
from django.test.client import RequestFactory
from django.utils.translation import gettext_lazy as _
from rest_framework.test import force_authenticate
from InvenTree.unit_test import InvenTreeTestCase
from .api import StatusView
from .states import StatusCode
class GeneralStatus(StatusCode):
"""Defines a set of status codes for tests."""
PENDING = 10, _("Pending"), 'secondary'
PLACED = 20, _("Placed"), 'primary'
COMPLETE = 30, _("Complete"), 'success'
ABC = None # This should be ignored
_DEF = None # This should be ignored
jkl = None # This should be ignored
def GHI(self): # This should be ignored
"""A invalid function"""
pass
class GeneralStateTest(InvenTreeTestCase):
"""Test that the StatusCode class works."""
def test_code_definition(self):
"""Test that the status code class has been defined correctly."""
self.assertEqual(GeneralStatus.PENDING, 10)
self.assertEqual(GeneralStatus.PLACED, 20)
self.assertEqual(GeneralStatus.COMPLETE, 30)
def test_code_functions(self):
"""Test that the status code class functions work correctly"""
# render
self.assertEqual(GeneralStatus.render(10), "<span class='badge rounded-pill bg-secondary'>Pending</span>")
self.assertEqual(GeneralStatus.render(20), "<span class='badge rounded-pill bg-primary'>Placed</span>")
# 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), "<span class='badge rounded-pill bg-secondary'>Pending</span>")
# 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")

View File

@ -0,0 +1 @@
"""Template tags for generic *things*."""

View File

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

View File

@ -16,15 +16,18 @@ from rest_framework.response import Response
from common.models import InvenTreeSetting, ProjectCode from common.models import InvenTreeSetting, ProjectCode
from common.settings import settings from common.settings import settings
from company.models import SupplierPart from company.models import SupplierPart
from generic.states import StatusView
from InvenTree.api import (APIDownloadMixin, AttachmentMixin, from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView, MetadataView, StatusView) ListCreateDestroyAPIView, MetadataView)
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
RetrieveUpdateDestroyAPI) RetrieveUpdateDestroyAPI)
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, from InvenTree.status_codes import (PurchaseOrderStatus,
ReturnOrderStatus, SalesOrderStatus) PurchaseOrderStatusGroups,
ReturnOrderLineStatus, ReturnOrderStatus,
SalesOrderStatus, SalesOrderStatusGroups)
from order import models, serializers from order import models, serializers
from order.admin import (PurchaseOrderExtraLineResource, from order.admin import (PurchaseOrderExtraLineResource,
PurchaseOrderLineItemResource, PurchaseOrderResource, PurchaseOrderLineItemResource, PurchaseOrderResource,
@ -431,9 +434,9 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
"""Filter by "pending" status (order status = pending)""" """Filter by "pending" status (order status = pending)"""
if str2bool(value): if str2bool(value):
return queryset.filter(order__status__in=PurchaseOrderStatus.OPEN) return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
else: 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') received = rest_filters.BooleanFilter(label='received', method='filter_received')
@ -448,7 +451,7 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
return queryset.filter(q) return queryset.filter(q)
else: else:
# Only count "pending" orders # 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: class PurchaseOrderLineItemMixin:
@ -984,12 +987,12 @@ class SalesOrderAllocationList(ListAPI):
# Filter only "open" orders # Filter only "open" orders
# Filter only allocations which have *not* shipped # Filter only allocations which have *not* shipped
queryset = queryset.filter( queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None, shipment__shipment_date=None,
) )
else: else:
queryset = queryset.exclude( queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None shipment__shipment_date=None
) )
@ -1471,21 +1474,21 @@ class OrderCalendarExport(ICalFeed):
if obj['include_completed'] is False: if obj['include_completed'] is False:
# Do not include completed orders from list in this case # Do not include completed orders from list in this case
# Completed status = 30 # 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: else:
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False) outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'sales-order': elif obj["ordertype"] == 'sales-order':
if obj['include_completed'] is False: if obj['include_completed'] is False:
# Do not include completed (=shipped) orders from list in this case # Do not include completed (=shipped) orders from list in this case
# Shipped status = 20 # 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: else:
outlist = models.SalesOrder.objects.filter(target_date__isnull=False) outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
elif obj["ordertype"] == 'return-order': elif obj["ordertype"] == 'return-order':
if obj['include_completed'] is False: if obj['include_completed'] is False:
# Do not include completed orders from list in this case # Do not include completed orders from list in this case
# Complete status = 30 # 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: else:
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False) outlist = models.ReturnOrder.objects.filter(target_date__isnull=False)
else: else:

View File

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

View File

@ -41,9 +41,12 @@ from InvenTree.helpers_model import getSetting, notify_responsible
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin, from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, MetadataMixin, InvenTreeNotesMixin, MetadataMixin,
ReferenceIndexingMixin) ReferenceIndexingMixin)
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, from InvenTree.status_codes import (PurchaseOrderStatus,
ReturnOrderStatus, SalesOrderStatus, PurchaseOrderStatusGroups,
StockHistoryCode, StockStatus) ReturnOrderLineStatus, ReturnOrderStatus,
ReturnOrderStatusGroups, SalesOrderStatus,
SalesOrderStatusGroups, StockHistoryCode,
StockStatus)
from part import models as PartModels from part import models as PartModels
from plugin.events import trigger_event from plugin.events import trigger_event
@ -294,7 +297,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
@classmethod @classmethod
def get_status_class(cls): def get_status_class(cls):
"""Return the PurchasOrderStatus class""" """Return the PurchasOrderStatus class"""
return PurchaseOrderStatus return PurchaseOrderStatusGroups
@classmethod @classmethod
def api_defaults(cls, request): def api_defaults(cls, request):
@ -333,10 +336,10 @@ class PurchaseOrder(TotalPriceMixin, Order):
return queryset return queryset
# Construct a queryset for "received" orders within the range # 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 # 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 # TODO - Construct a queryset for "overdue" orders within the range
@ -361,7 +364,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
] ]
) )
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING.value, choices=PurchaseOrderStatus.items(),
help_text=_('Purchase order status')) help_text=_('Purchase order status'))
@property @property
@ -479,7 +482,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
Order must be currently PENDING. Order must be currently PENDING.
""" """
if self.status == PurchaseOrderStatus.PENDING: if self.status == PurchaseOrderStatus.PENDING:
self.status = PurchaseOrderStatus.PLACED self.status = PurchaseOrderStatus.PLACED.value
self.issue_date = datetime.now().date() self.issue_date = datetime.now().date()
self.save() self.save()
@ -500,7 +503,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
Order must be currently PLACED. Order must be currently PLACED.
""" """
if self.status == PurchaseOrderStatus.PLACED: if self.status == PurchaseOrderStatus.PLACED:
self.status = PurchaseOrderStatus.COMPLETE self.status = PurchaseOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date() self.complete_date = datetime.now().date()
self.save() self.save()
@ -520,7 +523,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
@property @property
def is_open(self): def is_open(self):
"""Return True if the PurchaseOrder is 'open'""" """Return True if the PurchaseOrder is 'open'"""
return self.status in PurchaseOrderStatus.OPEN return self.status in PurchaseOrderStatusGroups.OPEN
def can_cancel(self): def can_cancel(self):
"""A PurchaseOrder can only be cancelled under the following circumstances. """A PurchaseOrder can only be cancelled under the following circumstances.
@ -537,7 +540,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
def cancel_order(self): def cancel_order(self):
"""Marks the PurchaseOrder as CANCELLED.""" """Marks the PurchaseOrder as CANCELLED."""
if self.can_cancel(): if self.can_cancel():
self.status = PurchaseOrderStatus.CANCELLED self.status = PurchaseOrderStatus.CANCELLED.value
self.save() self.save()
trigger_event('purchaseorder.cancelled', id=self.pk) trigger_event('purchaseorder.cancelled', id=self.pk)
@ -574,7 +577,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
return self.lines.count() > 0 and self.pending_line_items().count() == 0 return self.lines.count() > 0 and self.pending_line_items().count() == 0
@transaction.atomic @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.""" """Receive a line item (or partial line item) against this PurchaseOrder."""
# Extract optional batch code for the new stock item # Extract optional batch code for the new stock item
batch_code = kwargs.get('batch_code', '') batch_code = kwargs.get('batch_code', '')
@ -701,7 +704,7 @@ class SalesOrder(TotalPriceMixin, Order):
@classmethod @classmethod
def get_status_class(cls): def get_status_class(cls):
"""Return the SalesOrderStatus class""" """Return the SalesOrderStatus class"""
return SalesOrderStatus return SalesOrderStatusGroups
@classmethod @classmethod
def api_defaults(cls, request): def api_defaults(cls, request):
@ -739,10 +742,10 @@ class SalesOrder(TotalPriceMixin, Order):
return queryset return queryset
# Construct a queryset for "completed" orders within the range # 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 # 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 # TODO: Construct a queryset for "overdue" orders within the range
@ -783,7 +786,7 @@ class SalesOrder(TotalPriceMixin, Order):
return self.customer return self.customer
status = models.PositiveIntegerField( status = models.PositiveIntegerField(
default=SalesOrderStatus.PENDING, default=SalesOrderStatus.PENDING.value,
choices=SalesOrderStatus.items(), choices=SalesOrderStatus.items(),
verbose_name=_('Status'), help_text=_('Purchase order status') verbose_name=_('Status'), help_text=_('Purchase order status')
) )
@ -813,7 +816,7 @@ class SalesOrder(TotalPriceMixin, Order):
@property @property
def is_open(self): def is_open(self):
"""Return True if this order is 'open' (either 'pending' or 'in_progress')""" """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 @property
def stock_allocations(self): def stock_allocations(self):
@ -881,7 +884,7 @@ class SalesOrder(TotalPriceMixin, Order):
"""Change this order from 'PENDING' to 'IN_PROGRESS'""" """Change this order from 'PENDING' to 'IN_PROGRESS'"""
if self.status == SalesOrderStatus.PENDING: if self.status == SalesOrderStatus.PENDING:
self.status = SalesOrderStatus.IN_PROGRESS self.status = SalesOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date() self.issue_date = datetime.now().date()
self.save() self.save()
@ -892,7 +895,7 @@ class SalesOrder(TotalPriceMixin, Order):
if not self.can_complete(**kwargs): if not self.can_complete(**kwargs):
return False return False
self.status = SalesOrderStatus.SHIPPED self.status = SalesOrderStatus.SHIPPED.value
self.shipped_by = user self.shipped_by = user
self.shipment_date = datetime.now() self.shipment_date = datetime.now()
@ -921,7 +924,7 @@ class SalesOrder(TotalPriceMixin, Order):
if not self.can_cancel(): if not self.can_cancel():
return False return False
self.status = SalesOrderStatus.CANCELLED self.status = SalesOrderStatus.CANCELLED.value
self.save() self.save()
for line in self.lines.all(): for line in self.lines.all():
@ -1696,7 +1699,7 @@ class ReturnOrder(TotalPriceMixin, Order):
@classmethod @classmethod
def get_status_class(cls): def get_status_class(cls):
"""Return the ReturnOrderStatus class""" """Return the ReturnOrderStatus class"""
return ReturnOrderStatus return ReturnOrderStatusGroups
@classmethod @classmethod
def api_defaults(cls, request): def api_defaults(cls, request):
@ -1742,7 +1745,7 @@ class ReturnOrder(TotalPriceMixin, Order):
return self.customer return self.customer
status = models.PositiveIntegerField( status = models.PositiveIntegerField(
default=ReturnOrderStatus.PENDING, default=ReturnOrderStatus.PENDING.value,
choices=ReturnOrderStatus.items(), choices=ReturnOrderStatus.items(),
verbose_name=_('Status'), help_text=_('Return order status') verbose_name=_('Status'), help_text=_('Return order status')
) )
@ -1773,7 +1776,7 @@ class ReturnOrder(TotalPriceMixin, Order):
@property @property
def is_open(self): def is_open(self):
"""Return True if this order is outstanding""" """Return True if this order is outstanding"""
return self.status in ReturnOrderStatus.OPEN return self.status in ReturnOrderStatusGroups.OPEN
@property @property
def is_received(self): def is_received(self):
@ -1784,7 +1787,7 @@ class ReturnOrder(TotalPriceMixin, Order):
def cancel_order(self): def cancel_order(self):
"""Cancel this ReturnOrder (if not already cancelled)""" """Cancel this ReturnOrder (if not already cancelled)"""
if self.status != ReturnOrderStatus.CANCELLED: if self.status != ReturnOrderStatus.CANCELLED:
self.status = ReturnOrderStatus.CANCELLED self.status = ReturnOrderStatus.CANCELLED.value
self.save() self.save()
trigger_event('returnorder.cancelled', id=self.pk) trigger_event('returnorder.cancelled', id=self.pk)
@ -1794,7 +1797,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Complete this ReturnOrder (if not already completed)""" """Complete this ReturnOrder (if not already completed)"""
if self.status == ReturnOrderStatus.IN_PROGRESS: if self.status == ReturnOrderStatus.IN_PROGRESS:
self.status = ReturnOrderStatus.COMPLETE self.status = ReturnOrderStatus.COMPLETE.value
self.complete_date = datetime.now().date() self.complete_date = datetime.now().date()
self.save() self.save()
@ -1809,7 +1812,7 @@ class ReturnOrder(TotalPriceMixin, Order):
"""Issue this ReturnOrder (if currently pending)""" """Issue this ReturnOrder (if currently pending)"""
if self.status == ReturnOrderStatus.PENDING: if self.status == ReturnOrderStatus.PENDING:
self.status = ReturnOrderStatus.IN_PROGRESS self.status = ReturnOrderStatus.IN_PROGRESS.value
self.issue_date = datetime.now().date() self.issue_date = datetime.now().date()
self.save() self.save()
@ -1833,7 +1836,7 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item = line.item stock_item = line.item
deltas = { deltas = {
'status': StockStatus.QUARANTINED, 'status': StockStatus.QUARANTINED.value,
'returnorder': self.pk, 'returnorder': self.pk,
'location': location.pk, 'location': location.pk,
} }
@ -1842,7 +1845,7 @@ class ReturnOrder(TotalPriceMixin, Order):
deltas['customer'] = stock_item.customer.pk deltas['customer'] = stock_item.customer.pk
# Update the StockItem # Update the StockItem
stock_item.status = StockStatus.QUARANTINED stock_item.status = StockStatus.QUARANTINED.value
stock_item.location = location stock_item.location = location
stock_item.customer = None stock_item.customer = None
stock_item.sales_order = None stock_item.sales_order = None
@ -1926,7 +1929,7 @@ class ReturnOrderLineItem(OrderLineItem):
return self.received_date is not None return self.received_date is not None
outcome = models.PositiveIntegerField( outcome = models.PositiveIntegerField(
default=ReturnOrderLineStatus.PENDING, default=ReturnOrderLineStatus.PENDING.value,
choices=ReturnOrderLineStatus.items(), choices=ReturnOrderLineStatus.items(),
verbose_name=_('Outcome'), help_text=_('Outcome for this line item') verbose_name=_('Outcome'), help_text=_('Outcome for this line item')
) )

View File

@ -27,8 +27,9 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer,
InvenTreeDecimalField, InvenTreeDecimalField,
InvenTreeModelSerializer, InvenTreeModelSerializer,
InvenTreeMoneySerializer) InvenTreeMoneySerializer)
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderStatus, from InvenTree.status_codes import (PurchaseOrderStatusGroups,
SalesOrderStatus, StockStatus) ReturnOrderStatus, SalesOrderStatusGroups,
StockStatus)
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
from users.serializers import OwnerSerializer from users.serializers import OwnerSerializer
@ -381,7 +382,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
def validate_purchase_order(self, purchase_order): def validate_purchase_order(self, purchase_order):
"""Validation for the 'purchase_order' field""" """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')) raise ValidationError(_('Order is not open'))
return purchase_order return purchase_order
@ -518,8 +519,8 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
) )
status = serializers.ChoiceField( status = serializers.ChoiceField(
choices=list(StockStatus.items()), choices=StockStatus.items(),
default=StockStatus.OK, default=StockStatus.OK.value,
label=_('Status'), label=_('Status'),
) )
@ -906,7 +907,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
queryset = queryset.annotate( queryset = queryset.annotate(
overdue=Case( overdue=Case(
When( 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()), default=Value(False, output_field=BooleanField()),
) )

View File

@ -7,7 +7,8 @@ from django.utils.translation import gettext_lazy as _
import common.notifications import common.notifications
import InvenTree.helpers_model import InvenTree.helpers_model
import order.models 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 InvenTree.tasks import ScheduledTask, scheduled_task
from plugin.events import trigger_event from plugin.events import trigger_event
@ -68,7 +69,7 @@ def check_overdue_purchase_orders():
overdue_orders = order.models.PurchaseOrder.objects.filter( overdue_orders = order.models.PurchaseOrder.objects.filter(
target_date=yesterday, target_date=yesterday,
status__in=PurchaseOrderStatus.OPEN status__in=PurchaseOrderStatusGroups.OPEN,
) )
for po in overdue_orders: for po in overdue_orders:
@ -131,7 +132,7 @@ def check_overdue_sales_orders():
overdue_orders = order.models.SalesOrder.objects.filter( overdue_orders = order.models.SalesOrder.objects.filter(
target_date=yesterday, target_date=yesterday,
status__in=SalesOrderStatus.OPEN status__in=SalesOrderStatusGroups.OPEN,
) )
for po in overdue_orders: for po in overdue_orders:

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% block page_title %} {% block page_title %}
{% inventree_title %} | {% trans "Purchase Order" %} {% inventree_title %} | {% trans "Purchase Order" %}
@ -121,7 +121,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
<td> <td>
{% purchase_order_status_label order.status %} {% status_label 'purchase_order' order.status %}
{% if order.is_overdue %} {% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% extends "order/order_base.html" %} {% extends "order/order_base.html" %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% block page_title %} {% block page_title %}
{% inventree_title %} | {% trans "Return Order" %} {% inventree_title %} | {% trans "Return Order" %}
@ -113,7 +113,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
<td> <td>
{% return_order_status_label order.status %} {% status_label 'return_order' order.status %}
{% if order.is_overdue %} {% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% extends "order/return_order_base.html" %} {% extends "order/return_order_base.html" %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}

View File

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% block page_title %} {% block page_title %}
{% inventree_title %} | {% trans "Sales Order" %} {% inventree_title %} | {% trans "Sales Order" %}
@ -118,7 +118,7 @@ src="{% static 'img/blank_image.png' %}"
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Order Status" %}</td> <td>{% trans "Order Status" %}</td>
<td> <td>
{% sales_order_status_label order.status %} {% status_label 'sales_order' order.status %}
{% if order.is_overdue %} {% if order.is_overdue %}
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span> <span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
{% endif %} {% endif %}

View File

@ -1,7 +1,7 @@
{% extends "order/sales_order_base.html" %} {% extends "order/sales_order_base.html" %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}

View File

@ -17,7 +17,7 @@ from common.settings import currency_codes
from company.models import Company from company.models import Company
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus, from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
ReturnOrderStatus, SalesOrderStatus, ReturnOrderStatus, SalesOrderStatus,
StockStatus) SalesOrderStatusGroups, StockStatus)
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from order import models from order import models
from part.models import Part from part.models import Part
@ -562,7 +562,7 @@ class PurchaseOrderTest(OrderTest):
# Test without completed orders # Test without completed orders
response = self.get(url, expected_code=200, format=None) 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 # Transform content to a Calendar object
calendar = Calendar.from_ical(response.content) calendar = Calendar.from_ical(response.content)
@ -743,7 +743,7 @@ class PurchaseOrderReceiveTest(OrderTest):
# Mark the order as "placed" so we can receive line items # Mark the order as "placed" so we can receive line items
order = models.PurchaseOrder.objects.get(pk=1) order = models.PurchaseOrder.objects.get(pk=1)
order.status = PurchaseOrderStatus.PLACED order.status = PurchaseOrderStatus.PLACED.value
order.save() order.save()
def test_empty(self): def test_empty(self):
@ -944,7 +944,7 @@ class PurchaseOrderReceiveTest(OrderTest):
# Before posting "valid" data, we will mark the purchase order as "pending" # Before posting "valid" data, we will mark the purchase order as "pending"
# In this case we do expect an error! # In this case we do expect an error!
order = models.PurchaseOrder.objects.get(pk=1) order = models.PurchaseOrder.objects.get(pk=1)
order.status = PurchaseOrderStatus.PENDING order.status = PurchaseOrderStatus.PENDING.value
order.save() order.save()
response = self.post( response = self.post(
@ -956,7 +956,7 @@ class PurchaseOrderReceiveTest(OrderTest):
self.assertIn('can only be received against', str(response.data)) self.assertIn('can only be received against', str(response.data))
# Now, set the PurchaseOrder back to "PLACED" so the items can be received # Now, set the PurchaseOrder back to "PLACED" so the items can be received
order.status = PurchaseOrderStatus.PLACED order.status = PurchaseOrderStatus.PLACED.value
order.save() order.save()
# Receive two separate line items against this order # Receive two separate line items against this order
@ -1388,7 +1388,7 @@ class SalesOrderTest(OrderTest):
# Test without completed orders # Test without completed orders
response = self.get(url, expected_code=200, format=None) 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 # Transform content to a Calendar object
calendar = Calendar.from_ical(response.content) calendar = Calendar.from_ical(response.content)
@ -1621,7 +1621,7 @@ class SalesOrderDownloadTest(OrderTest):
file, file,
required_cols=required_cols, required_cols=required_cols,
excluded_cols=excluded_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', delimiter='\t',
) )

View File

@ -109,7 +109,7 @@ class TestShipmentMigration(MigratorTestCase):
reference=f'SO{ii}', reference=f'SO{ii}',
customer=customer, customer=customer,
description='A sales order for stuffs', description='A sales order for stuffs',
status=SalesOrderStatus.PENDING, status=SalesOrderStatus.PENDING.value,
) )
order.save() order.save()

View File

@ -29,8 +29,9 @@ from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI, RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
UpdateAPI) UpdateAPI)
from InvenTree.permissions import RolePermission from InvenTree.permissions import RolePermission
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, from InvenTree.status_codes import (BuildStatusGroups,
SalesOrderStatus) PurchaseOrderStatusGroups,
SalesOrderStatusGroups)
from part.admin import PartCategoryResource, PartResource from part.admin import PartCategoryResource, PartResource
from . import serializers as part_serializers from . import serializers as part_serializers
@ -479,7 +480,7 @@ class PartScheduling(RetrieveAPI):
# Add purchase order (incoming stock) information # Add purchase order (incoming stock) information
po_lines = order.models.PurchaseOrderLineItem.objects.filter( po_lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part=part, part__part=part,
order__status__in=PurchaseOrderStatus.OPEN, order__status__in=PurchaseOrderStatusGroups.OPEN,
) )
for line in po_lines: for line in po_lines:
@ -502,7 +503,7 @@ class PartScheduling(RetrieveAPI):
# Add sales order (outgoing stock) information # Add sales order (outgoing stock) information
so_lines = order.models.SalesOrderLineItem.objects.filter( so_lines = order.models.SalesOrderLineItem.objects.filter(
part=part, part=part,
order__status__in=SalesOrderStatus.OPEN, order__status__in=SalesOrderStatusGroups.OPEN,
) )
for line in so_lines: for line in so_lines:
@ -522,7 +523,7 @@ class PartScheduling(RetrieveAPI):
# Add build orders (incoming stock) information # Add build orders (incoming stock) information
build_orders = Build.objects.filter( build_orders = Build.objects.filter(
part=part, part=part,
status__in=BuildStatus.ACTIVE_CODES status__in=BuildStatusGroups.ACTIVE_CODES
) )
for build in build_orders: for build in build_orders:
@ -567,12 +568,12 @@ class PartScheduling(RetrieveAPI):
# An "inherited" BOM item filters down to variant parts also # An "inherited" BOM item filters down to variant parts also
children = bom_item.part.get_descendants(include_self=True) children = bom_item.part.get_descendants(include_self=True)
builds = Build.objects.filter( builds = Build.objects.filter(
status__in=BuildStatus.ACTIVE_CODES, status__in=BuildStatusGroups.ACTIVE_CODES,
part__in=children, part__in=children,
) )
else: else:
builds = Build.objects.filter( builds = Build.objects.filter(
status__in=BuildStatus.ACTIVE_CODES, status__in=BuildStatusGroups.ACTIVE_CODES,
part=bom_item.part, part=bom_item.part,
) )
@ -1197,7 +1198,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
if stock_to_build is not None: if stock_to_build is not None:
# Get active builds # 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 # Store parts with builds needing stock
parts_needed_to_complete_builds = [] parts_needed_to_complete_builds = []
# Filter required parts # Filter required parts

View File

@ -28,8 +28,9 @@ from sql_util.utils import SubquerySum
import part.models import part.models
import stock.models import stock.models
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, from InvenTree.status_codes import (BuildStatusGroups,
SalesOrderStatus) PurchaseOrderStatusGroups,
SalesOrderStatusGroups)
def annotate_on_order_quantity(reference: str = ''): 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 'active' purhase orders
# Filter only line with outstanding quantity # Filter only line with outstanding quantity
order_filter = Q( order_filter = Q(
order__status__in=PurchaseOrderStatus.OPEN, order__status__in=PurchaseOrderStatusGroups.OPEN,
quantity__gt=F('received'), quantity__gt=F('received'),
) )
@ -111,7 +112,7 @@ def annotate_build_order_allocations(reference: str = ''):
""" """
# Build filter only returns 'active' build orders # 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( return Coalesce(
SubquerySum( SubquerySum(
@ -137,7 +138,7 @@ def annotate_sales_order_allocations(reference: str = ''):
# Order filter only returns incomplete shipments for open orders # Order filter only returns incomplete shipments for open orders
order_filter = Q( order_filter = Q(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None, shipment__shipment_date=None,
) )

View File

@ -51,8 +51,9 @@ from InvenTree.helpers import (decimal2money, decimal2string, normalize,
from InvenTree.models import (DataImportMixin, InvenTreeAttachment, from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
InvenTreeBarcodeMixin, InvenTreeNotesMixin, InvenTreeBarcodeMixin, InvenTreeNotesMixin,
InvenTreeTree, MetadataMixin) InvenTreeTree, MetadataMixin)
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, from InvenTree.status_codes import (BuildStatusGroups, PurchaseOrderStatus,
SalesOrderStatus) PurchaseOrderStatusGroups,
SalesOrderStatus, SalesOrderStatusGroups)
from order import models as OrderModels from order import models as OrderModels
from stock import models as StockModels from stock import models as StockModels
@ -1070,7 +1071,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
# Now, get a list of outstanding build orders which require this part # Now, get a list of outstanding build orders which require this part
builds = BuildModels.Build.objects.filter( builds = BuildModels.Build.objects.filter(
part__in=self.get_used_in(), part__in=self.get_used_in(),
status__in=BuildStatus.ACTIVE_CODES status__in=BuildStatusGroups.ACTIVE_CODES
) )
return builds return builds
@ -1104,7 +1105,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
# Get a list of line items for open orders which match this part # Get a list of line items for open orders which match this part
open_lines = OrderModels.SalesOrderLineItem.objects.filter( open_lines = OrderModels.SalesOrderLineItem.objects.filter(
order__status__in=SalesOrderStatus.OPEN, order__status__in=SalesOrderStatusGroups.OPEN,
part=self part=self
) )
@ -1117,7 +1118,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
"""Return the quantity of this part required for active sales orders.""" """Return the quantity of this part required for active sales orders."""
# Get a list of line items for open orders which match this part # Get a list of line items for open orders which match this part
open_lines = OrderModels.SalesOrderLineItem.objects.filter( open_lines = OrderModels.SalesOrderLineItem.objects.filter(
order__status__in=SalesOrderStatus.OPEN, order__status__in=SalesOrderStatusGroups.OPEN,
part=self part=self
) )
@ -1329,7 +1330,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
Builds marked as 'complete' or 'cancelled' are ignored 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 @property
def quantity_being_built(self): def quantity_being_built(self):
@ -1401,13 +1402,13 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
if pending is True: if pending is True:
# Look only for 'open' orders which have not shipped # Look only for 'open' orders which have not shipped
queryset = queryset.filter( queryset = queryset.filter(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None, shipment__shipment_date=None,
) )
elif pending is False: elif pending is False:
# Look only for 'closed' orders or orders which have shipped # Look only for 'closed' orders or orders which have shipped
queryset = queryset.exclude( queryset = queryset.exclude(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None, shipment__shipment_date=None,
) )
@ -2161,7 +2162,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
# Look at any incomplete line item for open orders # Look at any incomplete line item for open orders
lines = sp.purchase_order_line_items.filter( lines = sp.purchase_order_line_items.filter(
order__status__in=PurchaseOrderStatus.OPEN, order__status__in=PurchaseOrderStatusGroups.OPEN,
quantity__gt=F('received'), quantity__gt=F('received'),
) )
@ -2559,7 +2560,7 @@ class PartPricing(common.models.MetaMixin):
# Find all line items for completed orders which reference this part # Find all line items for completed orders which reference this part
line_items = OrderModels.PurchaseOrderLineItem.objects.filter( line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
order__status=PurchaseOrderStatus.COMPLETE, order__status=PurchaseOrderStatus.COMPLETE.value,
received__gt=0, received__gt=0,
part__part=self.part, part__part=self.part,
) )

View File

@ -25,7 +25,7 @@ import InvenTree.status
import part.filters import part.filters
import part.tasks import part.tasks
import stock.models import stock.models
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatusGroups
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment, from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
@ -532,7 +532,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
# Filter to limit builds to "active" # Filter to limit builds to "active"
build_filter = Q( build_filter = Q(
status__in=BuildStatus.ACTIVE_CODES status__in=BuildStatusGroups.ACTIVE_CODES
) )
# Annotate with the total 'building' quantity # Annotate with the total 'building' quantity

View File

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

View File

@ -18,7 +18,7 @@ import company.models
import order.models import order.models
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus, from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatusGroups,
StockStatus) StockStatus)
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import (BomItem, BomItemSubstitute, Part, PartCategory, from part.models import (BomItem, BomItemSubstitute, Part, PartCategory,
@ -1628,7 +1628,7 @@ class PartDetailTests(PartAPITestBase):
# How many parts are 'on order' for this part? # How many parts are 'on order' for this part?
lines = order.models.PurchaseOrderLineItem.objects.filter( lines = order.models.PurchaseOrderLineItem.objects.filter(
part__part__pk=1, part__part__pk=1,
order__status__in=PurchaseOrderStatus.OPEN, order__status__in=PurchaseOrderStatusGroups.OPEN,
) )
on_order = 0 on_order = 0
@ -1857,7 +1857,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
StockItem.objects.create(part=cls.part, quantity=300) StockItem.objects.create(part=cls.part, quantity=300)
# Now create another 400 units which are LOST # 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): def get_part_data(self):
"""Helper function for retrieving part data""" """Helper function for retrieving part data"""
@ -1992,7 +1992,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
quantity=10, quantity=10,
title='Making some assemblies', title='Making some assemblies',
reference='BO-9999', reference='BO-9999',
status=BuildStatus.PRODUCTION, status=BuildStatus.PRODUCTION.value,
) )
bom_item = BomItem.objects.get(pk=6) bom_item = BomItem.objects.get(pk=6)
@ -2133,7 +2133,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
for line_item in sp.purchase_order_line_items.all(): for line_item in sp.purchase_order_line_items.all():
po = line_item.order po = line_item.order
if po.status in PurchaseOrderStatus.OPEN: if po.status in PurchaseOrderStatusGroups.OPEN:
remaining = line_item.quantity - line_item.received remaining = line_item.quantity - line_item.received
if remaining > 0: if remaining > 0:

View File

@ -345,7 +345,7 @@ class PartPricingTests(InvenTreeTestCase):
self.assertIsNone(pricing.purchase_cost_min) self.assertIsNone(pricing.purchase_cost_min)
self.assertIsNone(pricing.purchase_cost_max) self.assertIsNone(pricing.purchase_cost_max)
po.status = PurchaseOrderStatus.COMPLETE po.status = PurchaseOrderStatus.COMPLETE.value
po.save() po.save()
pricing.update_purchase_cost() pricing.update_purchase_cost()

View File

@ -22,8 +22,9 @@ from build.models import Build
from build.serializers import BuildSerializer from build.serializers import BuildSerializer
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from company.serializers import CompanySerializer, SupplierPartSerializer from company.serializers import CompanySerializer, SupplierPartSerializer
from generic.states import StatusView
from InvenTree.api import (APIDownloadMixin, AttachmentMixin, from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView, MetadataView, StatusView) ListCreateDestroyAPIView, MetadataView)
from InvenTree.filters import (ORDER_FILTER, SEARCH_ORDER_FILTER, from InvenTree.filters import (ORDER_FILTER, SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS) SEARCH_ORDER_FILTER_ALIAS)
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull, from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,

View File

@ -190,7 +190,7 @@ def update_history(apps, schema_editor):
tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER
if tracking_type is not None: if tracking_type is not None:
entry.tracking_type = tracking_type entry.tracking_type = tracking_type.value
updated = True updated = True
if updated: if updated:

View File

@ -43,7 +43,7 @@ def update_stock_history(apps, schema_editor):
history.deltas['salesorder'] = item.sales_order.pk history.deltas['salesorder'] = item.sales_order.pk
# Change the history type # Change the history type
history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER.value
history.save() history.save()
n += 1 n += 1

View File

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

View File

@ -34,8 +34,8 @@ from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin, from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, InvenTreeTree, InvenTreeNotesMixin, InvenTreeTree,
MetadataMixin, extract_int) MetadataMixin, extract_int)
from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode, from InvenTree.status_codes import (SalesOrderStatusGroups, StockHistoryCode,
StockStatus) StockStatus, StockStatusGroups)
from part import models as PartModels from part import models as PartModels
from plugin.events import trigger_event from plugin.events import trigger_event
from users.models import Owner from users.models import Owner
@ -334,7 +334,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
customer=None, customer=None,
consumed_by=None, consumed_by=None,
is_building=False, 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 # A query filter which can be used to filter StockItem objects which have expired
@ -806,7 +806,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
) )
status = models.PositiveIntegerField( status = models.PositiveIntegerField(
default=StockStatus.OK, default=StockStatus.OK.value,
choices=StockStatus.items(), choices=StockStatus.items(),
validators=[MinValueValidator(0)]) validators=[MinValueValidator(0)])
@ -1082,12 +1082,12 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
if active is True: if active is True:
query = query.filter( query = query.filter(
line__order__status__in=SalesOrderStatus.OPEN, line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None shipment__shipment_date=None
) )
elif active is False: elif active is False:
query = query.exclude( query = query.exclude(
line__order__status__in=SalesOrderStatus.OPEN line__order__status__in=SalesOrderStatusGroups.OPEN,
).exclude( ).exclude(
shipment__shipment_date=None shipment__shipment_date=None
) )
@ -1346,7 +1346,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
entry = StockItemTracking.objects.create( entry = StockItemTracking.objects.create(
item=self, item=self,
tracking_type=entry_type, tracking_type=entry_type.value,
user=user, user=user,
date=datetime.now(), date=datetime.now(),
notes=notes, notes=notes,

View File

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% load plugin_extras %} {% load plugin_extras %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
{% load i18n %} {% load i18n %}
{% load l10n %} {% load l10n %}
@ -421,7 +421,7 @@
<tr> <tr>
<td><span class='fas fa-info'></span></td> <td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td> <td>{% trans "Status" %}</td>
<td>{% stock_status_label item.status %}</td> <td>{% status_label 'stock' item.status %}</td>
</tr> </tr>
{% if item.expiry_date %} {% if item.expiry_date %}
<tr> <tr>

View File

@ -385,11 +385,11 @@ class StockItemListTest(StockAPITestCase):
def test_filter_by_status(self): def test_filter_by_status(self):
"""Filter StockItem by 'status' field.""" """Filter StockItem by 'status' field."""
codes = { codes = {
StockStatus.OK: 27, StockStatus.OK.value: 27,
StockStatus.DESTROYED: 1, StockStatus.DESTROYED.value: 1,
StockStatus.LOST: 1, StockStatus.LOST.value: 1,
StockStatus.DAMAGED: 0, StockStatus.DAMAGED.value: 0,
StockStatus.REJECTED: 0, StockStatus.REJECTED.value: 0,
} }
for code in codes.keys(): for code in codes.keys():
@ -1465,7 +1465,7 @@ class StockAssignTest(StockAPITestCase):
stock_item = StockItem.objects.create( stock_item = StockItem.objects.create(
part=part.models.Part.objects.get(pk=1), part=part.models.Part.objects.get(pk=1),
status=StockStatus.DESTROYED, status=StockStatus.DESTROYED.value,
quantity=5, quantity=5,
) )

View File

@ -112,7 +112,7 @@ class StockOwnershipTest(StockViewTestCase):
"""Helper function to get response to API change.""" """Helper function to get response to API change."""
return self.client.patch( return self.client.patch(
reverse('api-stock-detail', args=(self.test_item_id,)), reverse('api-stock-detail', args=(self.test_item_id,)),
{'status': StockStatus.DAMAGED}, {'status': StockStatus.DAMAGED.value},
content_type='application/json', content_type='application/json',
) )
@ -156,7 +156,7 @@ class StockOwnershipTest(StockViewTestCase):
# Check that user is allowed to change item # Check that user is allowed to change item
self.assertTrue(item.check_ownership(self.user)) # Owner is group -> True self.assertTrue(item.check_ownership(self.user)) # Owner is group -> True
self.assertTrue(location.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 # Change group
new_group = Group.objects.create(name='new_group') new_group = Group.objects.create(name='new_group')

View File

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load status_codes %} {% load generic %}
{% load inventree_extras %} {% load inventree_extras %}
/* globals /* globals

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% load status_codes %} {% load generic %}
/* globals /* globals
addCachedAlert, addCachedAlert,

View File

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load status_codes %} {% load generic %}
{% load inventree_extras %} {% load inventree_extras %}
/* globals /* globals