Coverage for src/backend/InvenTree/report/api.py: 62%
172 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""API functionality for the 'report' app."""
3from django.core.exceptions import ValidationError
4from django.urls import include, path
5from django.utils.decorators import method_decorator
6from django.utils.translation import gettext_lazy as _
7from django.views.decorators.cache import never_cache
9import django_filters.rest_framework.filters as rest_filters
10from django_filters.rest_framework.filterset import FilterSet
11from rest_framework.exceptions import PermissionDenied
12from rest_framework.generics import GenericAPIView
13from rest_framework.response import Response
15import InvenTree.permissions
16import report.helpers
17import report.models
18import report.serializers
19import users.permissions
20from common.models import DataOutput
21from common.serializers import DataOutputSerializer
22from InvenTree.api import meta_path
23from InvenTree.filters import SEARCH_ORDER_FILTER
24from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
25from plugin import PluginMixinEnum
26from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
29class TemplatePermissionMixin:
30 """Permission mixin for report and label templates."""
32 # Read only for non-staff users
33 permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
36class ReportFilterBase(FilterSet):
37 """Base filter class for label and report templates."""
39 enabled = rest_filters.BooleanFilter()
41 model_type = rest_filters.ChoiceFilter(
42 choices=report.helpers.report_model_options(), label=_('Model Type')
43 )
45 items = rest_filters.CharFilter(method='filter_items', label=_('Items'))
47 def filter_items(self, queryset, name, values):
48 """Filter against a comma-separated list of provided items.
50 Note: This filter is only applied if the 'model_type' is also provided.
51 """
52 model_type = self.data.get('model_type', None)
53 values = values.strip().split(',')
55 if model_class := report.helpers.report_model_from_name(model_type): 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 model_items = model_class.objects.filter(pk__in=values)
58 # Ensure that we have already filtered by model_type
59 queryset = queryset.filter(model_type=model_type)
61 # Construct a list of templates which match the list of provided IDs
62 matching_template_ids = []
64 for template in queryset.all():
65 filters = template.get_filters()
66 results = model_items.filter(**filters)
67 # If the resulting queryset is *shorter* than the provided items, then this template does not match
68 if results.count() == model_items.count():
69 matching_template_ids.append(template.pk)
71 queryset = queryset.filter(pk__in=matching_template_ids)
73 return queryset
76class ReportFilter(ReportFilterBase):
77 """Filter class for report template list."""
79 class Meta:
80 """Filter options."""
82 model = report.models.ReportTemplate
83 fields = ['landscape', 'merge', 'attach_to_model', 'enabled', 'model_type']
86class LabelFilter(ReportFilterBase):
87 """Filter class for label template list."""
89 class Meta:
90 """Filter options."""
92 model = report.models.LabelTemplate
93 fields = ['enabled']
96class LabelPrint(GenericAPIView):
97 """API endpoint for printing labels."""
99 # Any authenticated user can print labels
100 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
101 serializer_class = report.serializers.LabelPrintSerializer
103 def get_plugin_class(self, plugin_slug: str, raise_error=False):
104 """Return the plugin class for the given plugin key."""
105 from plugin import registry
107 if not plugin_slug:
108 # Use the default label printing plugin
109 plugin_slug = InvenTreeLabelPlugin.NAME.lower()
111 plugin = registry.get_plugin(plugin_slug, active=True)
113 error = None
115 if not plugin:
116 error = _('Plugin not found')
117 elif not plugin.mixin_enabled(PluginMixinEnum.LABELS): 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 error = _('Plugin does not support label printing')
120 if error:
121 plugin = None
123 if raise_error: 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true
124 raise ValidationError({'plugin': error})
126 return plugin
128 def get_plugin_serializer(self, plugin):
129 """Return the serializer for the given plugin."""
130 if plugin and hasattr(plugin, 'get_printing_options_serializer'):
131 return plugin.get_printing_options_serializer(
132 self.request,
133 data=self.request.data,
134 context=self.get_serializer_context(),
135 )
137 return None
139 def get_serializer(self, *args, **kwargs):
140 """Return serializer information for the label print endpoint."""
141 plugin = None
143 # Plugin information provided?
144 if self.request: 144 ↛ 155line 144 didn't jump to line 155 because the condition on line 144 was always true
145 plugin_key = self.request.data.get('plugin', '')
146 # Legacy url based lookup
147 if not plugin_key:
148 plugin_key = self.request.query_params.get('plugin', '')
149 plugin = self.get_plugin_class(plugin_key)
150 plugin_serializer = self.get_plugin_serializer(plugin)
152 if plugin_serializer: 152 ↛ 153line 152 didn't jump to line 153 because the condition on line 152 was never true
153 kwargs['plugin_serializer'] = plugin_serializer
155 return super().get_serializer(*args, **kwargs)
157 @method_decorator(never_cache)
158 def post(self, request, *args, **kwargs):
159 """POST action for printing labels."""
160 serializer = self.get_serializer(data=request.data)
161 serializer.is_valid(raise_exception=True)
163 template = serializer.validated_data['template']
165 model_class = template.get_model()
166 if model_class and not users.permissions.check_user_permission(
167 request.user, model_class, 'view'
168 ):
169 raise PermissionDenied(
170 _('You do not have permission to view this model type')
171 )
173 if template.width <= 0 or template.height <= 0:
174 raise ValidationError({'template': _('Invalid label dimensions')})
176 items = serializer.validated_data['items']
178 # Default to the InvenTreeLabelPlugin
179 plugin_key = InvenTreeLabelPlugin.NAME.lower()
181 if plugin_config := serializer.validated_data.get('plugin', None):
182 plugin_key = plugin_config.key
184 plugin = self.get_plugin_class(plugin_key, raise_error=True)
186 instances = model_class.objects.filter(pk__in=items)
188 # Sort the instances by the order of the provided items
189 instances = sorted(instances, key=lambda item: items.index(item.pk))
191 if len(instances) == 0:
192 raise ValidationError(_('No valid items provided to template'))
194 return self.print(template, instances, plugin, request)
196 def print(self, template, items_to_print, plugin, request):
197 """Print this label template against a number of provided items."""
198 import report.tasks
199 from InvenTree.tasks import offload_task
201 if plugin_serializer := plugin.get_printing_options_serializer(
202 request, data=request.data, context=self.get_serializer_context()
203 ):
204 plugin_serializer.is_valid(raise_exception=True)
206 user = getattr(request, 'user', None)
208 # Generate a new DataOutput object to print against
209 output = DataOutput.objects.create(
210 user=user if user and user.is_authenticated else None,
211 total=len(items_to_print),
212 progress=0,
213 complete=False,
214 output_type=DataOutput.DataOutputTypes.LABEL,
215 plugin=plugin.slug,
216 template_name=template.name,
217 output=None,
218 )
220 output.refresh_from_db()
222 offload_task(
223 report.tasks.print_labels,
224 template.pk,
225 [item.pk for item in items_to_print],
226 output.pk,
227 user.pk if user else None,
228 plugin.slug,
229 options=(plugin_serializer.data if plugin_serializer else {}),
230 )
232 output.refresh_from_db()
234 return Response(DataOutputSerializer(output).data, status=201)
237class LabelTemplateMixin:
238 """Mixin class for label template API views."""
240 queryset = report.models.LabelTemplate.objects.all().prefetch_related('updated_by')
241 serializer_class = report.serializers.LabelTemplateSerializer
244class LabelTemplateList(TemplatePermissionMixin, LabelTemplateMixin, ListCreateAPI):
245 """API endpoint for viewing list of LabelTemplate objects."""
247 filterset_class = LabelFilter
248 filter_backends = SEARCH_ORDER_FILTER
249 search_fields = ['name', 'description']
250 ordering_fields = ['name', 'enabled', 'width', 'height']
253class LabelTemplateDetail(
254 TemplatePermissionMixin, LabelTemplateMixin, RetrieveUpdateDestroyAPI
255):
256 """Detail API endpoint for label template model."""
259class ReportPrint(GenericAPIView):
260 """API endpoint for printing reports."""
262 # Any authenticated user can print reports
263 permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
264 serializer_class = report.serializers.ReportPrintSerializer
266 @method_decorator(never_cache)
267 def post(self, request, *args, **kwargs):
268 """POST action for printing a report."""
269 serializer = self.get_serializer(data=request.data)
270 serializer.is_valid(raise_exception=True)
272 template = serializer.validated_data['template']
274 model_class = template.get_model()
275 if model_class and not users.permissions.check_user_permission(
276 request.user, model_class, 'view'
277 ):
278 raise PermissionDenied(
279 _('You do not have permission to view this model type')
280 )
282 items = serializer.validated_data['items']
284 instances = model_class.objects.filter(pk__in=items)
286 # Sort the instances by the order of the provided items
287 instances = sorted(instances, key=lambda item: items.index(item.pk))
289 if len(instances) == 0:
290 raise ValidationError(_('No valid items provided to template'))
292 return self.print(template, instances, request)
294 def print(self, template, items_to_print, request):
295 """Print this report template against a number of provided items.
297 This functionality is offloaded to the background worker process,
298 which will update the status of the DataOutput object as it progresses.
299 """
300 import report.tasks
301 from InvenTree.tasks import offload_task
303 user = getattr(request, 'user', None)
305 # Generate a new DataOutput object
306 output = DataOutput.objects.create(
307 user=user if user and user.is_authenticated else None,
308 total=len(items_to_print),
309 progress=0,
310 complete=False,
311 output_type=DataOutput.DataOutputTypes.REPORT,
312 template_name=template.name,
313 output=None,
314 )
316 item_ids = [item.pk for item in items_to_print]
318 # Offload the task to the background worker
319 offload_task(
320 report.tasks.print_reports,
321 template.pk,
322 item_ids,
323 output.pk,
324 user.pk if user else None,
325 )
327 output.refresh_from_db()
329 return Response(DataOutputSerializer(output).data, status=201)
332class ReportTemplateMixin:
333 """Mixin class for report template API views."""
335 queryset = report.models.ReportTemplate.objects.all().prefetch_related('updated_by')
336 serializer_class = report.serializers.ReportTemplateSerializer
339class ReportTemplateList(TemplatePermissionMixin, ReportTemplateMixin, ListCreateAPI):
340 """API endpoint for viewing list of ReportTemplate objects."""
342 filterset_class = ReportFilter
343 filter_backends = SEARCH_ORDER_FILTER
344 search_fields = ['name', 'description']
345 ordering_fields = ['name', 'enabled']
348class ReportTemplateDetail(
349 TemplatePermissionMixin, ReportTemplateMixin, RetrieveUpdateDestroyAPI
350):
351 """Detail API endpoint for report template model."""
354class ReportSnippetList(TemplatePermissionMixin, ListCreateAPI):
355 """API endpoint for listing ReportSnippet objects."""
357 queryset = report.models.ReportSnippet.objects.all()
358 serializer_class = report.serializers.ReportSnippetSerializer
361class ReportSnippetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
362 """API endpoint for a single ReportSnippet object."""
364 queryset = report.models.ReportSnippet.objects.all()
365 serializer_class = report.serializers.ReportSnippetSerializer
368class ReportAssetList(TemplatePermissionMixin, ListCreateAPI):
369 """API endpoint for listing ReportAsset objects."""
371 queryset = report.models.ReportAsset.objects.all()
372 serializer_class = report.serializers.ReportAssetSerializer
375class ReportAssetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
376 """API endpoint for a single ReportAsset object."""
378 queryset = report.models.ReportAsset.objects.all()
379 serializer_class = report.serializers.ReportAssetSerializer
382label_api_urls = [
383 # Printing endpoint
384 path('print/', LabelPrint.as_view(), name='api-label-print'),
385 # Label templates
386 path(
387 'template/',
388 include([
389 path(
390 '<int:pk>/',
391 include([
392 meta_path(report.models.LabelTemplate),
393 path(
394 '',
395 LabelTemplateDetail.as_view(),
396 name='api-label-template-detail',
397 ),
398 ]),
399 ),
400 path('', LabelTemplateList.as_view(), name='api-label-template-list'),
401 ]),
402 ),
403]
405report_api_urls = [
406 # Printing endpoint
407 path('print/', ReportPrint.as_view(), name='api-report-print'),
408 # Report templates
409 path(
410 'template/',
411 include([
412 path(
413 '<int:pk>/',
414 include([
415 meta_path(report.models.ReportTemplate),
416 path(
417 '',
418 ReportTemplateDetail.as_view(),
419 name='api-report-template-detail',
420 ),
421 ]),
422 ),
423 path('', ReportTemplateList.as_view(), name='api-report-template-list'),
424 ]),
425 ),
426 # Report assets
427 path(
428 'asset/',
429 include([
430 path(
431 '<int:pk>/', ReportAssetDetail.as_view(), name='api-report-asset-detail'
432 ),
433 path('', ReportAssetList.as_view(), name='api-report-asset-list'),
434 ]),
435 ),
436 # Report snippets
437 path(
438 'snippet/',
439 include([
440 path(
441 '<int:pk>/',
442 ReportSnippetDetail.as_view(),
443 name='api-report-snippet-detail',
444 ),
445 path('', ReportSnippetList.as_view(), name='api-report-snippet-list'),
446 ]),
447 ),
448]