Implement a generic API endpoint for enumeration of status codes (#4543)

* Implement a generic API endpoint for enumeration of status codes

* Adds endpoint for PurchaseOrderStatus

* Add more endpoints (sales order / return orer)

* Add endpoints for StockStatus and StockTrackingCode

* Support build status

* Use the attribute name as the dict key

* Refactored status codes in javascript

- Now accessible by "name" (instead of integer key)
- Will make javascript code much more readable

* Bump API version
This commit is contained in:
Oliver 2023-03-31 07:27:24 +11:00 committed by GitHub
parent f4f7803e96
commit 327ecf2156
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 61 deletions

View File

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

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 104
INVENTREE_API_VERSION = 105
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v105 -> 2023-03-31 : https://github.com/inventree/InvenTree/pull/4543
- Adds API endpoints for status label information on various models
v104 -> 2023-03-23 : https://github.com/inventree/InvenTree/pull/4488
- Adds various endpoints for new "ReturnOrder" models
- Adds various endpoints for new "ReturnOrderReport" templates

View File

@ -31,23 +31,7 @@ class StatusCode:
@classmethod
def list(cls):
"""Return the StatusCode options as a list of mapped key / value items."""
codes = []
for key in cls.options.keys():
opt = {
'key': key,
'value': cls.options[key]
}
color = cls.colors.get(key, None)
if color:
opt['color'] = color
codes.append(opt)
return codes
return list(cls.dict().values())
@classmethod
def text(cls, key):
@ -69,6 +53,62 @@ class StatusCode:
"""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."""

View File

@ -10,7 +10,7 @@ from rest_framework.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, StatusView
from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.status_codes import BuildStatus
@ -536,6 +536,9 @@ build_api_urls = [
re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
])),
# Build order status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: BuildStatus}, name='api-build-status-codes'),
# Build List
re_path(r'^.*$', BuildList.as_view(), name='api-build-list'),
]

View File

