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)
class StatusView(APIView):
"""Generic API endpoint for discovering information on 'status codes' for a particular model.
This class should be implemented as a subclass for each type of status.
For example, the API endpoint /stock/status/ will have information about
all available 'StockStatus' codes
"""
permission_classes = [
permissions.IsAuthenticated,
]
# Override status_class for implementing subclass
MODEL_REF = 'statusmodel'
def get_status_model(self, *args, **kwargs):
"""Return the StatusCode moedl based on extra parameters passed to the view"""
status_model = self.kwargs.get(self.MODEL_REF, None)
if status_model is None:
raise ValidationError(f"StatusView view called without '{self.MODEL_REF}' parameter")
return status_model
def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes"""
status_class = self.get_status_model()
if not status_class:
raise NotImplementedError("status_class not defined for this endpoint")
data = {
'class': status_class.__name__,
'values': status_class.dict(),
}
return Response(data)
class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model"""

View File

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

View File

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

View File

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

View File

@ -9,9 +9,10 @@ from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView, StatusView
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
from generic.states import StatusView
from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.status_codes import BuildStatus
from InvenTree.status_codes import BuildStatus, BuildStatusGroups
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
import build.admin
@ -41,9 +42,9 @@ class BuildFilter(rest_filters.FilterSet):
def filter_active(self, queryset, name, value):
"""Filter the queryset to either include or exclude orders which are active."""
if str2bool(value):
return queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
return queryset.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
else:
return queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
return queryset.exclude(status__in=BuildStatusGroups.ACTIVE_CODES)
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -298,7 +298,7 @@ class BuildTest(BuildAPITest):
expected_code=400,
)
bo.status = BuildStatus.CANCELLED
bo.status = BuildStatus.CANCELLED.value
bo.save()
# Now, we should be able to delete
@ -843,7 +843,7 @@ class BuildListTest(BuildAPITest):
builds = self.get(self.url, data={'active': True})
self.assertEqual(len(builds.data), 1)
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE.value})
self.assertEqual(len(builds.data), 4)
builds = self.get(self.url, data={'overdue': False})
@ -863,7 +863,7 @@ class BuildListTest(BuildAPITest):
reference="BO-0006",
quantity=10,
title='Just some thing',
status=BuildStatus.PRODUCTION,
status=BuildStatus.PRODUCTION.value,
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.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
InvenTreeNotesMixin, MetadataMixin)
from InvenTree.status_codes import PurchaseOrderStatus
from InvenTree.status_codes import PurchaseOrderStatusGroups
def rename_company_image(instance, filename):
@ -697,7 +697,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
def open_orders(self):
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN)
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
def on_order(self):
"""Return the total quantity of items currently on order.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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