mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
f4f7803e96
commit
327ecf2156
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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'),
|
||||
]
|
||||
|
@ -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'),
|
||||
])),
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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'),
|
||||
]
|
||||
|
@ -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 %}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user