mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
005c8341bf
commit
5d1d8ec889
@ -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"""
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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"),
|
||||
)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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.
|
||||
|
4
InvenTree/generic/__init__.py
Normal file
4
InvenTree/generic/__init__.py
Normal 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.
|
||||
"""
|
15
InvenTree/generic/states/__init__.py
Normal file
15
InvenTree/generic/states/__init__.py
Normal 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,
|
||||
]
|
54
InvenTree/generic/states/api.py
Normal file
54
InvenTree/generic/states/api.py
Normal 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)
|
170
InvenTree/generic/states/states.py
Normal file
170
InvenTree/generic/states/states.py
Normal 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
|
17
InvenTree/generic/states/tags.py
Normal file
17
InvenTree/generic/states/tags.py
Normal 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}'")
|
110
InvenTree/generic/states/tests.py
Normal file
110
InvenTree/generic/states/tests.py
Normal 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")
|
1
InvenTree/generic/templatetags/__init__.py
Normal file
1
InvenTree/generic/templatetags/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Template tags for generic *things*."""
|
10
InvenTree/generic/templatetags/generic.py
Normal file
10
InvenTree/generic/templatetags/generic.py
Normal 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,
|
||||
]
|
@ -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:
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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')
|
||||
)
|
||||
|
@ -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()),
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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 %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "order/order_base.html" %}
|
||||
|
||||
{% load inventree_extras %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "order/return_order_base.html" %}
|
||||
|
||||
{% load inventree_extras %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "order/sales_order_base.html" %}
|
||||
|
||||
{% load inventree_extras %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)))
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
20
InvenTree/stock/migrations/0102_alter_stockitem_status.py
Normal file
20
InvenTree/stock/migrations/0102_alter_stockitem_status.py
Normal 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)]),
|
||||
),
|
||||
]
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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')
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
/* globals
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
|
||||
/* globals
|
||||
addCachedAlert,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
{% load generic %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
/* globals
|
||||
|
Loading…
Reference in New Issue
Block a user