InvenTree/InvenTree/label/api.py
Matthias Mair cf70e4220f
Update requirements (#3228)
* Remove stat context variables

* Revert "Remove stat context variables"

This reverts commit 0989c308d0.

* remove old backport

* move requirements

* move isort

* move migrations-tests

* add pip-tools as req and add hook

* Use hashes and also run when txt files are added

* Update to hashes in requirements

* update requirements install path

* remove allauth-2fa for a test

* install dev-requirements

* re-enable allauth

* remove hashes from pre normal setup

* Remove version bindings if they are the laest

* move depencies to upper bound

* fix django-money

* remove windows specific requirements

* also install dependencies

* update markdownify

* remove django-admin-shell

* remove unneeded packages

* add docs

* remove pins and fix pre-commit pipeline

* pin django-money

* really remove admin shell

* return default behaviour for money serializer

* pip fix

* update filter name

* set decimal for output

* add migrations

* pin coveralls

* update requirements

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
2022-06-28 22:12:00 +10:00

545 lines
17 KiB
Python

"""API functionality for the 'label' app"""
from django.conf import settings
from django.core.exceptions import FieldError, ValidationError
from django.http import HttpResponse, JsonResponse
from django.urls import include, re_path
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework.exceptions import NotFound
import common.models
import InvenTree.helpers
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateDestroyAPI
from InvenTree.tasks import offload_task
from part.models import Part
from plugin.base.label import label as plugin_label
from plugin.registry import registry
from stock.models import StockItem, StockLocation
from .models import PartLabel, StockItemLabel, StockLocationLabel
from .serializers import (PartLabelSerializer, StockItemLabelSerializer,
StockLocationLabelSerializer)
class LabelListView(ListAPI):
"""Generic API class for label templates."""
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter
]
filterset_fields = [
'enabled',
]
search_fields = [
'name',
'description',
]
class LabelPrintMixin:
"""Mixin for printing labels."""
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
"""
if not settings.PLUGINS_ENABLED:
return None # pragma: no cover
plugin_key = request.query_params.get('plugin', None)
# No plugin provided, and that's OK
if plugin_key is None:
return None
plugin = registry.get_plugin(plugin_key)
if plugin:
config = plugin.plugin_config()
if config and config.active:
# Only return the plugin if it is enabled!
return plugin
else:
raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
else:
raise NotFound(f"Plugin '{plugin_key}' not found")
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')
outputs = []
# In debug mode, generate single HTML output, rather than PDF
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
label_name = "label.pdf"
label_names = []
# Merge one or more PDF files into a single download
for item in items_to_print:
label = self.get_object()
label.object_to_print = item
label_name = label.generate_filename(request)
label_names.append(label_name)
if debug_mode:
outputs.append(label.render_as_string(request))
else:
outputs.append(label.render(request))
if not label_name.endswith(".pdf"):
label_name += ".pdf"
if plugin is not None:
"""Label printing is to be handled by a plugin, rather than being exported to PDF.
In this case, we do the following:
- Individually generate each label, exporting as an image file
- Pass all the images through to the label printing plugin
- Return a JSON response indicating that the printing has been offloaded
"""
# Label instance
label_instance = self.get_object()
for idx, output in enumerate(outputs):
"""For each output, we generate a temporary image file, which will then get sent to the printer."""
# Generate PDF data for the label
pdf = output.get_document().write_pdf()
# Offload a background task to print the provided label
offload_task(
plugin_label.print_label,
plugin.plugin_slug(),
pdf,
filename=label_names[idx],
label_instance=label_instance,
user=request.user,
)
return JsonResponse({
'plugin': plugin.plugin_slug(),
'labels': label_names,
})
elif debug_mode:
"""Contatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
html = "\n".join(outputs)
return HttpResponse(html)
else:
"""Concatenate all rendered pages into a single PDF object, and return the resulting document!"""
pages = []
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)
return InvenTree.helpers.DownloadFile(
pdf,
label_name,
content_type='application/pdf',
inline=inline
)
class StockItemLabelMixin:
"""Mixin for extracting stock items from query params."""
def get_items(self):
"""Return a list of requested stock items."""
items = []
params = self.request.query_params
for key in ['item', 'item[]', 'items', 'items[]']:
if key in params:
items = params.getlist(key, [])
break
valid_ids = []
for item in items:
try:
valid_ids.append(int(item))
except (ValueError):
pass
# List of StockItems which match provided values
valid_items = StockItem.objects.filter(pk__in=valid_ids)
return valid_items
class StockItemLabelList(LabelListView, StockItemLabelMixin):
"""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
"""
queryset = StockItemLabel.objects.all()
serializer_class = StockItemLabelSerializer
def filter_queryset(self, queryset):
"""Filter the StockItem label queryset."""
queryset = super().filter_queryset(queryset)
# List of StockItem objects to match against
items = self.get_items()
# We wish to filter by stock 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.
"""
# Keep track of which labels match every specified stockitem
valid_label_ids = set()
for label in queryset.all():
matches = True
# Filter string defined for the StockItemLabel object
try:
filters = InvenTree.helpers.validateFilterString(label.filters)
except ValidationError: # pragma: no cover
continue
for item in items:
item_query = StockItem.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(label.pk)
else:
continue # pragma: no cover
# Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
return queryset
class StockItemLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockItemLabel object."""
queryset = StockItemLabel.objects.all()
serializer_class = StockItemLabelSerializer
class StockItemLabelPrint(RetrieveAPI, StockItemLabelMixin, LabelPrintMixin):
"""API endpoint for printing a StockItemLabel object."""
queryset = StockItemLabel.objects.all()
serializer_class = StockItemLabelSerializer
def get(self, request, *args, **kwargs):
"""Check if valid stock item(s) have been provided."""
items = self.get_items()
return self.print(request, items)
class StockLocationLabelMixin:
"""Mixin for extracting stock locations from query params."""
def get_locations(self):
"""Return a list of requested stock locations."""
locations = []
params = self.request.query_params
for key in ['location', 'location[]', 'locations', 'locations[]']:
if key in params:
locations = params.getlist(key, [])
valid_ids = []
for loc in locations:
try:
valid_ids.append(int(loc))
except (ValueError):
pass
# List of StockLocation objects which match provided values
valid_locations = StockLocation.objects.filter(pk__in=valid_ids)
return valid_locations
class StockLocationLabelList(LabelListView, StockLocationLabelMixin):
"""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
"""
queryset = StockLocationLabel.objects.all()
serializer_class = StockLocationLabelSerializer
def filter_queryset(self, queryset):
"""Filter the StockLocationLabel queryset."""
queryset = super().filter_queryset(queryset)
# List of StockLocation objects to match against
locations = self.get_locations()
# We wish to filter by stock location(s)
if len(locations) > 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 label in queryset.all():
matches = True
# Filter string defined for the StockLocationLabel object
try:
filters = InvenTree.helpers.validateFilterString(label.filters)
except Exception: # pragma: no cover
# Skip if there was an error validating the filters...
continue
for loc in locations:
loc_query = StockLocation.objects.filter(pk=loc.pk)
try:
if not loc_query.filter(**filters).exists():
matches = False
break
except FieldError:
matches = False
break
# Matched all items
if matches:
valid_label_ids.add(label.pk)
else:
continue # pragma: no cover
# Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
return queryset
class StockLocationLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single StockLocationLabel object."""
queryset = StockLocationLabel.objects.all()
serializer_class = StockLocationLabelSerializer
class StockLocationLabelPrint(RetrieveAPI, StockLocationLabelMixin, LabelPrintMixin):
"""API endpoint for printing a StockLocationLabel object."""
queryset = StockLocationLabel.objects.all()
seiralizer_class = StockLocationLabelSerializer
def get(self, request, *args, **kwargs):
"""Print labels based on the request parameters"""
locations = self.get_locations()
return self.print(request, locations)
class PartLabelMixin:
"""Mixin for extracting Part objects from query parameters."""
def get_parts(self):
"""Return a list of requested Part objects."""
parts = []
params = self.request.query_params
for key in ['part', 'part[]', 'parts', 'parts[]']:
if key in params:
parts = params.getlist(key, [])
break
valid_ids = []
for part in parts:
try:
valid_ids.append(int(part))
except (ValueError):
pass
# List of Part objects which match provided values
return Part.objects.filter(pk__in=valid_ids)
class PartLabelList(LabelListView, PartLabelMixin):
"""API endpoint for viewing list of PartLabel objects."""
queryset = PartLabel.objects.all()
serializer_class = PartLabelSerializer
def filter_queryset(self, queryset):
"""Custom queryset filtering for the PartLabel list"""
queryset = super().filter_queryset(queryset)
parts = self.get_parts()
if len(parts) > 0:
valid_label_ids = set()
for label in queryset.all():
matches = True
try:
filters = InvenTree.helpers.validateFilterString(label.filters)
except ValidationError: # pragma: no cover
continue
for part in parts:
part_query = Part.objects.filter(pk=part.pk)
try:
if not part_query.filter(**filters).exists():
matches = False
break
except FieldError:
matches = False
break
if matches:
valid_label_ids.add(label.pk)
# Reduce queryset to only valid matches
queryset = queryset.filter(pk__in=[pk for pk in valid_label_ids])
return queryset
class PartLabelDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for a single PartLabel object."""
queryset = PartLabel.objects.all()
serializer_class = PartLabelSerializer
class PartLabelPrint(RetrieveAPI, PartLabelMixin, LabelPrintMixin):
"""API endpoint for printing a PartLabel object."""
queryset = PartLabel.objects.all()
serializer_class = PartLabelSerializer
def get(self, request, *args, **kwargs):
"""Check if valid part(s) have been provided."""
parts = self.get_parts()
return self.print(request, parts)
label_api_urls = [
# Stock item labels
re_path(r'stock/', include([
# Detail views
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'print/?', StockItemLabelPrint.as_view(), name='api-stockitem-label-print'),
re_path(r'^.*$', StockItemLabelDetail.as_view(), name='api-stockitem-label-detail'),
])),
# List view
re_path(r'^.*$', StockItemLabelList.as_view(), name='api-stockitem-label-list'),
])),
# Stock location labels
re_path(r'location/', include([
# Detail views
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'print/?', StockLocationLabelPrint.as_view(), name='api-stocklocation-label-print'),
re_path(r'^.*$', StockLocationLabelDetail.as_view(), name='api-stocklocation-label-detail'),
])),
# List view
re_path(r'^.*$', StockLocationLabelList.as_view(), name='api-stocklocation-label-list'),
])),
# Part labels
re_path(r'^part/', include([
# Detail views
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^print/', PartLabelPrint.as_view(), name='api-part-label-print'),
re_path(r'^.*$', PartLabelDetail.as_view(), name='api-part-label-detail'),
])),
# List view
re_path(r'^.*$', PartLabelList.as_view(), name='api-part-label-list'),
])),
]