mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-03 05:48:47 +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)
|
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
|
||||||
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
|
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
|
v104 -> 2023-03-23 : https://github.com/inventree/InvenTree/pull/4488
|
||||||
- Adds various endpoints for new "ReturnOrder" models
|
- Adds various endpoints for new "ReturnOrder" models
|
||||||
- Adds various endpoints for new "ReturnOrderReport" templates
|
- Adds various endpoints for new "ReturnOrderReport" templates
|
||||||
|
@ -31,23 +31,7 @@ class StatusCode:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def list(cls):
|
def list(cls):
|
||||||
"""Return the StatusCode options as a list of mapped key / value items."""
|
"""Return the StatusCode options as a list of mapped key / value items."""
|
||||||
codes = []
|
return list(cls.dict().values())
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def text(cls, key):
|
def text(cls, key):
|
||||||
@ -69,6 +53,62 @@ class StatusCode:
|
|||||||
"""All status code labels."""
|
"""All status code labels."""
|
||||||
return cls.options.values()
|
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
|
@classmethod
|
||||||
def label(cls, value):
|
def label(cls, value):
|
||||||
"""Return the status code label associated with the provided 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.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
|
from InvenTree.api import AttachmentMixin, APIDownloadMixin, ListCreateDestroyAPIView, StatusView
|
||||||
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
from InvenTree.helpers import str2bool, isNull, DownloadFile
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
@ -536,6 +536,9 @@ build_api_urls = [
|
|||||||
re_path(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
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
|
# Build List
|
||||||
re_path(r'^.*$', BuildList.as_view(), name='api-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 common.settings import settings
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView)
|
ListCreateDestroyAPIView, StatusView)
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import DownloadFile, str2bool
|
||||||
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
|
||||||
|
ReturnOrderStatus, SalesOrderStatus)
|
||||||
from order.admin import (PurchaseOrderExtraLineResource,
|
from order.admin import (PurchaseOrderExtraLineResource,
|
||||||
PurchaseOrderLineItemResource, PurchaseOrderResource,
|
PurchaseOrderLineItemResource, PurchaseOrderResource,
|
||||||
ReturnOrderResource, SalesOrderExtraLineResource,
|
ReturnOrderResource, SalesOrderExtraLineResource,
|
||||||
@ -1610,6 +1611,9 @@ order_api_urls = [
|
|||||||
re_path(r'.*$', PurchaseOrderDetail.as_view(), name='api-po-detail'),
|
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
|
# Purchase order list
|
||||||
re_path(r'^.*$', PurchaseOrderList.as_view(), name='api-po-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'),
|
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
|
# Sales order list view
|
||||||
re_path(r'^.*$', SalesOrderList.as_view(), name='api-so-list'),
|
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'),
|
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
|
# Return Order list
|
||||||
re_path(r'^.*$', ReturnOrderList.as_view(), name='api-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
|
# API endpoints for reutrn order lines
|
||||||
re_path(r'^ro-line/', include([
|
re_path(r'^ro-line/', include([
|
||||||
path('<int:pk>/', ReturnOrderLineItemDetail.as_view(), name='api-return-order-line-detail'),
|
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'),
|
path('', ReturnOrderLineItemList.as_view(), name='api-return-order-line-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -502,7 +502,7 @@ report_api_urls = [
|
|||||||
])),
|
])),
|
||||||
|
|
||||||
# Return order reports
|
# Return order reports
|
||||||
re_path(r'return-order/', include([
|
re_path(r'ro/', include([
|
||||||
path(r'<int:pk>/', include([
|
path(r'<int:pk>/', include([
|
||||||
path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'),
|
path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'),
|
||||||
path('', ReturnOrderReportDetail.as_view(), name='api-return-order-report-detail'),
|
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.models import Company, SupplierPart
|
||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||||
ListCreateDestroyAPIView)
|
ListCreateDestroyAPIView, StatusView)
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
|
||||||
str2bool, str2int)
|
str2bool, str2int)
|
||||||
from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
|
from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
|
||||||
ListAPI, ListCreateAPI, RetrieveAPI,
|
ListAPI, ListCreateAPI, RetrieveAPI,
|
||||||
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
|
||||||
|
from InvenTree.status_codes import StockHistoryCode, StockStatus
|
||||||
from order.models import (PurchaseOrder, ReturnOrder, SalesOrder,
|
from order.models import (PurchaseOrder, ReturnOrder, SalesOrder,
|
||||||
SalesOrderAllocation)
|
SalesOrderAllocation)
|
||||||
from order.serializers import (PurchaseOrderSerializer, ReturnOrderSerializer,
|
from order.serializers import (PurchaseOrderSerializer, ReturnOrderSerializer,
|
||||||
@ -1421,6 +1422,10 @@ stock_api_urls = [
|
|||||||
# StockItemTracking API endpoints
|
# StockItemTracking API endpoints
|
||||||
re_path(r'^track/', include([
|
re_path(r'^track/', include([
|
||||||
path(r'<int:pk>/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'),
|
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'),
|
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'),
|
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
|
# Anything else
|
||||||
re_path(r'^.*$', StockList.as_view(), name='api-stock-list'),
|
re_path(r'^.*$', StockList.as_view(), name='api-stock-list'),
|
||||||
]
|
]
|
||||||
|
@ -15,10 +15,51 @@
|
|||||||
stockStatusDisplay,
|
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 %}
|
* Generic function to render a status label
|
||||||
{% include "status_codes.html" with label='purchaseOrder' options=PurchaseOrderStatus.list %}
|
*/
|
||||||
{% include "status_codes.html" with label='salesOrder' options=SalesOrderStatus.list %}
|
function renderStatusLabel(key, codes, options={}) {
|
||||||
{% include "status_codes.html" with label='returnOrder' options=ReturnOrderStatus.list %}
|
|
||||||
{% include "status_codes.html" with label='returnOrderLineItem' options=ReturnOrderLineStatus.list %}
|
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.
|
* Status codes for the {{ label }} model.
|
||||||
|
* Generated from the values specified in "status_codes.py"
|
||||||
*/
|
*/
|
||||||
const {{ label }}Codes = {
|
const {{ label }}Codes = {
|
||||||
{% for opt in options %}'{{ opt.key }}': {
|
{% for entry in data %}
|
||||||
key: '{{ opt.key }}',
|
'{{ entry.name }}': {
|
||||||
value: '{{ opt.value }}',{% if opt.color %}
|
key: {{ entry.key }},
|
||||||
label: 'bg-{{ opt.color }}',{% endif %}
|
value: '{{ entry.label }}',{% if entry.color %}
|
||||||
|
label: 'bg-{{ entry.color }}',{% endif %}
|
||||||
},
|
},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
};
|
};
|
||||||
@ -13,33 +17,7 @@ const {{ label }}Codes = {
|
|||||||
/*
|
/*
|
||||||
* Render the status for a {{ label }} object.
|
* Render the status for a {{ label }} object.
|
||||||
* Uses the values specified in "status_codes.py"
|
* Uses the values specified in "status_codes.py"
|
||||||
* This function is generated by the "status_codes.html" template
|
|
||||||
*/
|
*/
|
||||||
function {{ label }}StatusDisplay(key, options={}) {
|
function {{ label }}StatusDisplay(key, options={}) {
|
||||||
|
return renderStatusLabel(key, {{ label }}Codes, 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>`;
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user