mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
* Added initial draft for machines * refactor: isPluginRegistryLoaded check into own ready function * Added suggestions from codereview * Refactor: base_drivers -> machine_types * Use new BaseInvenTreeSetting unique interface * Fix Django not ready error * Added get_machines function to driver - get_machines function on driver - get_machine function on driver - initialized attribute on machine * Added error handeling for driver and machine type * Extended get_machines functionality * Export everything from plugin module * Fix spelling mistakes * Better states handeling, BaseMachineType is now used instead of Machine Model * Use uuid as pk * WIP: machine termination hook * Remove termination hook as this does not work with gunicorn * Remove machine from registry after delete * Added ClassProviderMixin * Check for slug dupplication * Added config_type to MachineSettings to define machine/driver settings * Refactor helper mixins into own file in InvenTree app * Fixed typing and added required_attributes for BaseDriver * fix: generic status import * Added first draft for machine states * Added convention for status codes * Added update_machine hook * Removed unnecessary _key suffix from machine config model * Initil draft for machine API * Refactored BaseInvenTreeSetting all_items and allValues method * Added required to InvenTreeBaseSetting and check_settings method * check if all required machine settings are defined and refactor: use getattr * Fix: comment * Fix initialize error and python 3.9 compability * Make machine states available through the global states api * Added basic PUI machine admin implementation that is still in dev * Added basic machine setting UI to PUI * Added machine detail view to PUI admin center * Fix merge issues * Fix style issues * Added machine type,machine driver,error stack tables * Fix style in machine/serializers.py * Added pui link from machine to machine type/driver drawer * Removed only partially working django admin in favor of the PUI admin center implementation * Added required field to settings item * Added machine restart function * Added restart requird badge to machine table/drawer * Added driver init function * handle error functions for machines and registry * Added driver errors * Added machine table to driver drawer * Added back button to detail drawer component * Fix auto formatable pre-commit * fix: style * Fix deepsource * Removed slug field from table, added more links between drawers, remove detail drawer blur * Added initial docs * Removed description from driver/machine type select and fixed disabled driver select if no machine type is selected * Added basic label printing implementation * Remove translated column names because they are now retrieved from the api * Added printer location setting * Save last 10 used printer machine per user and sort them in the printing dialog * Added BasePrintingOptionsSerializer for common options * Fix not printing_options are not properly casted to its internal value * Fix type * Improved machine docs * Fix docs * Added UNKNOWN status code to label printer status * Skip machine loading when running migrations * Fix testing? * Fix: tests? * Fix: tests? * Disable docs check precommit * Disable docs check precommit * First draft for tests * fix test * Add type ignore * Added API tests * Test ci? * Add more tests * Added more tests * Bump api version * Changed driver/base driver naming schema * Added more tests * Fix tests * Added setting choice with kwargs and get_machines with initialized=None * Refetch table after deleting machine * Fix test --------- Co-authored-by: Matthias Mair <code@mjmair.com>
505 lines
16 KiB
Python
505 lines
16 KiB
Python
"""API functionality for the 'label' app."""
|
|
|
|
from django.core.exceptions import FieldError, ValidationError
|
|
from django.http import JsonResponse
|
|
from django.urls import include, path, re_path
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.cache import cache_page, never_cache
|
|
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import serializers
|
|
from rest_framework.exceptions import NotFound
|
|
from rest_framework.request import clone_request
|
|
|
|
import build.models
|
|
import common.models
|
|
import InvenTree.exceptions
|
|
import InvenTree.helpers
|
|
import label.models
|
|
import label.serializers
|
|
from InvenTree.api import MetadataView
|
|
from InvenTree.filters import InvenTreeSearchFilter
|
|
from InvenTree.mixins import ListCreateAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
|
|
from part.models import Part
|
|
from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
|
|
from plugin.registry import registry
|
|
from stock.models import StockItem, StockLocation
|
|
|
|
|
|
class LabelFilterMixin:
|
|
"""Mixin for filtering a queryset by a list of object ID values.
|
|
|
|
Each implementing class defines a database model to lookup,
|
|
and a "key" (query parameter) for providing a list of ID (PK) values.
|
|
|
|
This mixin defines a 'get_items' method which provides a generic
|
|
implementation to return a list of matching database model instances.
|
|
"""
|
|
|
|
# Database model for instances to actually be "printed" against this label template
|
|
ITEM_MODEL = None
|
|
|
|
# Default key for looking up database model instances
|
|
ITEM_KEY = 'item'
|
|
|
|
def get_items(self):
|
|
"""Return a list of database objects from query parameter."""
|
|
ids = []
|
|
|
|
# Construct a list of possible query parameter value options
|
|
# e.g. if self.ITEM_KEY = 'part' -> ['part', 'part[]', 'parts', parts[]']
|
|
for k in [self.ITEM_KEY + x for x in ['', '[]', 's', 's[]']]:
|
|
if ids := self.request.query_params.getlist(k, []):
|
|
# Return the first list of matches
|
|
break
|
|
|
|
# Next we must validate each provided object ID
|
|
valid_ids = []
|
|
|
|
for id in ids:
|
|
try:
|
|
valid_ids.append(int(id))
|
|
except ValueError:
|
|
pass
|
|
|
|
# Filter queryset by matching ID values
|
|
return self.ITEM_MODEL.objects.filter(pk__in=valid_ids)
|
|
|
|
|
|
class LabelListView(LabelFilterMixin, ListCreateAPI):
|
|
"""Generic API class for label templates."""
|
|
|
|
def filter_queryset(self, queryset):
|
|
"""Filter the queryset based on the provided label ID values.
|
|
|
|
As each 'label' instance may optionally define its own filters,
|
|
the resulting queryset is the 'union' of the two.
|
|
"""
|
|
queryset = super().filter_queryset(queryset)
|
|
|
|
items = self.get_items()
|
|
|
|
if len(items) > 0:
|
|
"""
|
|
At this point, we are basically forced to be inefficient,
|
|
as we need to compare the 'filters' string of each label,
|
|
and see if it matches against each of the requested items.
|
|
|
|
TODO: In the future, if this becomes excessively slow, it
|
|
will need to be readdressed.
|
|
"""
|
|
valid_label_ids = set()
|
|
|
|
for lbl in queryset.all():
|
|
matches = True
|
|
|
|
try:
|
|
filters = InvenTree.helpers.validateFilterString(lbl.filters)
|
|
except ValidationError:
|
|
continue
|
|
|
|
for item in items:
|
|
item_query = self.ITEM_MODEL.objects.filter(pk=item.pk)
|
|
|
|
try:
|
|
if not item_query.filter(**filters).exists():
|
|
matches = False
|
|
break
|
|
except FieldError:
|
|
matches = False
|
|
break
|
|
|
|
# Matched all items
|
|
if matches:
|
|
valid_label_ids.add(lbl.pk)
|
|
else:
|
|
continue
|
|
|
|
# Reduce queryset to only valid matches
|
|
queryset = queryset.filter(pk__in=list(valid_label_ids))
|
|
|
|
return queryset
|
|
|
|
filter_backends = [DjangoFilterBackend, InvenTreeSearchFilter]
|
|
|
|
filterset_fields = ['enabled']
|
|
|
|
search_fields = ['name', 'description']
|
|
|
|
|
|
@method_decorator(cache_page(5), name='dispatch')
|
|
class LabelPrintMixin(LabelFilterMixin):
|
|
"""Mixin for printing labels."""
|
|
|
|
rolemap = {'GET': 'view', 'POST': 'view'}
|
|
|
|
def check_permissions(self, request):
|
|
"""Override request method to GET so that also non superusers can print using a post request."""
|
|
if request.method == 'POST':
|
|
request = clone_request(request, 'GET')
|
|
return super().check_permissions(request)
|
|
|
|
@method_decorator(never_cache)
|
|
def dispatch(self, *args, **kwargs):
|
|
"""Prevent caching when printing report templates."""
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
def get_serializer(self, *args, **kwargs):
|
|
"""Define a get_serializer method to be discoverable by the OPTIONS request."""
|
|
# Check the request to determine if the user has selected a label printing plugin
|
|
plugin = self.get_plugin(self.request)
|
|
|
|
kwargs.setdefault('context', self.get_serializer_context())
|
|
serializer = plugin.get_printing_options_serializer(
|
|
self.request, *args, **kwargs
|
|
)
|
|
|
|
# if no serializer is defined, return an empty serializer
|
|
if not serializer:
|
|
return serializers.Serializer()
|
|
|
|
return serializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Perform a GET request against this endpoint to print labels."""
|
|
common.models.InvenTreeUserSetting.set_setting(
|
|
'DEFAULT_' + self.ITEM_KEY.upper() + '_LABEL_TEMPLATE',
|
|
self.get_object().pk,
|
|
None,
|
|
user=request.user,
|
|
)
|
|
return self.print(request, self.get_items())
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
"""Perform a GET request against this endpoint to print labels."""
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def get_plugin(self, request):
|
|
"""Return the label printing plugin associated with this request.
|
|
|
|
This is provided in the url, e.g. ?plugin=myprinter
|
|
|
|
Requires:
|
|
- settings.PLUGINS_ENABLED is True
|
|
- matching plugin can be found
|
|
- matching plugin implements the 'labels' mixin
|
|
- matching plugin is enabled
|
|
"""
|
|
plugin_key = request.query_params.get('plugin', None)
|
|
|
|
# No plugin provided!
|
|
if plugin_key is None:
|
|
# Default to the builtin label printing plugin
|
|
plugin_key = InvenTreeLabelPlugin.NAME.lower()
|
|
|
|
plugin = registry.get_plugin(plugin_key)
|
|
|
|
if not plugin:
|
|
raise NotFound(f"Plugin '{plugin_key}' not found")
|
|
|
|
if not plugin.is_active():
|
|
raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
|
|
|
|
if not plugin.mixin_enabled('labels'):
|
|
raise ValidationError(
|
|
f"Plugin '{plugin_key}' is not a label printing plugin"
|
|
)
|
|
|
|
# Only return the plugin if it is enabled and has the label printing mixin
|
|
return plugin
|
|
|
|
def print(self, request, items_to_print):
|
|
"""Print this label template against a number of pre-validated items."""
|
|
# Check the request to determine if the user has selected a label printing plugin
|
|
plugin = self.get_plugin(request)
|
|
|
|
if len(items_to_print) == 0:
|
|
# No valid items provided, return an error message
|
|
raise ValidationError('No valid objects provided to label template')
|
|
|
|
# Label template
|
|
label = self.get_object()
|
|
|
|
# Check the label dimensions
|
|
if label.width <= 0 or label.height <= 0:
|
|
raise ValidationError('Label has invalid dimensions')
|
|
|
|
# if the plugin returns a serializer, validate the data
|
|
if serializer := plugin.get_printing_options_serializer(
|
|
request, data=request.data, context=self.get_serializer_context()
|
|
):
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
# At this point, we offload the label(s) to the selected plugin.
|
|
# The plugin is responsible for handling the request and returning a response.
|
|
|
|
try:
|
|
result = plugin.print_labels(
|
|
label,
|
|
items_to_print,
|
|
request,
|
|
printing_options=(serializer.data if serializer else {}),
|
|
)
|
|
except ValidationError as e:
|
|
raise (e)
|
|
except Exception as e:
|
|
raise ValidationError([_('Error printing label'), str(e)])
|
|
|
|
if isinstance(result, JsonResponse):
|
|
result['plugin'] = plugin.plugin_slug()
|
|
return result
|
|
raise ValidationError(
|
|
f"Plugin '{plugin.plugin_slug()}' returned invalid response type '{type(result)}'"
|
|
)
|
|
|
|
|
|
class StockItemLabelMixin:
|
|
"""Mixin for StockItemLabel endpoints."""
|
|
|
|
queryset = label.models.StockItemLabel.objects.all()
|
|
serializer_class = label.serializers.StockItemLabelSerializer
|
|
|
|
ITEM_MODEL = StockItem
|
|
ITEM_KEY = 'item'
|
|
|
|
|
|
class StockItemLabelList(StockItemLabelMixin, LabelListView):
|
|
"""API endpoint for viewing list of StockItemLabel objects.
|
|
|
|
Filterable by:
|
|
|
|
- enabled: Filter by enabled / disabled status
|
|
- item: Filter by single stock item
|
|
- items: Filter by list of stock items
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class StockItemLabelDetail(StockItemLabelMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single StockItemLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class StockItemLabelPrint(StockItemLabelMixin, LabelPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a StockItemLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class StockLocationLabelMixin:
|
|
"""Mixin for StockLocationLabel endpoints."""
|
|
|
|
queryset = label.models.StockLocationLabel.objects.all()
|
|
serializer_class = label.serializers.StockLocationLabelSerializer
|
|
|
|
ITEM_MODEL = StockLocation
|
|
ITEM_KEY = 'location'
|
|
|
|
|
|
class StockLocationLabelList(StockLocationLabelMixin, LabelListView):
|
|
"""API endpoint for viewiing list of StockLocationLabel objects.
|
|
|
|
Filterable by:
|
|
|
|
- enabled: Filter by enabled / disabled status
|
|
- location: Filter by a single stock location
|
|
- locations: Filter by list of stock locations
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class StockLocationLabelDetail(StockLocationLabelMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single StockLocationLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class StockLocationLabelPrint(StockLocationLabelMixin, LabelPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a StockLocationLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class PartLabelMixin:
|
|
"""Mixin for PartLabel endpoints."""
|
|
|
|
queryset = label.models.PartLabel.objects.all()
|
|
serializer_class = label.serializers.PartLabelSerializer
|
|
|
|
ITEM_MODEL = Part
|
|
ITEM_KEY = 'part'
|
|
|
|
|
|
class PartLabelList(PartLabelMixin, LabelListView):
|
|
"""API endpoint for viewing list of PartLabel objects."""
|
|
|
|
pass
|
|
|
|
|
|
class PartLabelDetail(PartLabelMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single PartLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class PartLabelPrint(PartLabelMixin, LabelPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a PartLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class BuildLineLabelMixin:
|
|
"""Mixin class for BuildLineLabel endpoints."""
|
|
|
|
queryset = label.models.BuildLineLabel.objects.all()
|
|
serializer_class = label.serializers.BuildLineLabelSerializer
|
|
|
|
ITEM_MODEL = build.models.BuildLine
|
|
ITEM_KEY = 'line'
|
|
|
|
|
|
class BuildLineLabelList(BuildLineLabelMixin, LabelListView):
|
|
"""API endpoint for viewing a list of BuildLineLabel objects."""
|
|
|
|
pass
|
|
|
|
|
|
class BuildLineLabelDetail(BuildLineLabelMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single BuildLineLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
class BuildLineLabelPrint(BuildLineLabelMixin, LabelPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a BuildLineLabel object."""
|
|
|
|
pass
|
|
|
|
|
|
label_api_urls = [
|
|
# Stock item labels
|
|
path(
|
|
'stock/',
|
|
include([
|
|
# Detail views
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
re_path(
|
|
r'print/?',
|
|
StockItemLabelPrint.as_view(),
|
|
name='api-stockitem-label-print',
|
|
),
|
|
path(
|
|
'metadata/',
|
|
MetadataView.as_view(),
|
|
{'model': label.models.StockItemLabel},
|
|
name='api-stockitem-label-metadata',
|
|
),
|
|
path(
|
|
'',
|
|
StockItemLabelDetail.as_view(),
|
|
name='api-stockitem-label-detail',
|
|
),
|
|
]),
|
|
),
|
|
# List view
|
|
path('', StockItemLabelList.as_view(), name='api-stockitem-label-list'),
|
|
]),
|
|
),
|
|
# Stock location labels
|
|
path(
|
|
'location/',
|
|
include([
|
|
# Detail views
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
re_path(
|
|
r'print/?',
|
|
StockLocationLabelPrint.as_view(),
|
|
name='api-stocklocation-label-print',
|
|
),
|
|
path(
|
|
'metadata/',
|
|
MetadataView.as_view(),
|
|
{'model': label.models.StockLocationLabel},
|
|
name='api-stocklocation-label-metadata',
|
|
),
|
|
path(
|
|
'',
|
|
StockLocationLabelDetail.as_view(),
|
|
name='api-stocklocation-label-detail',
|
|
),
|
|
]),
|
|
),
|
|
# List view
|
|
path(
|
|
'',
|
|
StockLocationLabelList.as_view(),
|
|
name='api-stocklocation-label-list',
|
|
),
|
|
]),
|
|
),
|
|
# Part labels
|
|
path(
|
|
'part/',
|
|
include([
|
|
# Detail views
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
re_path(
|
|
r'print/?',
|
|
PartLabelPrint.as_view(),
|
|
name='api-part-label-print',
|
|
),
|
|
path(
|
|
'metadata/',
|
|
MetadataView.as_view(),
|
|
{'model': label.models.PartLabel},
|
|
name='api-part-label-metadata',
|
|
),
|
|
path('', PartLabelDetail.as_view(), name='api-part-label-detail'),
|
|
]),
|
|
),
|
|
# List view
|
|
path('', PartLabelList.as_view(), name='api-part-label-list'),
|
|
]),
|
|
),
|
|
# BuildLine labels
|
|
path(
|
|
'buildline/',
|
|
include([
|
|
# Detail views
|
|
path(
|
|
'<int:pk>/',
|
|
include([
|
|
re_path(
|
|
r'print/?',
|
|
BuildLineLabelPrint.as_view(),
|
|
name='api-buildline-label-print',
|
|
),
|
|
path(
|
|
'metadata/',
|
|
MetadataView.as_view(),
|
|
{'model': label.models.BuildLineLabel},
|
|
name='api-buildline-label-metadata',
|
|
),
|
|
path(
|
|
'',
|
|
BuildLineLabelDetail.as_view(),
|
|
name='api-buildline-label-detail',
|
|
),
|
|
]),
|
|
),
|
|
# List view
|
|
path('', BuildLineLabelList.as_view(), name='api-buildline-label-list'),
|
|
]),
|
|
),
|
|
]
|