mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +00:00
Refactor states/status (#4857)
* add file for states * move general definition out * add some tests and docs * add tests for invalid definitions * make status_label tag generic * move templatetags * remove unused tag * rename test file * make status label a lookup * rename tags * move import structure * add missing tag * collect states dynamically * fix context function * move api function out * add tests for tags * rename tests * refactor imports * Add test for API function * improve errors and add tests for imporved errors * make test calls simpler * refactor definitions to use enums * switch to enum * refactor definitions to use enums * fix lookup * fix tag name * make _TAG lookup a function * cleanup BaseEnum * make _TAG definition simpler * restructure status codes to enum * reduce LoC * type status codes as int * add specific function for template context * Add definition for lookups * fix filter lookup * TEST: "fix" action lookup * Add missing migrations * Make all group code references explict * change default on models to value * switch to IntEnum * move groups into a seperate class * only request _TAG if it exsists * use value and list * use dedicated groups * fix stock assigment * fix order code * more fixes * fix borked change * fix render lookup * add group * fix import * fix syntax * clenup * fix migrations * fix typo * fix wrong value usage * fix test * remove group section * remove group section * add more test cases * Add more docstring * move choices out of migrations * change import ordeR? * last try before I revert * Update part.migrations.0112 - Add custom migration class which handles errors * Add unit test for migration - Ensure that the new fields are added to the model * Update reference to PR --------- Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
parent
005c8341bf
commit
5d1d8ec889
@ -306,47 +306,6 @@ class APISearchView(APIView):
|
|||||||
return Response(results)
|
return Response(results)
|
||||||
|
|
||||||
|
|
||||||
class StatusView(APIView):
|
|
||||||
"""Generic API endpoint for discovering information on 'status codes' for a particular model.
|
|
||||||
|
|
||||||
This class should be implemented as a subclass for each type of status.
|
|
||||||
For example, the API endpoint /stock/status/ will have information about
|
|
||||||
all available 'StockStatus' codes
|
|
||||||
"""
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
permissions.IsAuthenticated,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Override status_class for implementing subclass
|
|
||||||
MODEL_REF = 'statusmodel'
|
|
||||||
|
|
||||||
def get_status_model(self, *args, **kwargs):
|
|
||||||
"""Return the StatusCode moedl based on extra parameters passed to the view"""
|
|
||||||
|
|
||||||
status_model = self.kwargs.get(self.MODEL_REF, None)
|
|
||||||
|
|
||||||
if status_model is None:
|
|
||||||
raise ValidationError(f"StatusView view called without '{self.MODEL_REF}' parameter")
|
|
||||||
|
|
||||||
return status_model
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""Perform a GET request to learn information about status codes"""
|
|
||||||
|
|
||||||
status_class = self.get_status_model()
|
|
||||||
|
|
||||||
if not status_class:
|
|
||||||
raise NotImplementedError("status_class not defined for this endpoint")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'class': status_class.__name__,
|
|
||||||
'values': status_class.dict(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(data)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataView(RetrieveUpdateAPI):
|
class MetadataView(RetrieveUpdateAPI):
|
||||||
"""Generic API endpoint for reading and editing metadata for a model"""
|
"""Generic API endpoint for reading and editing metadata for a model"""
|
||||||
|
|
||||||
|
@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
import InvenTree.email
|
import InvenTree.email
|
||||||
import InvenTree.status
|
import InvenTree.status
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from generic.states import StatusCode
|
||||||
ReturnOrderLineStatus, ReturnOrderStatus,
|
from InvenTree.helpers import inheritors
|
||||||
SalesOrderStatus, StockHistoryCode,
|
|
||||||
StockStatus)
|
|
||||||
from users.models import RuleSet, check_user_role
|
from users.models import RuleSet, check_user_role
|
||||||
|
|
||||||
|
|
||||||
@ -57,17 +55,7 @@ def status_codes(request):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
request._inventree_status_codes = True
|
request._inventree_status_codes = True
|
||||||
|
return {cls.__name__: cls.template_context() for cls in inheritors(StatusCode)}
|
||||||
return {
|
|
||||||
# Expose the StatusCode classes to the templates
|
|
||||||
'ReturnOrderStatus': ReturnOrderStatus,
|
|
||||||
'ReturnOrderLineStatus': ReturnOrderLineStatus,
|
|
||||||
'SalesOrderStatus': SalesOrderStatus,
|
|
||||||
'PurchaseOrderStatus': PurchaseOrderStatus,
|
|
||||||
'BuildStatus': BuildStatus,
|
|
||||||
'StockStatus': StockStatus,
|
|
||||||
'StockHistoryCode': StockHistoryCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def user_roles(request):
|
def user_roles(request):
|
||||||
|
@ -196,6 +196,7 @@ INSTALLED_APPS = [
|
|||||||
'stock.apps.StockConfig',
|
'stock.apps.StockConfig',
|
||||||
'users.apps.UsersConfig',
|
'users.apps.UsersConfig',
|
||||||
'plugin.apps.PluginAppConfig',
|
'plugin.apps.PluginAppConfig',
|
||||||
|
'generic',
|
||||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||||
|
|
||||||
# Core django modules
|
# Core django modules
|
||||||
|
@ -2,377 +2,161 @@
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from generic.states import StatusCode
|
||||||
class StatusCode:
|
|
||||||
"""Base class for representing a set of StatusCodes.
|
|
||||||
|
|
||||||
This is used to map a set of integer values to text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
colors = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render(cls, key, large=False):
|
|
||||||
"""Render the value as a HTML label."""
|
|
||||||
# If the key cannot be found, pass it back
|
|
||||||
if key not in cls.options.keys():
|
|
||||||
return key
|
|
||||||
|
|
||||||
value = cls.options.get(key, key)
|
|
||||||
color = cls.colors.get(key, 'secondary')
|
|
||||||
|
|
||||||
span_class = f'badge rounded-pill bg-{color}'
|
|
||||||
|
|
||||||
return "<span class='{cl}'>{value}</span>".format(
|
|
||||||
cl=span_class,
|
|
||||||
value=value
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def list(cls):
|
|
||||||
"""Return the StatusCode options as a list of mapped key / value items."""
|
|
||||||
return list(cls.dict().values())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def text(cls, key):
|
|
||||||
"""Text for supplied status code."""
|
|
||||||
return cls.options.get(key, None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def items(cls):
|
|
||||||
"""All status code items."""
|
|
||||||
return cls.options.items()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def keys(cls):
|
|
||||||
"""All status code keys."""
|
|
||||||
return cls.options.keys()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def labels(cls):
|
|
||||||
"""All status code labels."""
|
|
||||||
return cls.options.values()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def names(cls):
|
|
||||||
"""Return a map of all 'names' of status codes in this class
|
|
||||||
|
|
||||||
Will return a dict object, with the attribute name indexed to the integer value.
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
{
|
|
||||||
'PENDING': 10,
|
|
||||||
'IN_PROGRESS': 20,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
keys = cls.keys()
|
|
||||||
status_names = {}
|
|
||||||
|
|
||||||
for d in dir(cls):
|
|
||||||
if d.startswith('_'):
|
|
||||||
continue
|
|
||||||
if d != d.upper():
|
|
||||||
continue
|
|
||||||
|
|
||||||
value = getattr(cls, d, None)
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
if callable(value):
|
|
||||||
continue
|
|
||||||
if type(value) != int:
|
|
||||||
continue
|
|
||||||
if value not in keys:
|
|
||||||
continue
|
|
||||||
|
|
||||||
status_names[d] = value
|
|
||||||
|
|
||||||
return status_names
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def dict(cls):
|
|
||||||
"""Return a dict representation containing all required information"""
|
|
||||||
values = {}
|
|
||||||
|
|
||||||
for name, value, in cls.names().items():
|
|
||||||
entry = {
|
|
||||||
'key': value,
|
|
||||||
'name': name,
|
|
||||||
'label': cls.label(value),
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasattr(cls, 'colors'):
|
|
||||||
if color := cls.colors.get(value, None):
|
|
||||||
entry['color'] = color
|
|
||||||
|
|
||||||
values[name] = entry
|
|
||||||
|
|
||||||
return values
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def label(cls, value):
|
|
||||||
"""Return the status code label associated with the provided value."""
|
|
||||||
return cls.options.get(value, value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def value(cls, label):
|
|
||||||
"""Return the value associated with the provided label."""
|
|
||||||
for k in cls.options.keys():
|
|
||||||
if cls.options[k].lower() == label.lower():
|
|
||||||
return k
|
|
||||||
|
|
||||||
raise ValueError("Label not found")
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderStatus(StatusCode):
|
class PurchaseOrderStatus(StatusCode):
|
||||||
"""Defines a set of status codes for a PurchaseOrder."""
|
"""Defines a set of status codes for a PurchaseOrder."""
|
||||||
|
|
||||||
# Order status codes
|
# Order status codes
|
||||||
PENDING = 10 # Order is pending (not yet placed)
|
PENDING = 10, _("Pending"), 'secondary' # Order is pending (not yet placed)
|
||||||
PLACED = 20 # Order has been placed with supplier
|
PLACED = 20, _("Placed"), 'primary' # Order has been placed with supplier
|
||||||
COMPLETE = 30 # Order has been completed
|
COMPLETE = 30, _("Complete"), 'success' # Order has been completed
|
||||||
CANCELLED = 40 # Order was cancelled
|
CANCELLED = 40, _("Cancelled"), 'danger' # Order was cancelled
|
||||||
LOST = 50 # Order was lost
|
LOST = 50, _("Lost"), 'warning' # Order was lost
|
||||||
RETURNED = 60 # Order was returned
|
RETURNED = 60, _("Returned"), 'warning' # Order was returned
|
||||||
|
|
||||||
options = {
|
|
||||||
PENDING: _("Pending"),
|
|
||||||
PLACED: _("Placed"),
|
|
||||||
COMPLETE: _("Complete"),
|
|
||||||
CANCELLED: _("Cancelled"),
|
|
||||||
LOST: _("Lost"),
|
|
||||||
RETURNED: _("Returned"),
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
class PurchaseOrderStatusGroups:
|
||||||
PENDING: 'secondary',
|
"""Groups for PurchaseOrderStatus codes."""
|
||||||
PLACED: 'primary',
|
|
||||||
COMPLETE: 'success',
|
|
||||||
CANCELLED: 'danger',
|
|
||||||
LOST: 'warning',
|
|
||||||
RETURNED: 'warning',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Open orders
|
# Open orders
|
||||||
OPEN = [
|
OPEN = [
|
||||||
PENDING,
|
PurchaseOrderStatus.PENDING.value,
|
||||||
PLACED,
|
PurchaseOrderStatus.PLACED.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Failed orders
|
# Failed orders
|
||||||
FAILED = [
|
FAILED = [
|
||||||
CANCELLED,
|
PurchaseOrderStatus.CANCELLED.value,
|
||||||
LOST,
|
PurchaseOrderStatus.LOST.value,
|
||||||
RETURNED
|
PurchaseOrderStatus.RETURNED.value
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderStatus(StatusCode):
|
class SalesOrderStatus(StatusCode):
|
||||||
"""Defines a set of status codes for a SalesOrder."""
|
"""Defines a set of status codes for a SalesOrder."""
|
||||||
|
|
||||||
PENDING = 10 # Order is pending
|
PENDING = 10, _("Pending"), 'secondary' # Order is pending
|
||||||
IN_PROGRESS = 15 # Order has been issued, and is in progress
|
IN_PROGRESS = 15, _("In Progress"), 'primary' # Order has been issued, and is in progress
|
||||||
SHIPPED = 20 # Order has been shipped to customer
|
SHIPPED = 20, _("Shipped"), 'success' # Order has been shipped to customer
|
||||||
CANCELLED = 40 # Order has been cancelled
|
CANCELLED = 40, _("Cancelled"), 'danger' # Order has been cancelled
|
||||||
LOST = 50 # Order was lost
|
LOST = 50, _("Lost"), 'warning' # Order was lost
|
||||||
RETURNED = 60 # Order was returned
|
RETURNED = 60, _("Returned"), 'warning' # Order was returned
|
||||||
|
|
||||||
options = {
|
|
||||||
PENDING: _("Pending"),
|
|
||||||
IN_PROGRESS: _("In Progress"),
|
|
||||||
SHIPPED: _("Shipped"),
|
|
||||||
CANCELLED: _("Cancelled"),
|
|
||||||
LOST: _("Lost"),
|
|
||||||
RETURNED: _("Returned"),
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
class SalesOrderStatusGroups:
|
||||||
PENDING: 'secondary',
|
"""Groups for SalesOrderStatus codes."""
|
||||||
IN_PROGRESS: 'primary',
|
|
||||||
SHIPPED: 'success',
|
|
||||||
CANCELLED: 'danger',
|
|
||||||
LOST: 'warning',
|
|
||||||
RETURNED: 'warning',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Open orders
|
# Open orders
|
||||||
OPEN = [
|
OPEN = [
|
||||||
PENDING,
|
SalesOrderStatus.PENDING.value,
|
||||||
IN_PROGRESS,
|
SalesOrderStatus.IN_PROGRESS.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Completed orders
|
# Completed orders
|
||||||
COMPLETE = [
|
COMPLETE = [
|
||||||
SHIPPED,
|
SalesOrderStatus.SHIPPED.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class StockStatus(StatusCode):
|
class StockStatus(StatusCode):
|
||||||
"""Status codes for Stock."""
|
"""Status codes for Stock."""
|
||||||
|
|
||||||
OK = 10 # Item is OK
|
OK = 10, _("OK"), 'success' # Item is OK
|
||||||
ATTENTION = 50 # Item requires attention
|
ATTENTION = 50, _("Attention needed"), 'warning' # Item requires attention
|
||||||
DAMAGED = 55 # Item is damaged
|
DAMAGED = 55, _("Damaged"), 'warning' # Item is damaged
|
||||||
DESTROYED = 60 # Item is destroyed
|
DESTROYED = 60, _("Destroyed"), 'danger' # Item is destroyed
|
||||||
REJECTED = 65 # Item is rejected
|
REJECTED = 65, _("Rejected"), 'danger' # Item is rejected
|
||||||
LOST = 70 # Item has been lost
|
LOST = 70, _("Lost"), 'dark' # Item has been lost
|
||||||
QUARANTINED = 75 # Item has been quarantined and is unavailable
|
QUARANTINED = 75, _("Quarantined"), 'info' # Item has been quarantined and is unavailable
|
||||||
RETURNED = 85 # Item has been returned from a customer
|
RETURNED = 85, _("Returned"), 'warning' # Item has been returned from a customer
|
||||||
|
|
||||||
options = {
|
|
||||||
OK: _("OK"),
|
|
||||||
ATTENTION: _("Attention needed"),
|
|
||||||
DAMAGED: _("Damaged"),
|
|
||||||
DESTROYED: _("Destroyed"),
|
|
||||||
LOST: _("Lost"),
|
|
||||||
REJECTED: _("Rejected"),
|
|
||||||
QUARANTINED: _("Quarantined"),
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
class StockStatusGroups:
|
||||||
OK: 'success',
|
"""Groups for StockStatus codes."""
|
||||||
ATTENTION: 'warning',
|
|
||||||
DAMAGED: 'warning',
|
|
||||||
DESTROYED: 'danger',
|
|
||||||
LOST: 'dark',
|
|
||||||
REJECTED: 'danger',
|
|
||||||
QUARANTINED: 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
# The following codes correspond to parts that are 'available' or 'in stock'
|
# The following codes correspond to parts that are 'available' or 'in stock'
|
||||||
AVAILABLE_CODES = [
|
AVAILABLE_CODES = [
|
||||||
OK,
|
StockStatus.OK.value,
|
||||||
ATTENTION,
|
StockStatus.ATTENTION.value,
|
||||||
DAMAGED,
|
StockStatus.DAMAGED.value,
|
||||||
RETURNED,
|
StockStatus.RETURNED.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class StockHistoryCode(StatusCode):
|
class StockHistoryCode(StatusCode):
|
||||||
"""Status codes for StockHistory."""
|
"""Status codes for StockHistory."""
|
||||||
|
|
||||||
LEGACY = 0
|
LEGACY = 0, _('Legacy stock tracking entry')
|
||||||
|
|
||||||
CREATED = 1
|
CREATED = 1, _('Stock item created')
|
||||||
|
|
||||||
# Manual editing operations
|
# Manual editing operations
|
||||||
EDITED = 5
|
EDITED = 5, _('Edited stock item')
|
||||||
ASSIGNED_SERIAL = 6
|
ASSIGNED_SERIAL = 6, _('Assigned serial number')
|
||||||
|
|
||||||
# Manual stock operations
|
# Manual stock operations
|
||||||
STOCK_COUNT = 10
|
STOCK_COUNT = 10, _('Stock counted')
|
||||||
STOCK_ADD = 11
|
STOCK_ADD = 11, _('Stock manually added')
|
||||||
STOCK_REMOVE = 12
|
STOCK_REMOVE = 12, _('Stock manually removed')
|
||||||
|
|
||||||
# Location operations
|
# Location operations
|
||||||
STOCK_MOVE = 20
|
STOCK_MOVE = 20, _('Location changed')
|
||||||
STOCK_UPDATE = 25
|
STOCK_UPDATE = 25, _('Stock updated')
|
||||||
|
|
||||||
# Installation operations
|
# Installation operations
|
||||||
INSTALLED_INTO_ASSEMBLY = 30
|
INSTALLED_INTO_ASSEMBLY = 30, _('Installed into assembly')
|
||||||
REMOVED_FROM_ASSEMBLY = 31
|
REMOVED_FROM_ASSEMBLY = 31, _('Removed from assembly')
|
||||||
|
|
||||||
INSTALLED_CHILD_ITEM = 35
|
INSTALLED_CHILD_ITEM = 35, _('Installed component item')
|
||||||
REMOVED_CHILD_ITEM = 36
|
REMOVED_CHILD_ITEM = 36, _('Removed component item')
|
||||||
|
|
||||||
# Stock splitting operations
|
# Stock splitting operations
|
||||||
SPLIT_FROM_PARENT = 40
|
SPLIT_FROM_PARENT = 40, _('Split from parent item')
|
||||||
SPLIT_CHILD_ITEM = 42
|
SPLIT_CHILD_ITEM = 42, _('Split child item')
|
||||||
|
|
||||||
# Stock merging operations
|
# Stock merging operations
|
||||||
MERGED_STOCK_ITEMS = 45
|
MERGED_STOCK_ITEMS = 45, _('Merged stock items')
|
||||||
|
|
||||||
# Convert stock item to variant
|
# Convert stock item to variant
|
||||||
CONVERTED_TO_VARIANT = 48
|
CONVERTED_TO_VARIANT = 48, _('Converted to variant')
|
||||||
|
|
||||||
# Build order codes
|
# Build order codes
|
||||||
BUILD_OUTPUT_CREATED = 50
|
BUILD_OUTPUT_CREATED = 50, _('Build order output created')
|
||||||
BUILD_OUTPUT_COMPLETED = 55
|
BUILD_OUTPUT_COMPLETED = 55, _('Build order output completed')
|
||||||
BUILD_OUTPUT_REJECTED = 56
|
BUILD_OUTPUT_REJECTED = 56, _('Build order output rejected')
|
||||||
BUILD_CONSUMED = 57
|
BUILD_CONSUMED = 57, _('Consumed by build order')
|
||||||
|
|
||||||
# Sales order codes
|
# Sales order codes
|
||||||
SHIPPED_AGAINST_SALES_ORDER = 60
|
SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order")
|
||||||
|
|
||||||
# Purchase order codes
|
# Purchase order codes
|
||||||
RECEIVED_AGAINST_PURCHASE_ORDER = 70
|
RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
|
||||||
|
|
||||||
# Return order codes
|
# Return order codes
|
||||||
RETURNED_AGAINST_RETURN_ORDER = 80
|
RETURNED_AGAINST_RETURN_ORDER = 80, _('Returned against Return Order')
|
||||||
|
|
||||||
# Customer actions
|
# Customer actions
|
||||||
SENT_TO_CUSTOMER = 100
|
SENT_TO_CUSTOMER = 100, _('Sent to customer')
|
||||||
RETURNED_FROM_CUSTOMER = 105
|
RETURNED_FROM_CUSTOMER = 105, _('Returned from customer')
|
||||||
|
|
||||||
options = {
|
|
||||||
LEGACY: _('Legacy stock tracking entry'),
|
|
||||||
|
|
||||||
CREATED: _('Stock item created'),
|
|
||||||
|
|
||||||
EDITED: _('Edited stock item'),
|
|
||||||
ASSIGNED_SERIAL: _('Assigned serial number'),
|
|
||||||
|
|
||||||
STOCK_COUNT: _('Stock counted'),
|
|
||||||
STOCK_ADD: _('Stock manually added'),
|
|
||||||
STOCK_REMOVE: _('Stock manually removed'),
|
|
||||||
|
|
||||||
STOCK_MOVE: _('Location changed'),
|
|
||||||
STOCK_UPDATE: _('Stock updated'),
|
|
||||||
|
|
||||||
INSTALLED_INTO_ASSEMBLY: _('Installed into assembly'),
|
|
||||||
REMOVED_FROM_ASSEMBLY: _('Removed from assembly'),
|
|
||||||
|
|
||||||
INSTALLED_CHILD_ITEM: _('Installed component item'),
|
|
||||||
REMOVED_CHILD_ITEM: _('Removed component item'),
|
|
||||||
|
|
||||||
SPLIT_FROM_PARENT: _('Split from parent item'),
|
|
||||||
SPLIT_CHILD_ITEM: _('Split child item'),
|
|
||||||
|
|
||||||
MERGED_STOCK_ITEMS: _('Merged stock items'),
|
|
||||||
|
|
||||||
CONVERTED_TO_VARIANT: _('Converted to variant'),
|
|
||||||
|
|
||||||
SENT_TO_CUSTOMER: _('Sent to customer'),
|
|
||||||
RETURNED_FROM_CUSTOMER: _('Returned from customer'),
|
|
||||||
|
|
||||||
BUILD_OUTPUT_CREATED: _('Build order output created'),
|
|
||||||
BUILD_OUTPUT_COMPLETED: _('Build order output completed'),
|
|
||||||
BUILD_OUTPUT_REJECTED: _('Build order output rejected'),
|
|
||||||
BUILD_CONSUMED: _('Consumed by build order'),
|
|
||||||
|
|
||||||
SHIPPED_AGAINST_SALES_ORDER: _("Shipped against Sales Order"),
|
|
||||||
|
|
||||||
RECEIVED_AGAINST_PURCHASE_ORDER: _('Received against Purchase Order'),
|
|
||||||
|
|
||||||
RETURNED_AGAINST_RETURN_ORDER: _('Returned against Return Order'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BuildStatus(StatusCode):
|
class BuildStatus(StatusCode):
|
||||||
"""Build status codes."""
|
"""Build status codes."""
|
||||||
|
|
||||||
PENDING = 10 # Build is pending / active
|
PENDING = 10, _("Pending"), 'secondary' # Build is pending / active
|
||||||
PRODUCTION = 20 # BuildOrder is in production
|
PRODUCTION = 20, _("Production"), 'primary' # BuildOrder is in production
|
||||||
CANCELLED = 30 # Build was cancelled
|
CANCELLED = 30, _("Cancelled"), 'danger' # Build was cancelled
|
||||||
COMPLETE = 40 # Build is complete
|
COMPLETE = 40, _("Complete"), 'success' # Build is complete
|
||||||
|
|
||||||
options = {
|
|
||||||
PENDING: _("Pending"),
|
|
||||||
PRODUCTION: _("Production"),
|
|
||||||
CANCELLED: _("Cancelled"),
|
|
||||||
COMPLETE: _("Complete"),
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
class BuildStatusGroups:
|
||||||
PENDING: 'secondary',
|
"""Groups for BuildStatus codes."""
|
||||||
PRODUCTION: 'primary',
|
|
||||||
COMPLETE: 'success',
|
|
||||||
CANCELLED: 'danger',
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTIVE_CODES = [
|
ACTIVE_CODES = [
|
||||||
PENDING,
|
BuildStatus.PENDING.value,
|
||||||
PRODUCTION,
|
BuildStatus.PRODUCTION.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -380,68 +164,40 @@ class ReturnOrderStatus(StatusCode):
|
|||||||
"""Defines a set of status codes for a ReturnOrder"""
|
"""Defines a set of status codes for a ReturnOrder"""
|
||||||
|
|
||||||
# Order is pending, waiting for receipt of items
|
# Order is pending, waiting for receipt of items
|
||||||
PENDING = 10
|
PENDING = 10, _("Pending"), 'secondary'
|
||||||
|
|
||||||
# Items have been received, and are being inspected
|
# Items have been received, and are being inspected
|
||||||
IN_PROGRESS = 20
|
IN_PROGRESS = 20, _("In Progress"), 'primary'
|
||||||
|
|
||||||
COMPLETE = 30
|
COMPLETE = 30, _("Complete"), 'success'
|
||||||
CANCELLED = 40
|
CANCELLED = 40, _("Cancelled"), 'danger'
|
||||||
|
|
||||||
|
|
||||||
|
class ReturnOrderStatusGroups:
|
||||||
|
"""Groups for ReturnOrderStatus codes."""
|
||||||
|
|
||||||
OPEN = [
|
OPEN = [
|
||||||
PENDING,
|
ReturnOrderStatus.PENDING.value,
|
||||||
IN_PROGRESS,
|
ReturnOrderStatus.IN_PROGRESS.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
options = {
|
|
||||||
PENDING: _("Pending"),
|
|
||||||
IN_PROGRESS: _("In Progress"),
|
|
||||||
COMPLETE: _("Complete"),
|
|
||||||
CANCELLED: _("Cancelled"),
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
|
||||||
PENDING: 'secondary',
|
|
||||||
IN_PROGRESS: 'primary',
|
|
||||||
COMPLETE: 'success',
|
|
||||||
CANCELLED: 'danger',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ReturnOrderLineStatus(StatusCode):
|
class ReturnOrderLineStatus(StatusCode):
|
||||||
"""Defines a set of status codes for a ReturnOrderLineItem"""
|
"""Defines a set of status codes for a ReturnOrderLineItem"""
|
||||||
|
|
||||||
PENDING = 10
|
PENDING = 10, _("Pending"), 'secondary'
|
||||||
|
|
||||||
# Item is to be returned to customer, no other action
|
# Item is to be returned to customer, no other action
|
||||||
RETURN = 20
|
RETURN = 20, _("Return"), 'success'
|
||||||
|
|
||||||
# Item is to be repaired, and returned to customer
|
# Item is to be repaired, and returned to customer
|
||||||
REPAIR = 30
|
REPAIR = 30, _("Repair"), 'primary'
|
||||||
|
|
||||||
# Item is to be replaced (new item shipped)
|
# Item is to be replaced (new item shipped)
|
||||||
REPLACE = 40
|
REPLACE = 40, _("Replace"), 'warning'
|
||||||
|
|
||||||
# Item is to be refunded (cannot be repaired)
|
# Item is to be refunded (cannot be repaired)
|
||||||
REFUND = 50
|
REFUND = 50, _("Refund"), 'info'
|
||||||
|
|
||||||
# Item is rejected
|
# Item is rejected
|
||||||
REJECT = 60
|
REJECT = 60, _("Reject"), 'danger'
|
||||||
|
|
||||||
options = {
|
|
||||||
PENDING: _('Pending'),
|
|
||||||
RETURN: _('Return'),
|
|
||||||
REPAIR: _('Repair'),
|
|
||||||
REFUND: _('Refund'),
|
|
||||||
REPLACE: _('Replace'),
|
|
||||||
REJECT: _('Reject')
|
|
||||||
}
|
|
||||||
|
|
||||||
colors = {
|
|
||||||
PENDING: 'secondary',
|
|
||||||
RETURN: 'success',
|
|
||||||
REPAIR: 'primary',
|
|
||||||
REFUND: 'info',
|
|
||||||
REPLACE: 'warning',
|
|
||||||
REJECT: 'danger',
|
|
||||||
}
|
|
||||||
|
@ -9,9 +9,10 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django_filters import rest_framework as rest_filters
|
from django_filters import rest_framework as rest_filters
|
||||||
|
|
||||||
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView, StatusView
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, MetadataView
|
||||||
|
from generic.states import StatusView
|
||||||
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus, BuildStatusGroups
|
||||||
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
|
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
|
||||||
|
|
||||||
import build.admin
|
import build.admin
|
||||||
@ -41,9 +42,9 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
def filter_active(self, queryset, name, value):
|
def filter_active(self, queryset, name, value):
|
||||||
"""Filter the queryset to either include or exclude orders which are active."""
|
"""Filter the queryset to either include or exclude orders which are active."""
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
return queryset.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
else:
|
else:
|
||||||
return queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
|
return queryset.exclude(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ from mptt.exceptions import InvalidMove
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode, BuildStatusGroups
|
||||||
|
|
||||||
from build.validators import generate_next_build_reference, validate_build_order_reference
|
from build.validators import generate_next_build_reference, validate_build_order_reference
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
verbose_name = _("Build Order")
|
verbose_name = _("Build Order")
|
||||||
verbose_name_plural = _("Build Orders")
|
verbose_name_plural = _("Build Orders")
|
||||||
|
|
||||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
OVERDUE_FILTER = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||||
|
|
||||||
# Global setting for specifying reference pattern
|
# Global setting for specifying reference pattern
|
||||||
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
|
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
|
||||||
@ -129,10 +129,10 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Order was completed within the specified range
|
# Order was completed within the specified range
|
||||||
completed = Q(status=BuildStatus.COMPLETE) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
|
completed = Q(status=BuildStatus.COMPLETE.value) & Q(completion_date__gte=min_date) & Q(completion_date__lte=max_date)
|
||||||
|
|
||||||
# Order target date falls within specified range
|
# Order target date falls within specified range
|
||||||
pending = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
pending = Q(status__in=BuildStatusGroups.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
||||||
|
|
||||||
# TODO - Construct a queryset for "overdue" orders
|
# TODO - Construct a queryset for "overdue" orders
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
verbose_name=_('Build Status'),
|
verbose_name=_('Build Status'),
|
||||||
default=BuildStatus.PENDING,
|
default=BuildStatus.PENDING.value,
|
||||||
choices=BuildStatus.items(),
|
choices=BuildStatus.items(),
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(0)],
|
||||||
help_text=_('Build status code')
|
help_text=_('Build status code')
|
||||||
@ -331,7 +331,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
@property
|
@property
|
||||||
def active(self):
|
def active(self):
|
||||||
"""Return True if this build is active."""
|
"""Return True if this build is active."""
|
||||||
return self.status in BuildStatus.ACTIVE_CODES
|
return self.status in BuildStatusGroups.ACTIVE_CODES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bom_items(self):
|
def bom_items(self):
|
||||||
@ -503,7 +503,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
self.completion_date = datetime.now().date()
|
self.completion_date = datetime.now().date()
|
||||||
self.completed_by = user
|
self.completed_by = user
|
||||||
self.status = BuildStatus.COMPLETE
|
self.status = BuildStatus.COMPLETE.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# Remove untracked allocated stock
|
# Remove untracked allocated stock
|
||||||
@ -585,7 +585,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
self.completion_date = datetime.now().date()
|
self.completion_date = datetime.now().date()
|
||||||
self.completed_by = user
|
self.completed_by = user
|
||||||
|
|
||||||
self.status = BuildStatus.CANCELLED
|
self.status = BuildStatus.CANCELLED.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
trigger_event('build.cancelled', id=self.pk)
|
trigger_event('build.cancelled', id=self.pk)
|
||||||
@ -729,7 +729,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
_add_tracking_entry(output, user)
|
_add_tracking_entry(output, user)
|
||||||
|
|
||||||
if self.status == BuildStatus.PENDING:
|
if self.status == BuildStatus.PENDING:
|
||||||
self.status = BuildStatus.PRODUCTION
|
self.status = BuildStatus.PRODUCTION.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -831,7 +831,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
|
|
||||||
# Update build output item
|
# Update build output item
|
||||||
output.is_building = False
|
output.is_building = False
|
||||||
output.status = StockStatus.REJECTED
|
output.status = StockStatus.REJECTED.value
|
||||||
output.location = location
|
output.location = location
|
||||||
output.save(add_note=False)
|
output.save(add_note=False)
|
||||||
|
|
||||||
@ -851,7 +851,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
notes=notes,
|
notes=notes,
|
||||||
deltas={
|
deltas={
|
||||||
'location': location.pk,
|
'location': location.pk,
|
||||||
'status': StockStatus.REJECTED,
|
'status': StockStatus.REJECTED.value,
|
||||||
'buildorder': self.pk,
|
'buildorder': self.pk,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -865,7 +865,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
"""
|
"""
|
||||||
# Select the location for the build output
|
# Select the location for the build output
|
||||||
location = kwargs.get('location', self.destination)
|
location = kwargs.get('location', self.destination)
|
||||||
status = kwargs.get('status', StockStatus.OK)
|
status = kwargs.get('status', StockStatus.OK.value)
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
|
||||||
# List the allocated BuildItem objects for the given output
|
# List the allocated BuildItem objects for the given output
|
||||||
@ -1187,7 +1187,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
- PENDING
|
- PENDING
|
||||||
- HOLDING
|
- HOLDING
|
||||||
"""
|
"""
|
||||||
return self.status in BuildStatus.ACTIVE_CODES
|
return self.status in BuildStatusGroups.ACTIVE_CODES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
|
@ -490,8 +490,8 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=list(StockStatus.items()),
|
choices=StockStatus.items(),
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK.value,
|
||||||
label=_("Status"),
|
label=_("Status"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import build.models
|
|||||||
import InvenTree.email
|
import InvenTree.email
|
||||||
import InvenTree.helpers_model
|
import InvenTree.helpers_model
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatusGroups
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
|
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
@ -158,7 +158,7 @@ def check_overdue_build_orders():
|
|||||||
|
|
||||||
overdue_orders = build.models.Build.objects.filter(
|
overdue_orders = build.models.Build.objects.filter(
|
||||||
target_date=yesterday,
|
target_date=yesterday,
|
||||||
status__in=BuildStatus.ACTIVE_CODES
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
for bo in overdue_orders:
|
for bo in overdue_orders:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
@ -150,7 +150,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Status" %}</td>
|
<td>{% trans "Status" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% build_status_label build.status %}
|
{% status_label 'build' build.status %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if build.target_date %}
|
{% if build.target_date %}
|
||||||
@ -217,7 +217,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
|
|
||||||
{% block page_data %}
|
{% block page_data %}
|
||||||
<h3>
|
<h3>
|
||||||
{% build_status_label build.status large=True %}
|
{% status_label 'build' build.status large=True %}
|
||||||
{% if build.is_overdue %}
|
{% if build.is_overdue %}
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% include "build/sidebar.html" %}
|
{% include "build/sidebar.html" %}
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Status" %}</td>
|
<td>{% trans "Status" %}</td>
|
||||||
<td>{% build_status_label build.status %}</td>
|
<td>{% status_label 'build' build.status %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-check-circle'></span></td>
|
<td><span class='fas fa-check-circle'></span></td>
|
||||||
|
@ -298,7 +298,7 @@ class BuildTest(BuildAPITest):
|
|||||||
expected_code=400,
|
expected_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
bo.status = BuildStatus.CANCELLED
|
bo.status = BuildStatus.CANCELLED.value
|
||||||
bo.save()
|
bo.save()
|
||||||
|
|
||||||
# Now, we should be able to delete
|
# Now, we should be able to delete
|
||||||
@ -843,7 +843,7 @@ class BuildListTest(BuildAPITest):
|
|||||||
builds = self.get(self.url, data={'active': True})
|
builds = self.get(self.url, data={'active': True})
|
||||||
self.assertEqual(len(builds.data), 1)
|
self.assertEqual(len(builds.data), 1)
|
||||||
|
|
||||||
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE})
|
builds = self.get(self.url, data={'status': BuildStatus.COMPLETE.value})
|
||||||
self.assertEqual(len(builds.data), 4)
|
self.assertEqual(len(builds.data), 4)
|
||||||
|
|
||||||
builds = self.get(self.url, data={'overdue': False})
|
builds = self.get(self.url, data={'overdue': False})
|
||||||
@ -863,7 +863,7 @@ class BuildListTest(BuildAPITest):
|
|||||||
reference="BO-0006",
|
reference="BO-0006",
|
||||||
quantity=10,
|
quantity=10,
|
||||||
title='Just some thing',
|
title='Just some thing',
|
||||||
status=BuildStatus.PRODUCTION,
|
status=BuildStatus.PRODUCTION.value,
|
||||||
target_date=in_the_past
|
target_date=in_the_past
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ from common.settings import currency_code_default
|
|||||||
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField
|
||||||
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, MetadataMixin)
|
InvenTreeNotesMixin, MetadataMixin)
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatusGroups
|
||||||
|
|
||||||
|
|
||||||
def rename_company_image(instance, filename):
|
def rename_company_image(instance, filename):
|
||||||
@ -697,7 +697,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
|
|||||||
|
|
||||||
def open_orders(self):
|
def open_orders(self):
|
||||||
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
|
"""Return a database query for PurchaseOrder line items for this SupplierPart, limited to purchase orders that are open / outstanding."""
|
||||||
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatus.OPEN)
|
return self.purchase_order_line_items.prefetch_related('order').filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||||
|
|
||||||
def on_order(self):
|
def on_order(self):
|
||||||
"""Return the total quantity of items currently on order.
|
"""Return the total quantity of items currently on order.
|
||||||
|
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.models import InvenTreeSetting, ProjectCode
|
||||||
from common.settings import settings
|
from common.settings import settings
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
|
from generic.states import StatusView
|
||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView, MetadataView, StatusView)
|
ListCreateDestroyAPIView, MetadataView)
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
from InvenTree.filters import SEARCH_ORDER_FILTER, SEARCH_ORDER_FILTER_ALIAS
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
from InvenTree.helpers_model import construct_absolute_url, get_base_url
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
||||||
RetrieveUpdateDestroyAPI)
|
RetrieveUpdateDestroyAPI)
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatus,
|
||||||
ReturnOrderStatus, SalesOrderStatus)
|
PurchaseOrderStatusGroups,
|
||||||
|
ReturnOrderLineStatus, ReturnOrderStatus,
|
||||||
|
SalesOrderStatus, SalesOrderStatusGroups)
|
||||||
from order import models, serializers
|
from order import models, serializers
|
||||||
from order.admin import (PurchaseOrderExtraLineResource,
|
from order.admin import (PurchaseOrderExtraLineResource,
|
||||||
PurchaseOrderLineItemResource, PurchaseOrderResource,
|
PurchaseOrderLineItemResource, PurchaseOrderResource,
|
||||||
@ -431,9 +434,9 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
|||||||
"""Filter by "pending" status (order status = pending)"""
|
"""Filter by "pending" status (order status = pending)"""
|
||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.filter(order__status__in=PurchaseOrderStatus.OPEN)
|
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||||
else:
|
else:
|
||||||
return queryset.exclude(order__status__in=PurchaseOrderStatus.OPEN)
|
return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||||
|
|
||||||
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
received = rest_filters.BooleanFilter(label='received', method='filter_received')
|
||||||
|
|
||||||
@ -448,7 +451,7 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
|||||||
return queryset.filter(q)
|
return queryset.filter(q)
|
||||||
else:
|
else:
|
||||||
# Only count "pending" orders
|
# Only count "pending" orders
|
||||||
return queryset.exclude(q).filter(order__status__in=PurchaseOrderStatus.OPEN)
|
return queryset.exclude(q).filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderLineItemMixin:
|
class PurchaseOrderLineItemMixin:
|
||||||
@ -984,12 +987,12 @@ class SalesOrderAllocationList(ListAPI):
|
|||||||
# Filter only "open" orders
|
# Filter only "open" orders
|
||||||
# Filter only allocations which have *not* shipped
|
# Filter only allocations which have *not* shipped
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None,
|
shipment__shipment_date=None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.exclude(
|
queryset = queryset.exclude(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None
|
shipment__shipment_date=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1471,21 +1474,21 @@ class OrderCalendarExport(ICalFeed):
|
|||||||
if obj['include_completed'] is False:
|
if obj['include_completed'] is False:
|
||||||
# Do not include completed orders from list in this case
|
# Do not include completed orders from list in this case
|
||||||
# Completed status = 30
|
# Completed status = 30
|
||||||
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE)
|
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE.value)
|
||||||
else:
|
else:
|
||||||
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
|
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
|
||||||
elif obj["ordertype"] == 'sales-order':
|
elif obj["ordertype"] == 'sales-order':
|
||||||
if obj['include_completed'] is False:
|
if obj['include_completed'] is False:
|
||||||
# Do not include completed (=shipped) orders from list in this case
|
# Do not include completed (=shipped) orders from list in this case
|
||||||
# Shipped status = 20
|
# Shipped status = 20
|
||||||
outlist = models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED)
|
outlist = models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED.value)
|
||||||
else:
|
else:
|
||||||
outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
|
outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
|
||||||
elif obj["ordertype"] == 'return-order':
|
elif obj["ordertype"] == 'return-order':
|
||||||
if obj['include_completed'] is False:
|
if obj['include_completed'] is False:
|
||||||
# Do not include completed orders from list in this case
|
# Do not include completed orders from list in this case
|
||||||
# Complete status = 30
|
# Complete status = 30
|
||||||
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False).filter(status__lt=ReturnOrderStatus.COMPLETE)
|
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False).filter(status__lt=ReturnOrderStatus.COMPLETE.value)
|
||||||
else:
|
else:
|
||||||
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False)
|
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False)
|
||||||
else:
|
else:
|
||||||
|
@ -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,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, MetadataMixin,
|
InvenTreeNotesMixin, MetadataMixin,
|
||||||
ReferenceIndexingMixin)
|
ReferenceIndexingMixin)
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatus,
|
||||||
ReturnOrderStatus, SalesOrderStatus,
|
PurchaseOrderStatusGroups,
|
||||||
StockHistoryCode, StockStatus)
|
ReturnOrderLineStatus, ReturnOrderStatus,
|
||||||
|
ReturnOrderStatusGroups, SalesOrderStatus,
|
||||||
|
SalesOrderStatusGroups, StockHistoryCode,
|
||||||
|
StockStatus)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
|
||||||
@ -294,7 +297,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_status_class(cls):
|
def get_status_class(cls):
|
||||||
"""Return the PurchasOrderStatus class"""
|
"""Return the PurchasOrderStatus class"""
|
||||||
return PurchaseOrderStatus
|
return PurchaseOrderStatusGroups
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def api_defaults(cls, request):
|
def api_defaults(cls, request):
|
||||||
@ -333,10 +336,10 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Construct a queryset for "received" orders within the range
|
# Construct a queryset for "received" orders within the range
|
||||||
received = Q(status=PurchaseOrderStatus.COMPLETE) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
|
received = Q(status=PurchaseOrderStatus.COMPLETE.value) & Q(complete_date__gte=min_date) & Q(complete_date__lte=max_date)
|
||||||
|
|
||||||
# Construct a queryset for "pending" orders within the range
|
# Construct a queryset for "pending" orders within the range
|
||||||
pending = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
pending = Q(status__in=PurchaseOrderStatusGroups.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
||||||
|
|
||||||
# TODO - Construct a queryset for "overdue" orders within the range
|
# TODO - Construct a queryset for "overdue" orders within the range
|
||||||
|
|
||||||
@ -361,7 +364,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(),
|
status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING.value, choices=PurchaseOrderStatus.items(),
|
||||||
help_text=_('Purchase order status'))
|
help_text=_('Purchase order status'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -479,7 +482,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
Order must be currently PENDING.
|
Order must be currently PENDING.
|
||||||
"""
|
"""
|
||||||
if self.status == PurchaseOrderStatus.PENDING:
|
if self.status == PurchaseOrderStatus.PENDING:
|
||||||
self.status = PurchaseOrderStatus.PLACED
|
self.status = PurchaseOrderStatus.PLACED.value
|
||||||
self.issue_date = datetime.now().date()
|
self.issue_date = datetime.now().date()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -500,7 +503,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
Order must be currently PLACED.
|
Order must be currently PLACED.
|
||||||
"""
|
"""
|
||||||
if self.status == PurchaseOrderStatus.PLACED:
|
if self.status == PurchaseOrderStatus.PLACED:
|
||||||
self.status = PurchaseOrderStatus.COMPLETE
|
self.status = PurchaseOrderStatus.COMPLETE.value
|
||||||
self.complete_date = datetime.now().date()
|
self.complete_date = datetime.now().date()
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
@ -520,7 +523,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
@property
|
@property
|
||||||
def is_open(self):
|
def is_open(self):
|
||||||
"""Return True if the PurchaseOrder is 'open'"""
|
"""Return True if the PurchaseOrder is 'open'"""
|
||||||
return self.status in PurchaseOrderStatus.OPEN
|
return self.status in PurchaseOrderStatusGroups.OPEN
|
||||||
|
|
||||||
def can_cancel(self):
|
def can_cancel(self):
|
||||||
"""A PurchaseOrder can only be cancelled under the following circumstances.
|
"""A PurchaseOrder can only be cancelled under the following circumstances.
|
||||||
@ -537,7 +540,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
def cancel_order(self):
|
def cancel_order(self):
|
||||||
"""Marks the PurchaseOrder as CANCELLED."""
|
"""Marks the PurchaseOrder as CANCELLED."""
|
||||||
if self.can_cancel():
|
if self.can_cancel():
|
||||||
self.status = PurchaseOrderStatus.CANCELLED
|
self.status = PurchaseOrderStatus.CANCELLED.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
trigger_event('purchaseorder.cancelled', id=self.pk)
|
trigger_event('purchaseorder.cancelled', id=self.pk)
|
||||||
@ -574,7 +577,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
return self.lines.count() > 0 and self.pending_line_items().count() == 0
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs):
|
def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK.value, **kwargs):
|
||||||
"""Receive a line item (or partial line item) against this PurchaseOrder."""
|
"""Receive a line item (or partial line item) against this PurchaseOrder."""
|
||||||
# Extract optional batch code for the new stock item
|
# Extract optional batch code for the new stock item
|
||||||
batch_code = kwargs.get('batch_code', '')
|
batch_code = kwargs.get('batch_code', '')
|
||||||
@ -701,7 +704,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_status_class(cls):
|
def get_status_class(cls):
|
||||||
"""Return the SalesOrderStatus class"""
|
"""Return the SalesOrderStatus class"""
|
||||||
return SalesOrderStatus
|
return SalesOrderStatusGroups
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def api_defaults(cls, request):
|
def api_defaults(cls, request):
|
||||||
@ -739,10 +742,10 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
# Construct a queryset for "completed" orders within the range
|
# Construct a queryset for "completed" orders within the range
|
||||||
completed = Q(status__in=SalesOrderStatus.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
|
completed = Q(status__in=SalesOrderStatusGroups.COMPLETE) & Q(shipment_date__gte=min_date) & Q(shipment_date__lte=max_date)
|
||||||
|
|
||||||
# Construct a queryset for "pending" orders within the range
|
# Construct a queryset for "pending" orders within the range
|
||||||
pending = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
pending = Q(status__in=SalesOrderStatusGroups.OPEN) & ~Q(target_date=None) & Q(target_date__gte=min_date) & Q(target_date__lte=max_date)
|
||||||
|
|
||||||
# TODO: Construct a queryset for "overdue" orders within the range
|
# TODO: Construct a queryset for "overdue" orders within the range
|
||||||
|
|
||||||
@ -783,7 +786,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
return self.customer
|
return self.customer
|
||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
default=SalesOrderStatus.PENDING,
|
default=SalesOrderStatus.PENDING.value,
|
||||||
choices=SalesOrderStatus.items(),
|
choices=SalesOrderStatus.items(),
|
||||||
verbose_name=_('Status'), help_text=_('Purchase order status')
|
verbose_name=_('Status'), help_text=_('Purchase order status')
|
||||||
)
|
)
|
||||||
@ -813,7 +816,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
@property
|
@property
|
||||||
def is_open(self):
|
def is_open(self):
|
||||||
"""Return True if this order is 'open' (either 'pending' or 'in_progress')"""
|
"""Return True if this order is 'open' (either 'pending' or 'in_progress')"""
|
||||||
return self.status in SalesOrderStatus.OPEN
|
return self.status in SalesOrderStatusGroups.OPEN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stock_allocations(self):
|
def stock_allocations(self):
|
||||||
@ -881,7 +884,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
|
"""Change this order from 'PENDING' to 'IN_PROGRESS'"""
|
||||||
|
|
||||||
if self.status == SalesOrderStatus.PENDING:
|
if self.status == SalesOrderStatus.PENDING:
|
||||||
self.status = SalesOrderStatus.IN_PROGRESS
|
self.status = SalesOrderStatus.IN_PROGRESS.value
|
||||||
self.issue_date = datetime.now().date()
|
self.issue_date = datetime.now().date()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -892,7 +895,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
if not self.can_complete(**kwargs):
|
if not self.can_complete(**kwargs):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.status = SalesOrderStatus.SHIPPED
|
self.status = SalesOrderStatus.SHIPPED.value
|
||||||
self.shipped_by = user
|
self.shipped_by = user
|
||||||
self.shipment_date = datetime.now()
|
self.shipment_date = datetime.now()
|
||||||
|
|
||||||
@ -921,7 +924,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
if not self.can_cancel():
|
if not self.can_cancel():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.status = SalesOrderStatus.CANCELLED
|
self.status = SalesOrderStatus.CANCELLED.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
for line in self.lines.all():
|
for line in self.lines.all():
|
||||||
@ -1696,7 +1699,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_status_class(cls):
|
def get_status_class(cls):
|
||||||
"""Return the ReturnOrderStatus class"""
|
"""Return the ReturnOrderStatus class"""
|
||||||
return ReturnOrderStatus
|
return ReturnOrderStatusGroups
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def api_defaults(cls, request):
|
def api_defaults(cls, request):
|
||||||
@ -1742,7 +1745,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
return self.customer
|
return self.customer
|
||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
default=ReturnOrderStatus.PENDING,
|
default=ReturnOrderStatus.PENDING.value,
|
||||||
choices=ReturnOrderStatus.items(),
|
choices=ReturnOrderStatus.items(),
|
||||||
verbose_name=_('Status'), help_text=_('Return order status')
|
verbose_name=_('Status'), help_text=_('Return order status')
|
||||||
)
|
)
|
||||||
@ -1773,7 +1776,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
@property
|
@property
|
||||||
def is_open(self):
|
def is_open(self):
|
||||||
"""Return True if this order is outstanding"""
|
"""Return True if this order is outstanding"""
|
||||||
return self.status in ReturnOrderStatus.OPEN
|
return self.status in ReturnOrderStatusGroups.OPEN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_received(self):
|
def is_received(self):
|
||||||
@ -1784,7 +1787,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
def cancel_order(self):
|
def cancel_order(self):
|
||||||
"""Cancel this ReturnOrder (if not already cancelled)"""
|
"""Cancel this ReturnOrder (if not already cancelled)"""
|
||||||
if self.status != ReturnOrderStatus.CANCELLED:
|
if self.status != ReturnOrderStatus.CANCELLED:
|
||||||
self.status = ReturnOrderStatus.CANCELLED
|
self.status = ReturnOrderStatus.CANCELLED.value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
trigger_event('returnorder.cancelled', id=self.pk)
|
trigger_event('returnorder.cancelled', id=self.pk)
|
||||||
@ -1794,7 +1797,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
"""Complete this ReturnOrder (if not already completed)"""
|
"""Complete this ReturnOrder (if not already completed)"""
|
||||||
|
|
||||||
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
if self.status == ReturnOrderStatus.IN_PROGRESS:
|
||||||
self.status = ReturnOrderStatus.COMPLETE
|
self.status = ReturnOrderStatus.COMPLETE.value
|
||||||
self.complete_date = datetime.now().date()
|
self.complete_date = datetime.now().date()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -1809,7 +1812,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
"""Issue this ReturnOrder (if currently pending)"""
|
"""Issue this ReturnOrder (if currently pending)"""
|
||||||
|
|
||||||
if self.status == ReturnOrderStatus.PENDING:
|
if self.status == ReturnOrderStatus.PENDING:
|
||||||
self.status = ReturnOrderStatus.IN_PROGRESS
|
self.status = ReturnOrderStatus.IN_PROGRESS.value
|
||||||
self.issue_date = datetime.now().date()
|
self.issue_date = datetime.now().date()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -1833,7 +1836,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
stock_item = line.item
|
stock_item = line.item
|
||||||
|
|
||||||
deltas = {
|
deltas = {
|
||||||
'status': StockStatus.QUARANTINED,
|
'status': StockStatus.QUARANTINED.value,
|
||||||
'returnorder': self.pk,
|
'returnorder': self.pk,
|
||||||
'location': location.pk,
|
'location': location.pk,
|
||||||
}
|
}
|
||||||
@ -1842,7 +1845,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
deltas['customer'] = stock_item.customer.pk
|
deltas['customer'] = stock_item.customer.pk
|
||||||
|
|
||||||
# Update the StockItem
|
# Update the StockItem
|
||||||
stock_item.status = StockStatus.QUARANTINED
|
stock_item.status = StockStatus.QUARANTINED.value
|
||||||
stock_item.location = location
|
stock_item.location = location
|
||||||
stock_item.customer = None
|
stock_item.customer = None
|
||||||
stock_item.sales_order = None
|
stock_item.sales_order = None
|
||||||
@ -1926,7 +1929,7 @@ class ReturnOrderLineItem(OrderLineItem):
|
|||||||
return self.received_date is not None
|
return self.received_date is not None
|
||||||
|
|
||||||
outcome = models.PositiveIntegerField(
|
outcome = models.PositiveIntegerField(
|
||||||
default=ReturnOrderLineStatus.PENDING,
|
default=ReturnOrderLineStatus.PENDING.value,
|
||||||
choices=ReturnOrderLineStatus.items(),
|
choices=ReturnOrderLineStatus.items(),
|
||||||
verbose_name=_('Outcome'), help_text=_('Outcome for this line item')
|
verbose_name=_('Outcome'), help_text=_('Outcome for this line item')
|
||||||
)
|
)
|
||||||
|
@ -27,8 +27,9 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer,
|
|||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
InvenTreeMoneySerializer)
|
InvenTreeMoneySerializer)
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatusGroups,
|
||||||
SalesOrderStatus, StockStatus)
|
ReturnOrderStatus, SalesOrderStatusGroups,
|
||||||
|
StockStatus)
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
from users.serializers import OwnerSerializer
|
from users.serializers import OwnerSerializer
|
||||||
|
|
||||||
@ -381,7 +382,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def validate_purchase_order(self, purchase_order):
|
def validate_purchase_order(self, purchase_order):
|
||||||
"""Validation for the 'purchase_order' field"""
|
"""Validation for the 'purchase_order' field"""
|
||||||
if purchase_order.status not in PurchaseOrderStatus.OPEN:
|
if purchase_order.status not in PurchaseOrderStatusGroups.OPEN:
|
||||||
raise ValidationError(_('Order is not open'))
|
raise ValidationError(_('Order is not open'))
|
||||||
|
|
||||||
return purchase_order
|
return purchase_order
|
||||||
@ -518,8 +519,8 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
status = serializers.ChoiceField(
|
status = serializers.ChoiceField(
|
||||||
choices=list(StockStatus.items()),
|
choices=StockStatus.items(),
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK.value,
|
||||||
label=_('Status'),
|
label=_('Status'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -906,7 +907,7 @@ class SalesOrderLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
overdue=Case(
|
overdue=Case(
|
||||||
When(
|
When(
|
||||||
Q(order__status__in=SalesOrderStatus.OPEN) & order.models.SalesOrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
|
Q(order__status__in=SalesOrderStatusGroups.OPEN) & order.models.SalesOrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
|
||||||
),
|
),
|
||||||
default=Value(False, output_field=BooleanField()),
|
default=Value(False, output_field=BooleanField()),
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import common.notifications
|
import common.notifications
|
||||||
import InvenTree.helpers_model
|
import InvenTree.helpers_model
|
||||||
import order.models
|
import order.models
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import (PurchaseOrderStatusGroups,
|
||||||
|
SalesOrderStatusGroups)
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ def check_overdue_purchase_orders():
|
|||||||
|
|
||||||
overdue_orders = order.models.PurchaseOrder.objects.filter(
|
overdue_orders = order.models.PurchaseOrder.objects.filter(
|
||||||
target_date=yesterday,
|
target_date=yesterday,
|
||||||
status__in=PurchaseOrderStatus.OPEN
|
status__in=PurchaseOrderStatusGroups.OPEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
for po in overdue_orders:
|
for po in overdue_orders:
|
||||||
@ -131,7 +132,7 @@ def check_overdue_sales_orders():
|
|||||||
|
|
||||||
overdue_orders = order.models.SalesOrder.objects.filter(
|
overdue_orders = order.models.SalesOrder.objects.filter(
|
||||||
target_date=yesterday,
|
target_date=yesterday,
|
||||||
status__in=SalesOrderStatus.OPEN
|
status__in=SalesOrderStatusGroups.OPEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
for po in overdue_orders:
|
for po in overdue_orders:
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Purchase Order" %}
|
{% inventree_title %} | {% trans "Purchase Order" %}
|
||||||
@ -121,7 +121,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% purchase_order_status_label order.status %}
|
{% status_label 'purchase_order' order.status %}
|
||||||
{% if order.is_overdue %}
|
{% if order.is_overdue %}
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "order/order_base.html" %}
|
{% extends "order/order_base.html" %}
|
||||||
|
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Return Order" %}
|
{% inventree_title %} | {% trans "Return Order" %}
|
||||||
@ -113,7 +113,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% return_order_status_label order.status %}
|
{% status_label 'return_order' order.status %}
|
||||||
{% if order.is_overdue %}
|
{% if order.is_overdue %}
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "order/return_order_base.html" %}
|
{% extends "order/return_order_base.html" %}
|
||||||
|
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% inventree_title %} | {% trans "Sales Order" %}
|
{% inventree_title %} | {% trans "Sales Order" %}
|
||||||
@ -118,7 +118,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Order Status" %}</td>
|
<td>{% trans "Order Status" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% sales_order_status_label order.status %}
|
{% status_label 'sales_order' order.status %}
|
||||||
{% if order.is_overdue %}
|
{% if order.is_overdue %}
|
||||||
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
<span class='badge rounded-pill bg-danger'>{% trans "Overdue" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "order/sales_order_base.html" %}
|
{% extends "order/sales_order_base.html" %}
|
||||||
|
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from common.settings import currency_codes
|
|||||||
from company.models import Company
|
from company.models import Company
|
||||||
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||||
ReturnOrderStatus, SalesOrderStatus,
|
ReturnOrderStatus, SalesOrderStatus,
|
||||||
StockStatus)
|
SalesOrderStatusGroups, StockStatus)
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from order import models
|
from order import models
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -562,7 +562,7 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
# Test without completed orders
|
# Test without completed orders
|
||||||
response = self.get(url, expected_code=200, format=None)
|
response = self.get(url, expected_code=200, format=None)
|
||||||
|
|
||||||
number_orders = len(models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE))
|
number_orders = len(models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE.value))
|
||||||
|
|
||||||
# Transform content to a Calendar object
|
# Transform content to a Calendar object
|
||||||
calendar = Calendar.from_ical(response.content)
|
calendar = Calendar.from_ical(response.content)
|
||||||
@ -743,7 +743,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
|
|
||||||
# Mark the order as "placed" so we can receive line items
|
# Mark the order as "placed" so we can receive line items
|
||||||
order = models.PurchaseOrder.objects.get(pk=1)
|
order = models.PurchaseOrder.objects.get(pk=1)
|
||||||
order.status = PurchaseOrderStatus.PLACED
|
order.status = PurchaseOrderStatus.PLACED.value
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
@ -944,7 +944,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
# Before posting "valid" data, we will mark the purchase order as "pending"
|
# Before posting "valid" data, we will mark the purchase order as "pending"
|
||||||
# In this case we do expect an error!
|
# In this case we do expect an error!
|
||||||
order = models.PurchaseOrder.objects.get(pk=1)
|
order = models.PurchaseOrder.objects.get(pk=1)
|
||||||
order.status = PurchaseOrderStatus.PENDING
|
order.status = PurchaseOrderStatus.PENDING.value
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
@ -956,7 +956,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
self.assertIn('can only be received against', str(response.data))
|
self.assertIn('can only be received against', str(response.data))
|
||||||
|
|
||||||
# Now, set the PurchaseOrder back to "PLACED" so the items can be received
|
# Now, set the PurchaseOrder back to "PLACED" so the items can be received
|
||||||
order.status = PurchaseOrderStatus.PLACED
|
order.status = PurchaseOrderStatus.PLACED.value
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
# Receive two separate line items against this order
|
# Receive two separate line items against this order
|
||||||
@ -1388,7 +1388,7 @@ class SalesOrderTest(OrderTest):
|
|||||||
# Test without completed orders
|
# Test without completed orders
|
||||||
response = self.get(url, expected_code=200, format=None)
|
response = self.get(url, expected_code=200, format=None)
|
||||||
|
|
||||||
number_orders = len(models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED))
|
number_orders = len(models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED.value))
|
||||||
|
|
||||||
# Transform content to a Calendar object
|
# Transform content to a Calendar object
|
||||||
calendar = Calendar.from_ical(response.content)
|
calendar = Calendar.from_ical(response.content)
|
||||||
@ -1621,7 +1621,7 @@ class SalesOrderDownloadTest(OrderTest):
|
|||||||
file,
|
file,
|
||||||
required_cols=required_cols,
|
required_cols=required_cols,
|
||||||
excluded_cols=excluded_cols,
|
excluded_cols=excluded_cols,
|
||||||
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatus.OPEN).count(),
|
required_rows=models.SalesOrder.objects.filter(status__in=SalesOrderStatusGroups.OPEN).count(),
|
||||||
delimiter='\t',
|
delimiter='\t',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ class TestShipmentMigration(MigratorTestCase):
|
|||||||
reference=f'SO{ii}',
|
reference=f'SO{ii}',
|
||||||
customer=customer,
|
customer=customer,
|
||||||
description='A sales order for stuffs',
|
description='A sales order for stuffs',
|
||||||
status=SalesOrderStatus.PENDING,
|
status=SalesOrderStatus.PENDING.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
|
@ -29,8 +29,9 @@ from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
|
|||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI,
|
||||||
UpdateAPI)
|
UpdateAPI)
|
||||||
from InvenTree.permissions import RolePermission
|
from InvenTree.permissions import RolePermission
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatusGroups,
|
||||||
SalesOrderStatus)
|
PurchaseOrderStatusGroups,
|
||||||
|
SalesOrderStatusGroups)
|
||||||
from part.admin import PartCategoryResource, PartResource
|
from part.admin import PartCategoryResource, PartResource
|
||||||
|
|
||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
@ -479,7 +480,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
# Add purchase order (incoming stock) information
|
# Add purchase order (incoming stock) information
|
||||||
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
|
po_lines = order.models.PurchaseOrderLineItem.objects.filter(
|
||||||
part__part=part,
|
part__part=part,
|
||||||
order__status__in=PurchaseOrderStatus.OPEN,
|
order__status__in=PurchaseOrderStatusGroups.OPEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in po_lines:
|
for line in po_lines:
|
||||||
@ -502,7 +503,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
# Add sales order (outgoing stock) information
|
# Add sales order (outgoing stock) information
|
||||||
so_lines = order.models.SalesOrderLineItem.objects.filter(
|
so_lines = order.models.SalesOrderLineItem.objects.filter(
|
||||||
part=part,
|
part=part,
|
||||||
order__status__in=SalesOrderStatus.OPEN,
|
order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in so_lines:
|
for line in so_lines:
|
||||||
@ -522,7 +523,7 @@ class PartScheduling(RetrieveAPI):
|
|||||||
# Add build orders (incoming stock) information
|
# Add build orders (incoming stock) information
|
||||||
build_orders = Build.objects.filter(
|
build_orders = Build.objects.filter(
|
||||||
part=part,
|
part=part,
|
||||||
status__in=BuildStatus.ACTIVE_CODES
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
for build in build_orders:
|
for build in build_orders:
|
||||||
@ -567,12 +568,12 @@ class PartScheduling(RetrieveAPI):
|
|||||||
# An "inherited" BOM item filters down to variant parts also
|
# An "inherited" BOM item filters down to variant parts also
|
||||||
children = bom_item.part.get_descendants(include_self=True)
|
children = bom_item.part.get_descendants(include_self=True)
|
||||||
builds = Build.objects.filter(
|
builds = Build.objects.filter(
|
||||||
status__in=BuildStatus.ACTIVE_CODES,
|
status__in=BuildStatusGroups.ACTIVE_CODES,
|
||||||
part__in=children,
|
part__in=children,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
builds = Build.objects.filter(
|
builds = Build.objects.filter(
|
||||||
status__in=BuildStatus.ACTIVE_CODES,
|
status__in=BuildStatusGroups.ACTIVE_CODES,
|
||||||
part=bom_item.part,
|
part=bom_item.part,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1197,7 +1198,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
|
|
||||||
if stock_to_build is not None:
|
if stock_to_build is not None:
|
||||||
# Get active builds
|
# Get active builds
|
||||||
builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
builds = Build.objects.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
# Store parts with builds needing stock
|
# Store parts with builds needing stock
|
||||||
parts_needed_to_complete_builds = []
|
parts_needed_to_complete_builds = []
|
||||||
# Filter required parts
|
# Filter required parts
|
||||||
|
@ -28,8 +28,9 @@ from sql_util.utils import SubquerySum
|
|||||||
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatusGroups,
|
||||||
SalesOrderStatus)
|
PurchaseOrderStatusGroups,
|
||||||
|
SalesOrderStatusGroups)
|
||||||
|
|
||||||
|
|
||||||
def annotate_on_order_quantity(reference: str = ''):
|
def annotate_on_order_quantity(reference: str = ''):
|
||||||
@ -46,7 +47,7 @@ def annotate_on_order_quantity(reference: str = ''):
|
|||||||
# Filter only 'active' purhase orders
|
# Filter only 'active' purhase orders
|
||||||
# Filter only line with outstanding quantity
|
# Filter only line with outstanding quantity
|
||||||
order_filter = Q(
|
order_filter = Q(
|
||||||
order__status__in=PurchaseOrderStatus.OPEN,
|
order__status__in=PurchaseOrderStatusGroups.OPEN,
|
||||||
quantity__gt=F('received'),
|
quantity__gt=F('received'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ def annotate_build_order_allocations(reference: str = ''):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Build filter only returns 'active' build orders
|
# Build filter only returns 'active' build orders
|
||||||
build_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES)
|
build_filter = Q(build__status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
return Coalesce(
|
return Coalesce(
|
||||||
SubquerySum(
|
SubquerySum(
|
||||||
@ -137,7 +138,7 @@ def annotate_sales_order_allocations(reference: str = ''):
|
|||||||
|
|
||||||
# Order filter only returns incomplete shipments for open orders
|
# Order filter only returns incomplete shipments for open orders
|
||||||
order_filter = Q(
|
order_filter = Q(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None,
|
shipment__shipment_date=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,8 +51,9 @@ from InvenTree.helpers import (decimal2money, decimal2string, normalize,
|
|||||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||||
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
||||||
InvenTreeTree, MetadataMixin)
|
InvenTreeTree, MetadataMixin)
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatusGroups, PurchaseOrderStatus,
|
||||||
SalesOrderStatus)
|
PurchaseOrderStatusGroups,
|
||||||
|
SalesOrderStatus, SalesOrderStatusGroups)
|
||||||
from order import models as OrderModels
|
from order import models as OrderModels
|
||||||
from stock import models as StockModels
|
from stock import models as StockModels
|
||||||
|
|
||||||
@ -1070,7 +1071,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
# Now, get a list of outstanding build orders which require this part
|
# Now, get a list of outstanding build orders which require this part
|
||||||
builds = BuildModels.Build.objects.filter(
|
builds = BuildModels.Build.objects.filter(
|
||||||
part__in=self.get_used_in(),
|
part__in=self.get_used_in(),
|
||||||
status__in=BuildStatus.ACTIVE_CODES
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
return builds
|
return builds
|
||||||
@ -1104,7 +1105,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
# Get a list of line items for open orders which match this part
|
# Get a list of line items for open orders which match this part
|
||||||
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
order__status__in=SalesOrderStatus.OPEN,
|
order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
part=self
|
part=self
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1117,7 +1118,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
"""Return the quantity of this part required for active sales orders."""
|
"""Return the quantity of this part required for active sales orders."""
|
||||||
# Get a list of line items for open orders which match this part
|
# Get a list of line items for open orders which match this part
|
||||||
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
order__status__in=SalesOrderStatus.OPEN,
|
order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
part=self
|
part=self
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1329,7 +1330,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
Builds marked as 'complete' or 'cancelled' are ignored
|
Builds marked as 'complete' or 'cancelled' are ignored
|
||||||
"""
|
"""
|
||||||
return self.builds.filter(status__in=BuildStatus.ACTIVE_CODES)
|
return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_being_built(self):
|
def quantity_being_built(self):
|
||||||
@ -1401,13 +1402,13 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
if pending is True:
|
if pending is True:
|
||||||
# Look only for 'open' orders which have not shipped
|
# Look only for 'open' orders which have not shipped
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None,
|
shipment__shipment_date=None,
|
||||||
)
|
)
|
||||||
elif pending is False:
|
elif pending is False:
|
||||||
# Look only for 'closed' orders or orders which have shipped
|
# Look only for 'closed' orders or orders which have shipped
|
||||||
queryset = queryset.exclude(
|
queryset = queryset.exclude(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None,
|
shipment__shipment_date=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2161,7 +2162,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
|
|||||||
|
|
||||||
# Look at any incomplete line item for open orders
|
# Look at any incomplete line item for open orders
|
||||||
lines = sp.purchase_order_line_items.filter(
|
lines = sp.purchase_order_line_items.filter(
|
||||||
order__status__in=PurchaseOrderStatus.OPEN,
|
order__status__in=PurchaseOrderStatusGroups.OPEN,
|
||||||
quantity__gt=F('received'),
|
quantity__gt=F('received'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2559,7 +2560,7 @@ class PartPricing(common.models.MetaMixin):
|
|||||||
|
|
||||||
# Find all line items for completed orders which reference this part
|
# Find all line items for completed orders which reference this part
|
||||||
line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
|
line_items = OrderModels.PurchaseOrderLineItem.objects.filter(
|
||||||
order__status=PurchaseOrderStatus.COMPLETE,
|
order__status=PurchaseOrderStatus.COMPLETE.value,
|
||||||
received__gt=0,
|
received__gt=0,
|
||||||
part__part=self.part,
|
part__part=self.part,
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,7 @@ import InvenTree.status
|
|||||||
import part.filters
|
import part.filters
|
||||||
import part.tasks
|
import part.tasks
|
||||||
import stock.models
|
import stock.models
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatusGroups
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
|
|
||||||
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
|
from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
|
||||||
@ -532,7 +532,7 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize
|
|||||||
|
|
||||||
# Filter to limit builds to "active"
|
# Filter to limit builds to "active"
|
||||||
build_filter = Q(
|
build_filter = Q(
|
||||||
status__in=BuildStatus.ACTIVE_CODES
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate with the total 'building' quantity
|
# Annotate with the total 'building' quantity
|
||||||
|
@ -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
|
import order.models
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatusGroups,
|
||||||
StockStatus)
|
StockStatus)
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from part.models import (BomItem, BomItemSubstitute, Part, PartCategory,
|
from part.models import (BomItem, BomItemSubstitute, Part, PartCategory,
|
||||||
@ -1628,7 +1628,7 @@ class PartDetailTests(PartAPITestBase):
|
|||||||
# How many parts are 'on order' for this part?
|
# How many parts are 'on order' for this part?
|
||||||
lines = order.models.PurchaseOrderLineItem.objects.filter(
|
lines = order.models.PurchaseOrderLineItem.objects.filter(
|
||||||
part__part__pk=1,
|
part__part__pk=1,
|
||||||
order__status__in=PurchaseOrderStatus.OPEN,
|
order__status__in=PurchaseOrderStatusGroups.OPEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
on_order = 0
|
on_order = 0
|
||||||
@ -1857,7 +1857,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
StockItem.objects.create(part=cls.part, quantity=300)
|
StockItem.objects.create(part=cls.part, quantity=300)
|
||||||
|
|
||||||
# Now create another 400 units which are LOST
|
# Now create another 400 units which are LOST
|
||||||
StockItem.objects.create(part=cls.part, quantity=400, status=StockStatus.LOST)
|
StockItem.objects.create(part=cls.part, quantity=400, status=StockStatus.LOST.value)
|
||||||
|
|
||||||
def get_part_data(self):
|
def get_part_data(self):
|
||||||
"""Helper function for retrieving part data"""
|
"""Helper function for retrieving part data"""
|
||||||
@ -1992,7 +1992,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
quantity=10,
|
quantity=10,
|
||||||
title='Making some assemblies',
|
title='Making some assemblies',
|
||||||
reference='BO-9999',
|
reference='BO-9999',
|
||||||
status=BuildStatus.PRODUCTION,
|
status=BuildStatus.PRODUCTION.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
bom_item = BomItem.objects.get(pk=6)
|
bom_item = BomItem.objects.get(pk=6)
|
||||||
@ -2133,7 +2133,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
for line_item in sp.purchase_order_line_items.all():
|
for line_item in sp.purchase_order_line_items.all():
|
||||||
po = line_item.order
|
po = line_item.order
|
||||||
|
|
||||||
if po.status in PurchaseOrderStatus.OPEN:
|
if po.status in PurchaseOrderStatusGroups.OPEN:
|
||||||
remaining = line_item.quantity - line_item.received
|
remaining = line_item.quantity - line_item.received
|
||||||
|
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
|
@ -345,7 +345,7 @@ class PartPricingTests(InvenTreeTestCase):
|
|||||||
self.assertIsNone(pricing.purchase_cost_min)
|
self.assertIsNone(pricing.purchase_cost_min)
|
||||||
self.assertIsNone(pricing.purchase_cost_max)
|
self.assertIsNone(pricing.purchase_cost_max)
|
||||||
|
|
||||||
po.status = PurchaseOrderStatus.COMPLETE
|
po.status = PurchaseOrderStatus.COMPLETE.value
|
||||||
po.save()
|
po.save()
|
||||||
|
|
||||||
pricing.update_purchase_cost()
|
pricing.update_purchase_cost()
|
||||||
|
@ -22,8 +22,9 @@ from build.models import Build
|
|||||||
from build.serializers import BuildSerializer
|
from build.serializers import BuildSerializer
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
|
from generic.states import StatusView
|
||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView, MetadataView, StatusView)
|
ListCreateDestroyAPIView, MetadataView)
|
||||||
from InvenTree.filters import (ORDER_FILTER, SEARCH_ORDER_FILTER,
|
from InvenTree.filters import (ORDER_FILTER, SEARCH_ORDER_FILTER,
|
||||||
SEARCH_ORDER_FILTER_ALIAS)
|
SEARCH_ORDER_FILTER_ALIAS)
|
||||||
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
||||||
|
@ -190,7 +190,7 @@ def update_history(apps, schema_editor):
|
|||||||
tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER
|
tracking_type = StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER
|
||||||
|
|
||||||
if tracking_type is not None:
|
if tracking_type is not None:
|
||||||
entry.tracking_type = tracking_type
|
entry.tracking_type = tracking_type.value
|
||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
|
@ -43,7 +43,7 @@ def update_stock_history(apps, schema_editor):
|
|||||||
history.deltas['salesorder'] = item.sales_order.pk
|
history.deltas['salesorder'] = item.sales_order.pk
|
||||||
|
|
||||||
# Change the history type
|
# Change the history type
|
||||||
history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
|
history.tracking_type = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER.value
|
||||||
|
|
||||||
history.save()
|
history.save()
|
||||||
n += 1
|
n += 1
|
||||||
|
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,
|
from InvenTree.models import (InvenTreeAttachment, InvenTreeBarcodeMixin,
|
||||||
InvenTreeNotesMixin, InvenTreeTree,
|
InvenTreeNotesMixin, InvenTreeTree,
|
||||||
MetadataMixin, extract_int)
|
MetadataMixin, extract_int)
|
||||||
from InvenTree.status_codes import (SalesOrderStatus, StockHistoryCode,
|
from InvenTree.status_codes import (SalesOrderStatusGroups, StockHistoryCode,
|
||||||
StockStatus)
|
StockStatus, StockStatusGroups)
|
||||||
from part import models as PartModels
|
from part import models as PartModels
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
@ -334,7 +334,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
customer=None,
|
customer=None,
|
||||||
consumed_by=None,
|
consumed_by=None,
|
||||||
is_building=False,
|
is_building=False,
|
||||||
status__in=StockStatus.AVAILABLE_CODES
|
status__in=StockStatusGroups.AVAILABLE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
# A query filter which can be used to filter StockItem objects which have expired
|
# A query filter which can be used to filter StockItem objects which have expired
|
||||||
@ -806,7 +806,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
)
|
)
|
||||||
|
|
||||||
status = models.PositiveIntegerField(
|
status = models.PositiveIntegerField(
|
||||||
default=StockStatus.OK,
|
default=StockStatus.OK.value,
|
||||||
choices=StockStatus.items(),
|
choices=StockStatus.items(),
|
||||||
validators=[MinValueValidator(0)])
|
validators=[MinValueValidator(0)])
|
||||||
|
|
||||||
@ -1082,12 +1082,12 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
|
|
||||||
if active is True:
|
if active is True:
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN,
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
shipment__shipment_date=None
|
shipment__shipment_date=None
|
||||||
)
|
)
|
||||||
elif active is False:
|
elif active is False:
|
||||||
query = query.exclude(
|
query = query.exclude(
|
||||||
line__order__status__in=SalesOrderStatus.OPEN
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
).exclude(
|
).exclude(
|
||||||
shipment__shipment_date=None
|
shipment__shipment_date=None
|
||||||
)
|
)
|
||||||
@ -1346,7 +1346,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo
|
|||||||
|
|
||||||
entry = StockItemTracking.objects.create(
|
entry = StockItemTracking.objects.create(
|
||||||
item=self,
|
item=self,
|
||||||
tracking_type=entry_type,
|
tracking_type=entry_type.value,
|
||||||
user=user,
|
user=user,
|
||||||
date=datetime.now(),
|
date=datetime.now(),
|
||||||
notes=notes,
|
notes=notes,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load plugin_extras %}
|
{% load plugin_extras %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
|
|
||||||
@ -421,7 +421,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-info'></span></td>
|
<td><span class='fas fa-info'></span></td>
|
||||||
<td>{% trans "Status" %}</td>
|
<td>{% trans "Status" %}</td>
|
||||||
<td>{% stock_status_label item.status %}</td>
|
<td>{% status_label 'stock' item.status %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if item.expiry_date %}
|
{% if item.expiry_date %}
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -385,11 +385,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
def test_filter_by_status(self):
|
def test_filter_by_status(self):
|
||||||
"""Filter StockItem by 'status' field."""
|
"""Filter StockItem by 'status' field."""
|
||||||
codes = {
|
codes = {
|
||||||
StockStatus.OK: 27,
|
StockStatus.OK.value: 27,
|
||||||
StockStatus.DESTROYED: 1,
|
StockStatus.DESTROYED.value: 1,
|
||||||
StockStatus.LOST: 1,
|
StockStatus.LOST.value: 1,
|
||||||
StockStatus.DAMAGED: 0,
|
StockStatus.DAMAGED.value: 0,
|
||||||
StockStatus.REJECTED: 0,
|
StockStatus.REJECTED.value: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
for code in codes.keys():
|
for code in codes.keys():
|
||||||
@ -1465,7 +1465,7 @@ class StockAssignTest(StockAPITestCase):
|
|||||||
|
|
||||||
stock_item = StockItem.objects.create(
|
stock_item = StockItem.objects.create(
|
||||||
part=part.models.Part.objects.get(pk=1),
|
part=part.models.Part.objects.get(pk=1),
|
||||||
status=StockStatus.DESTROYED,
|
status=StockStatus.DESTROYED.value,
|
||||||
quantity=5,
|
quantity=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ class StockOwnershipTest(StockViewTestCase):
|
|||||||
"""Helper function to get response to API change."""
|
"""Helper function to get response to API change."""
|
||||||
return self.client.patch(
|
return self.client.patch(
|
||||||
reverse('api-stock-detail', args=(self.test_item_id,)),
|
reverse('api-stock-detail', args=(self.test_item_id,)),
|
||||||
{'status': StockStatus.DAMAGED},
|
{'status': StockStatus.DAMAGED.value},
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ class StockOwnershipTest(StockViewTestCase):
|
|||||||
# Check that user is allowed to change item
|
# Check that user is allowed to change item
|
||||||
self.assertTrue(item.check_ownership(self.user)) # Owner is group -> True
|
self.assertTrue(item.check_ownership(self.user)) # Owner is group -> True
|
||||||
self.assertTrue(location.check_ownership(self.user)) # Owner is group -> True
|
self.assertTrue(location.check_ownership(self.user)) # Owner is group -> True
|
||||||
self.assertContains(self.assert_api_change(), f'"status":{StockStatus.DAMAGED}', status_code=200)
|
self.assertContains(self.assert_api_change(), f'"status":{StockStatus.DAMAGED.value}', status_code=200)
|
||||||
|
|
||||||
# Change group
|
# Change group
|
||||||
new_group = Group.objects.create(name='new_group')
|
new_group = Group.objects.create(name='new_group')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
addCachedAlert,
|
addCachedAlert,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load status_codes %}
|
{% load generic %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
/* globals
|
/* globals
|
||||||
|
Loading…
x
Reference in New Issue
Block a user