From a349e77866a43dbe8dc6470d43a2644a50848b48 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Feb 2021 08:25:04 +1100 Subject: [PATCH] Adds model for BuildReport - List / Detail / Print --- InvenTree/report/admin.py | 2 + InvenTree/report/api.py | 138 +++++++++++++++++- .../report/migrations/0012_buildreport.py | 30 ++++ InvenTree/report/models.py | 73 +++++++-- InvenTree/report/serializers.py | 17 +++ 5 files changed, 245 insertions(+), 15 deletions(-) create mode 100644 InvenTree/report/migrations/0012_buildreport.py diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 66f6a1cd6d..2c008877cc 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -5,6 +5,7 @@ from django.contrib import admin from .models import ReportSnippet, ReportAsset from .models import TestReport +from .models import BuildReport from .models import BillOfMaterialsReport @@ -27,4 +28,5 @@ admin.site.register(ReportSnippet, ReportSnippetAdmin) admin.site.register(ReportAsset, ReportAssetAdmin) admin.site.register(TestReport, ReportTemplateAdmin) +admin.site.register(BuildReport, ReportTemplateAdmin) admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin) diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 9d3eea573c..ede938af8b 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -16,12 +16,15 @@ import InvenTree.helpers from stock.models import StockItem +import build.models import part.models from .models import TestReport +from .models import BuildReport from .models import BillOfMaterialsReport from .serializers import TestReportSerializer +from .serializers import BuildReportSerializer from .serializers import BOMReportSerializer @@ -81,6 +84,39 @@ class StockItemReportMixin: return valid_items +class BuildReportMixin: + """ + Mixin for extracting Build items from query params + """ + + def get_builds(self): + """ + Return a list of requested Build objects + """ + + builds = [] + + params = self.request.query_params + + if 'builds[]' in params: + builds = params.getlist('builds[]', []) + elif 'build' in params: + builds = [params.get('build', None)] + + if type(builds) not in [list, tuple]: + builds = [builds] + + valid_ids = [] + + for b in builds: + try: + valid_ids.append(int(b)) + except (ValueError): + continue + + return build.models.Build.objects.filter(pk__in=valid_ids) + + class PartReportMixin: """ Mixin for extracting part items from query params @@ -349,7 +385,7 @@ class BOMReportDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = BOMReportSerializer -class BOMReportPrint(generics.RetrieveUpdateDestroyAPIView, PartReportMixin, ReportPrintMixin): +class BOMReportPrint(generics.RetrieveAPIView, PartReportMixin, ReportPrintMixin): """ API endpoint for printing a BillOfMaterialReport object """ @@ -367,8 +403,108 @@ class BOMReportPrint(generics.RetrieveUpdateDestroyAPIView, PartReportMixin, Rep return self.print(request, parts) +class BuildReportList(ReportListView, BuildReportMixin): + """ + API endpoint for viewing a list of BuildReport objects. + + Can be filtered by: + + - enabled: Filter by enabled / disabled status + - build: Filter by a single build + - builds[]: Filter by a list of builds + """ + + queryset = BuildReport.objects.all() + serializer_class = BuildReportSerializer + + def filter_queryset(self, queryset): + + queryset = super().filter_queryset(queryset) + + # List of Build objects to match against + builds = self.get_builds() + + if len(builds) > 0: + """ + We wish to filter by Build(s) + + We need to compare the 'filters' string of each report, + and see if it matches against each of the specified parts + + # TODO: This code needs some refactoring! + """ + + valid_build_ids = set() + + for report in queryset.all(): + + matches = True + + try: + filters = InvenTree.helpers.validateFilterString(report.filters) + except ValidationError: + continue + + for b in builds: + build_query = build.models.Build.objects.filter(pk=b.pk) + + try: + if not build_query.filter(**filters).exists(): + matches = False + break + except FieldError: + matches = False + break + + if matches: + valid_build_ids.add(report.pk) + else: + continue + + # Reduce queryset to only valid matches + queryset = queryset.filter(pk__in=[pk for pk in valid_build_ids]) + + return queryset + + +class BuildReportDetail(generics.RetrieveUpdateDestroyAPIView): + """ + API endpoint for a single BuildReport object + """ + + queryset = BuildReport.objects.all() + serializer_class = BuildReportSerializer + + +class BuildReportPrint(generics.RetrieveAPIView, BuildReportMixin, ReportPrintMixin): + """ + API endpoint for printing a BuildReport + """ + + queryset = BuildReport.objects.all() + serializer_class = BuildReportSerializer + + def get(self, request, *ars, **kwargs): + + builds = self.get_builds() + + return self.print(request, builds) + + report_api_urls = [ + # Build reports + url(r'build/', include([ + # Detail views + url(r'^(?P\d+)/', include([ + url(r'print/?', BuildReportPrint.as_view(), name='api-build-report-print'), + url(r'^.*$', BuildReportDetail.as_view(), name='api-build-report-detail'), + ])), + + # List view + url(r'^.*$', BuildReportList.as_view(), name='api-build-report-list'), + ])), + # Bill of Material reports url(r'bom/', include([ diff --git a/InvenTree/report/migrations/0012_buildreport.py b/InvenTree/report/migrations/0012_buildreport.py new file mode 100644 index 0000000000..b2d3603480 --- /dev/null +++ b/InvenTree/report/migrations/0012_buildreport.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.7 on 2021-02-15 21:08 + +import django.core.validators +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0011_auto_20210212_2024'), + ] + + operations = [ + migrations.CreateModel( + name='BuildReport', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Template name', max_length=100, verbose_name='Name')), + ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])], verbose_name='Template')), + ('description', models.CharField(help_text='Report template description', max_length=250, verbose_name='Description')), + ('revision', models.PositiveIntegerField(default=1, editable=False, help_text='Report revision number (auto-increments)', verbose_name='Revision')), + ('enabled', models.BooleanField(default=True, help_text='Report template is enabled', verbose_name='Enabled')), + ('filters', models.CharField(blank=True, help_text='Build query filters (comma-separated list of key=value pairs', max_length=250, validators=[report.models.validate_build_report_filters], verbose_name='Build Filters')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 669cbd6fb6..f052c0321d 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -20,9 +20,10 @@ from django.template.loader import render_to_string from django.core.files.storage import FileSystemStorage from django.core.validators import FileExtensionValidator +import build.models +import common.models import part.models import stock.models -import common.models from InvenTree.helpers import validateFilterString @@ -86,6 +87,14 @@ def validate_part_report_filters(filters): return validateFilterString(filters, model=part.models.Part) +def validate_build_report_filters(filters): + """ + Validate filter string against Build model + """ + + return validateFilterString(filters, model=build.models.Build) + + class WeasyprintReportMixin(WeasyTemplateResponseMixin): """ Class for rendering a HTML template to a PDF. @@ -298,23 +307,40 @@ class TestReport(ReportTemplateBase): } -def rename_snippet(instance, filename): +class BuildReport(ReportTemplateBase): + """ + Build order / work order report + """ - filename = os.path.basename(filename) + def getSubdir(self): + return 'build' - path = os.path.join('report', 'snippets', filename) + filters = models.CharField( + blank=True, + max_length=250, + verbose_name=_('Build Filters'), + help_text=_('Build query filters (comma-separated list of key=value pairs'), + validators=[ + validate_build_report_filters, + ] + ) - # If the snippet file is the *same* filename as the one being uploaded, - # delete the original one from the media directory - if str(filename) == str(instance.snippet): - fullpath = os.path.join(settings.MEDIA_ROOT, path) - fullpath = os.path.abspath(fullpath) - - if os.path.exists(fullpath): - logger.info(f"Deleting existing snippet file: '{filename}'") - os.remove(fullpath) + def get_context_data(self, request): + """ + Custom context data for the build report + """ - return path + my_build = self.object_to_print + + if not type(my_build) == build.models.Build: + raise TypeError('Provided model is not a Build object') + + return { + 'build': my_build, + 'part': my_build.part, + 'reference': my_build.reference, + 'quantity': my_build.quantity, + } class BillOfMaterialsReport(ReportTemplateBase): @@ -345,6 +371,25 @@ class BillOfMaterialsReport(ReportTemplateBase): } +def rename_snippet(instance, filename): + + filename = os.path.basename(filename) + + path = os.path.join('report', 'snippets', filename) + + # If the snippet file is the *same* filename as the one being uploaded, + # delete the original one from the media directory + if str(filename) == str(instance.snippet): + fullpath = os.path.join(settings.MEDIA_ROOT, path) + fullpath = os.path.abspath(fullpath) + + if os.path.exists(fullpath): + logger.info(f"Deleting existing snippet file: '{filename}'") + os.remove(fullpath) + + return path + + class ReportSnippet(models.Model): """ Report template 'snippet' which can be used to make templates diff --git a/InvenTree/report/serializers.py b/InvenTree/report/serializers.py index 4474a276a2..4868406ed5 100644 --- a/InvenTree/report/serializers.py +++ b/InvenTree/report/serializers.py @@ -5,6 +5,7 @@ from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField from .models import TestReport +from .models import BuildReport from .models import BillOfMaterialsReport @@ -24,6 +25,22 @@ class TestReportSerializer(InvenTreeModelSerializer): ] +class BuildReportSerializer(InvenTreeModelSerializer): + + template = InvenTreeAttachmentSerializerField(required=True) + + class Meta: + model = BuildReport + fields = [ + 'pk', + 'name', + 'description', + 'template', + 'filters', + 'enabled', + ] + + class BOMReportSerializer(InvenTreeModelSerializer): template = InvenTreeAttachmentSerializerField(required=True)