@ -20,12 +20,13 @@ from common.models import InvenTreeSetting
from common.settings import settings
from company.models import SupplierPart
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView)
ListCreateDestroyAPIView, StatusView)
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
ReturnOrderStatus, SalesOrderStatus)
from order.admin import (PurchaseOrderExtraLineResource,
PurchaseOrderLineItemResource, PurchaseOrderResource,
ReturnOrderResource, SalesOrderExtraLineResource,
@ -1610,6 +1611,9 @@ order_api_urls = [
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
])),
# Purchase order status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: PurchaseOrderStatus}, name='api-po-status-codes'),
# Purchase order list
re_path(r'^.*$', PurchaseOrderList.as_view(), name='api-po-list'),
])),
@ -1661,6 +1665,9 @@ order_api_urls = [
re_path(r'^.*$', SalesOrderDetail.as_view(), name='api-so-detail'),
])),
# Sales order status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: SalesOrderStatus}, name='api-so-status-codes'),
# Sales order list view
re_path(r'^.*$', SalesOrderList.as_view(), name='api-so-list'),
])),
@ -1706,6 +1713,9 @@ order_api_urls = [
re_path(r'.*$', ReturnOrderDetail.as_view(), name='api-return-order-detail'),
])),
# Return order status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: ReturnOrderStatus}, name='api-return-order-status-codes'),
# Return Order list
re_path(r'^.*$', ReturnOrderList.as_view(), name='api-return-order-list'),
])),
@ -1713,6 +1723,10 @@ order_api_urls = [
# API endpoints for reutrn order lines
re_path(r'^ro-line/', include([
path('<int:pk>/', ReturnOrderLineItemDetail.as_view(), name='api-return-order-line-detail'),
# Return order line item status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: ReturnOrderLineStatus}, name='api-return-order-line-status-codes'),
path('', ReturnOrderLineItemList.as_view(), name='api-return-order-line-list'),
])),

View File

@ -502,7 +502,7 @@ report_api_urls = [
])),
# Return order reports
re_path(r'return-order/', include([
re_path(r'ro/', include([
path(r'<int:pk>/', include([
path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'),
path('', ReturnOrderReportDetail.as_view(), name='api-return-order-report-detail'),

View File

@ -23,13 +23,14 @@ from build.models import Build
from company.models import Company, SupplierPart
from company.serializers import CompanySerializer, SupplierPartSerializer
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
ListCreateDestroyAPIView)
ListCreateDestroyAPIView, StatusView)
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
str2bool, str2int)
from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from InvenTree.status_codes import StockHistoryCode, StockStatus
from order.models import (PurchaseOrder, ReturnOrder, SalesOrder,
SalesOrderAllocation)
from order.serializers import (PurchaseOrderSerializer, ReturnOrderSerializer,
@ -1421,6 +1422,10 @@ stock_api_urls = [
# StockItemTracking API endpoints
re_path(r'^track/', include([
path(r'<int:pk>/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'),
# Stock tracking status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: StockHistoryCode}, name='api-stock-tracking-status-codes'),
re_path(r'^.*$', StockTrackingList.as_view(), name='api-stock-tracking-list'),
])),
@ -1435,6 +1440,9 @@ stock_api_urls = [
re_path(r'^.*$', StockDetail.as_view(), name='api-stock-detail'),
])),
# Stock item status code information
re_path(r'status/', StatusView.as_view(), {StatusView.MODEL_REF: StockStatus}, name='api-stock-status-codes'),
# Anything else
re_path(r'^.*$', StockList.as_view(), name='api-stock-list'),
]

View File

@ -15,10 +15,51 @@
stockStatusDisplay,
*/
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
{% include "status_codes.html" with label='stockHistory' options=StockHistoryCode.list %}
{% include "status_codes.html" with label='build' options=BuildStatus.list %}
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
{% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %}
{% include "status_codes.html" with label='returnOrder' options=ReturnOrderStatus.list %}
{% include "status_codes.html" with label='returnOrderLineItem' options=ReturnOrderLineStatus.list %}
/*
* Generic function to render a status label
*/
function renderStatusLabel(key, codes, options={}) {
let text = null;
let label = null;
// Find the entry which matches the provided key
for (var name in codes) {
let entry = codes[name];
if (entry.key == key) {
text = entry.value;
label = entry.label;
break;
}
}
if (!text) {
console.error(`renderStatusLabel could not find match for code ${key}`);
}
// Fallback for color
label = label || 'bg-dark';
if (!text) {
text = key;
}
let classes = `badge rounded-pill ${label}`;
if (options.classes) {
classes += ` ${options.classes}`;
}
return `<span class='${classes}'>${text}</span>`;
}
{% include "status_codes.html" with label='stock' data=StockStatus.list %}
{% include "status_codes.html" with label='stockHistory' data=StockHistoryCode.list %}
{% include "status_codes.html" with label='build' data=BuildStatus.list %}
{% include "status_codes.html" with label='purchaseOrder' data=PurchaseOrderStatus.list %}
{% include "status_codes.html" with label='salesOrder' data=SalesOrderStatus.list %}
{% include "status_codes.html" with label='returnOrder' data=ReturnOrderStatus.list %}
{% include "status_codes.html" with label='returnOrderLineItem' data=ReturnOrderLineStatus.list %}

View File

@ -1,11 +1,15 @@
{% load report %}
/*
* Status codes for the {{ label }} model.
* Generated from the values specified in "status_codes.py"
*/
const {{ label }}Codes = {
{% for opt in options %}'{{ opt.key }}': {
key: '{{ opt.key }}',
value: '{{ opt.value }}',{% if opt.color %}
label: 'bg-{{ opt.color }}',{% endif %}
{% for entry in data %}
'{{ entry.name }}': {
key: {{ entry.key }},
value: '{{ entry.label }}',{% if entry.color %}
label: 'bg-{{ entry.color }}',{% endif %}
},
{% endfor %}
};
@ -13,33 +17,7 @@ const {{ label }}Codes = {
/*
* Render the status for a {{ label }} object.
* Uses the values specified in "status_codes.py"
* This function is generated by the "status_codes.html" template
*/
function {{ label }}StatusDisplay(key, options={}) {
key = String(key);
var value = null;
var label = null;
if (key in {{ label }}Codes) {
value = {{ label }}Codes[key].value;
label = {{ label }}Codes[key].label;
}
// Fallback option for label
label = label || 'bg-dark';
if (value == null || value.length == 0) {
value = key;
label = '';
}
var classes = `badge rounded-pill ${label}`;
if (options.classes) {
classes += ' ' + options.classes;
}
return `<span class='${classes}'>${value}</span>`;
return renderStatusLabel(key, {{ label }}Codes, options);
}