mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
579 lines
19 KiB
Python
579 lines
19 KiB
Python
"""API functionality for the 'report' app"""
|
|
|
|
from django.core.exceptions import FieldError, ValidationError
|
|
from django.core.files.base import ContentFile
|
|
from django.http import HttpResponse
|
|
from django.template.exceptions import TemplateDoesNotExist
|
|
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.response import Response
|
|
|
|
import build.models
|
|
import common.models
|
|
import InvenTree.helpers
|
|
import order.models
|
|
import part.models
|
|
from InvenTree.api import MetadataView
|
|
from InvenTree.exceptions import log_error
|
|
from InvenTree.filters import InvenTreeSearchFilter
|
|
from InvenTree.mixins import (ListCreateAPI, RetrieveAPI,
|
|
RetrieveUpdateDestroyAPI)
|
|
from stock.models import StockItem, StockItemAttachment, StockLocation
|
|
|
|
from .models import (BillOfMaterialsReport, BuildReport, PurchaseOrderReport,
|
|
ReturnOrderReport, SalesOrderReport, StockLocationReport,
|
|
TestReport)
|
|
from .serializers import (BOMReportSerializer, BuildReportSerializer,
|
|
PurchaseOrderReportSerializer,
|
|
ReturnOrderReportSerializer,
|
|
SalesOrderReportSerializer,
|
|
StockLocationReportSerializer, TestReportSerializer)
|
|
|
|
|
|
class ReportListView(ListCreateAPI):
|
|
"""Generic API class for report templates."""
|
|
|
|
filter_backends = [
|
|
DjangoFilterBackend,
|
|
InvenTreeSearchFilter,
|
|
]
|
|
|
|
filterset_fields = [
|
|
'enabled',
|
|
]
|
|
|
|
search_fields = [
|
|
'name',
|
|
'description',
|
|
]
|
|
|
|
|
|
class ReportFilterMixin:
|
|
"""Mixin for extracting multiple objects from query params.
|
|
|
|
Each subclass *must* have an attribute called 'ITEM_KEY',
|
|
which is used to determine what 'key' is used in the query parameters.
|
|
|
|
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 report 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 parameters"""
|
|
if not self.ITEM_MODEL:
|
|
raise NotImplementedError(f"ITEM_MODEL attribute not defined for {__class__}")
|
|
|
|
ids = []
|
|
|
|
# Construct a list of possible query parameter value options
|
|
# e.g. if self.ITEM_KEY = 'order' -> ['order', 'order[]', 'orders', 'orders[]']
|
|
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 validated 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)
|
|
|
|
def filter_queryset(self, queryset):
|
|
"""Filter the queryset based on the provided report ID values.
|
|
|
|
As each 'report' 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:
|
|
|
|
We need to compare the 'filters' string of each report template,
|
|
and see if it matches against each of the requested items.
|
|
|
|
In practice, this is not too bad.
|
|
"""
|
|
|
|
valid_report_ids = set()
|
|
|
|
for report in queryset.all():
|
|
matches = True
|
|
|
|
try:
|
|
filters = InvenTree.helpers.validateFilterString(report.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_report_ids.add(report.pk)
|
|
|
|
# Reduce queryset to only valid matches
|
|
queryset = queryset.filter(pk__in=list(valid_report_ids))
|
|
|
|
return queryset
|
|
|
|
|
|
@method_decorator(cache_page(5), name='dispatch')
|
|
class ReportPrintMixin:
|
|
"""Mixin for printing reports."""
|
|
|
|
@method_decorator(never_cache)
|
|
def dispatch(self, *args, **kwargs):
|
|
"""Prevent caching when printing report templates"""
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
def report_callback(self, object, report, request):
|
|
"""Callback function for each object/report combination.
|
|
|
|
Allows functionality to be performed before returning the consolidated PDF
|
|
|
|
Arguments:
|
|
object: The model instance to be printed
|
|
report: The individual PDF file object
|
|
request: The request instance associated with this print call
|
|
"""
|
|
...
|
|
|
|
def print(self, request, items_to_print):
|
|
"""Print this report template against a number of pre-validated items."""
|
|
if len(items_to_print) == 0:
|
|
# No valid items provided, return an error message
|
|
data = {
|
|
'error': _('No valid objects provided to template'),
|
|
}
|
|
|
|
return Response(data, status=400)
|
|
|
|
outputs = []
|
|
|
|
# In debug mode, generate single HTML output, rather than PDF
|
|
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE', cache=False)
|
|
|
|
# Start with a default report name
|
|
report_name = "report.pdf"
|
|
|
|
try:
|
|
# Merge one or more PDF files into a single download
|
|
for item in items_to_print:
|
|
report = self.get_object()
|
|
report.object_to_print = item
|
|
|
|
report_name = report.generate_filename(request)
|
|
output = report.render(request)
|
|
|
|
# Run report callback for each generated report
|
|
self.report_callback(item, output, request)
|
|
|
|
try:
|
|
if debug_mode:
|
|
outputs.append(report.render_as_string(request))
|
|
else:
|
|
outputs.append(output)
|
|
except TemplateDoesNotExist as e:
|
|
template = str(e)
|
|
if not template:
|
|
template = report.template
|
|
|
|
return Response(
|
|
{
|
|
'error': _(f"Template file '{template}' is missing or does not exist"),
|
|
},
|
|
status=400,
|
|
)
|
|
|
|
if not report_name.endswith('.pdf'):
|
|
report_name += '.pdf'
|
|
|
|
if debug_mode:
|
|
"""Concatenate 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 = []
|
|
|
|
try:
|
|
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()
|
|
|
|
except TemplateDoesNotExist as e:
|
|
|
|
template = str(e)
|
|
|
|
if not template:
|
|
template = report.template
|
|
|
|
return Response(
|
|
{
|
|
'error': _(f"Template file '{template}' is missing or does not exist"),
|
|
},
|
|
status=400,
|
|
)
|
|
|
|
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user, cache=False)
|
|
|
|
return InvenTree.helpers.DownloadFile(
|
|
pdf,
|
|
report_name,
|
|
content_type='application/pdf',
|
|
inline=inline,
|
|
)
|
|
|
|
except Exception as exc:
|
|
# Log the exception to the database
|
|
log_error(request.path)
|
|
|
|
# Re-throw the exception to the client as a DRF exception
|
|
raise ValidationError({
|
|
'error': 'Report printing failed',
|
|
'detail': str(exc),
|
|
'path': request.path,
|
|
})
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Default implementation of GET for a print endpoint.
|
|
|
|
Note that it expects the class has defined a get_items() method
|
|
"""
|
|
items = self.get_items()
|
|
return self.print(request, items)
|
|
|
|
|
|
class StockItemTestReportMixin(ReportFilterMixin):
|
|
"""Mixin for StockItemTestReport report template"""
|
|
|
|
ITEM_MODEL = StockItem
|
|
ITEM_KEY = 'item'
|
|
queryset = TestReport.objects.all()
|
|
serializer_class = TestReportSerializer
|
|
|
|
|
|
class StockItemTestReportList(StockItemTestReportMixin, ReportListView):
|
|
"""API endpoint for viewing list of TestReport objects.
|
|
|
|
Filterable by:
|
|
|
|
- enabled: Filter by enabled / disabled status
|
|
- item: Filter by stock item(s)
|
|
"""
|
|
pass
|
|
|
|
|
|
class StockItemTestReportDetail(StockItemTestReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single TestReport object."""
|
|
pass
|
|
|
|
|
|
class StockItemTestReportPrint(StockItemTestReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a TestReport object."""
|
|
|
|
def report_callback(self, item, report, request):
|
|
"""Callback to (optionally) save a copy of the generated report"""
|
|
if common.models.InvenTreeSetting.get_setting('REPORT_ATTACH_TEST_REPORT', cache=False):
|
|
|
|
# Construct a PDF file object
|
|
try:
|
|
pdf = report.get_document().write_pdf()
|
|
pdf_content = ContentFile(pdf, "test_report.pdf")
|
|
except TemplateDoesNotExist:
|
|
return
|
|
|
|
StockItemAttachment.objects.create(
|
|
attachment=pdf_content,
|
|
stock_item=item,
|
|
user=request.user,
|
|
comment=_("Test report")
|
|
)
|
|
|
|
|
|
class BOMReportMixin(ReportFilterMixin):
|
|
"""Mixin for BillOfMaterialsReport report template"""
|
|
|
|
ITEM_MODEL = part.models.Part
|
|
ITEM_KEY = 'part'
|
|
|
|
queryset = BillOfMaterialsReport.objects.all()
|
|
serializer_class = BOMReportSerializer
|
|
|
|
|
|
class BOMReportList(BOMReportMixin, ReportListView):
|
|
"""API endpoint for viewing a list of BillOfMaterialReport objects.
|
|
|
|
Filterably by:
|
|
|
|
- enabled: Filter by enabled / disabled status
|
|
- part: Filter by part(s)
|
|
"""
|
|
pass
|
|
|
|
|
|
class BOMReportDetail(BOMReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single BillOfMaterialReport object."""
|
|
pass
|
|
|
|
|
|
class BOMReportPrint(BOMReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a BillOfMaterialReport object."""
|
|
pass
|
|
|
|
|
|
class BuildReportMixin(ReportFilterMixin):
|
|
"""Mixin for the BuildReport report template"""
|
|
|
|
ITEM_MODEL = build.models.Build
|
|
ITEM_KEY = 'build'
|
|
|
|
queryset = BuildReport.objects.all()
|
|
serializer_class = BuildReportSerializer
|
|
|
|
|
|
class BuildReportList(BuildReportMixin, ReportListView):
|
|
"""API endpoint for viewing a list of BuildReport objects.
|
|
|
|
Can be filtered by:
|
|
|
|
- enabled: Filter by enabled / disabled status
|
|
- build: Filter by Build object
|
|
"""
|
|
pass
|
|
|
|
|
|
class BuildReportDetail(BuildReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single BuildReport object."""
|
|
pass
|
|
|
|
|
|
class BuildReportPrint(BuildReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a BuildReport."""
|
|
pass
|
|
|
|
|
|
class PurchaseOrderReportMixin(ReportFilterMixin):
|
|
"""Mixin for the PurchaseOrderReport report template"""
|
|
|
|
ITEM_MODEL = order.models.PurchaseOrder
|
|
ITEM_KEY = 'order'
|
|
|
|
queryset = PurchaseOrderReport.objects.all()
|
|
serializer_class = PurchaseOrderReportSerializer
|
|
|
|
|
|
class PurchaseOrderReportList(PurchaseOrderReportMixin, ReportListView):
|
|
"""API list endpoint for the PurchaseOrderReport model"""
|
|
pass
|
|
|
|
|
|
class PurchaseOrderReportDetail(PurchaseOrderReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single PurchaseOrderReport object."""
|
|
pass
|
|
|
|
|
|
class PurchaseOrderReportPrint(PurchaseOrderReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a PurchaseOrderReport object."""
|
|
pass
|
|
|
|
|
|
class SalesOrderReportMixin(ReportFilterMixin):
|
|
"""Mixin for the SalesOrderReport report template"""
|
|
|
|
ITEM_MODEL = order.models.SalesOrder
|
|
ITEM_KEY = 'order'
|
|
|
|
queryset = SalesOrderReport.objects.all()
|
|
serializer_class = SalesOrderReportSerializer
|
|
|
|
|
|
class SalesOrderReportList(SalesOrderReportMixin, ReportListView):
|
|
"""API list endpoint for the SalesOrderReport model"""
|
|
pass
|
|
|
|
|
|
class SalesOrderReportDetail(SalesOrderReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single SalesOrderReport object."""
|
|
pass
|
|
|
|
|
|
class SalesOrderReportPrint(SalesOrderReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a PurchaseOrderReport object."""
|
|
pass
|
|
|
|
|
|
class ReturnOrderReportMixin(ReportFilterMixin):
|
|
"""Mixin for the ReturnOrderReport report template"""
|
|
|
|
ITEM_MODEL = order.models.ReturnOrder
|
|
ITEM_KEY = 'order'
|
|
|
|
queryset = ReturnOrderReport.objects.all()
|
|
serializer_class = ReturnOrderReportSerializer
|
|
|
|
|
|
class ReturnOrderReportList(ReturnOrderReportMixin, ReportListView):
|
|
"""API list endpoint for the ReturnOrderReport model"""
|
|
pass
|
|
|
|
|
|
class ReturnOrderReportDetail(ReturnOrderReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single ReturnOrderReport object"""
|
|
pass
|
|
|
|
|
|
class ReturnOrderReportPrint(ReturnOrderReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a ReturnOrderReport object"""
|
|
pass
|
|
|
|
|
|
class StockLocationReportMixin(ReportFilterMixin):
|
|
"""Mixin for StockLocation report template"""
|
|
|
|
ITEM_MODEL = StockLocation
|
|
ITEM_KEY = 'location'
|
|
queryset = StockLocationReport.objects.all()
|
|
serializer_class = StockLocationReportSerializer
|
|
|
|
|
|
class StockLocationReportList(StockLocationReportMixin, ReportListView):
|
|
"""API list endpoint for the StockLocationReportList model"""
|
|
pass
|
|
|
|
|
|
class StockLocationReportDetail(StockLocationReportMixin, RetrieveUpdateDestroyAPI):
|
|
"""API endpoint for a single StockLocationReportDetail object."""
|
|
pass
|
|
|
|
|
|
class StockLocationReportPrint(StockLocationReportMixin, ReportPrintMixin, RetrieveAPI):
|
|
"""API endpoint for printing a StockLocationReportPrint object"""
|
|
pass
|
|
|
|
|
|
report_api_urls = [
|
|
|
|
# Purchase order reports
|
|
re_path(r'po/', include([
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/', PurchaseOrderReportPrint.as_view(), name='api-po-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'model': PurchaseOrderReport}, name='api-po-report-metadata'),
|
|
path('', PurchaseOrderReportDetail.as_view(), name='api-po-report-detail'),
|
|
])),
|
|
|
|
# List view
|
|
path('', PurchaseOrderReportList.as_view(), name='api-po-report-list'),
|
|
])),
|
|
|
|
# Sales order reports
|
|
re_path(r'so/', include([
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/', SalesOrderReportPrint.as_view(), name='api-so-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'model': SalesOrderReport}, name='api-so-report-metadata'),
|
|
path('', SalesOrderReportDetail.as_view(), name='api-so-report-detail'),
|
|
])),
|
|
|
|
path('', SalesOrderReportList.as_view(), name='api-so-report-list'),
|
|
])),
|
|
|
|
# Return order reports
|
|
re_path(r'ro/', include([
|
|
path(r'<int:pk>/', include([
|
|
path(r'print/', ReturnOrderReportPrint.as_view(), name='api-return-order-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'model': ReturnOrderReport}, name='api-so-report-metadata'),
|
|
path('', ReturnOrderReportDetail.as_view(), name='api-return-order-report-detail'),
|
|
])),
|
|
path('', ReturnOrderReportList.as_view(), name='api-return-order-report-list'),
|
|
])),
|
|
|
|
# Build reports
|
|
re_path(r'build/', include([
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'model': BuildReport}, name='api-build-report-metadata'),
|
|
re_path(r'^.*$', BuildReportDetail.as_view(), name='api-build-report-detail'),
|
|
])),
|
|
|
|
# List view
|
|
re_path(r'^.*$', BuildReportList.as_view(), name='api-build-report-list'),
|
|
])),
|
|
|
|
# Bill of Material reports
|
|
re_path(r'bom/', include([
|
|
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/?', BOMReportPrint.as_view(), name='api-bom-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'model': BillOfMaterialsReport}, name='api-bom-report-metadata'),
|
|
re_path(r'^.*$', BOMReportDetail.as_view(), name='api-bom-report-detail'),
|
|
])),
|
|
|
|
# List view
|
|
re_path(r'^.*$', BOMReportList.as_view(), name='api-bom-report-list'),
|
|
])),
|
|
|
|
# Stock item test reports
|
|
re_path(r'test/', include([
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/?', StockItemTestReportPrint.as_view(), name='api-stockitem-testreport-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'report': TestReport}, name='api-stockitem-testreport-metadata'),
|
|
re_path(r'^.*$', StockItemTestReportDetail.as_view(), name='api-stockitem-testreport-detail'),
|
|
])),
|
|
|
|
# List view
|
|
re_path(r'^.*$', StockItemTestReportList.as_view(), name='api-stockitem-testreport-list'),
|
|
])),
|
|
|
|
# Stock Location reports (Stock Location Reports -> sir)
|
|
re_path(r'slr/', include([
|
|
# Detail views
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'print/?', StockLocationReportPrint.as_view(), name='api-stocklocation-report-print'),
|
|
re_path(r'metadata/', MetadataView.as_view(), {'report': StockLocationReport}, name='api-stocklocation-report-metadata'),
|
|
re_path(r'^.*$', StockLocationReportDetail.as_view(), name='api-stocklocation-report-detail'),
|
|
])),
|
|
|
|
# List view
|
|
re_path(r'^.*$', StockLocationReportList.as_view(), name='api-stocklocation-report-list'),
|
|
])),
|
|
|
|
]
|