Adds model for BuildReport

- List / Detail / Print
This commit is contained in:
Oliver Walters 2021-02-16 08:25:04 +11:00
parent e8fd336612
commit a349e77866
5 changed files with 245 additions and 15 deletions

View File

@ -5,6 +5,7 @@ from django.contrib import admin
from .models import ReportSnippet, ReportAsset from .models import ReportSnippet, ReportAsset
from .models import TestReport from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport from .models import BillOfMaterialsReport
@ -27,4 +28,5 @@ admin.site.register(ReportSnippet, ReportSnippetAdmin)
admin.site.register(ReportAsset, ReportAssetAdmin) admin.site.register(ReportAsset, ReportAssetAdmin)
admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(TestReport, ReportTemplateAdmin)
admin.site.register(BuildReport, ReportTemplateAdmin)
admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin) admin.site.register(BillOfMaterialsReport, ReportTemplateAdmin)

View File

@ -16,12 +16,15 @@ import InvenTree.helpers
from stock.models import StockItem from stock.models import StockItem
import build.models
import part.models import part.models
from .models import TestReport from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport from .models import BillOfMaterialsReport
from .serializers import TestReportSerializer from .serializers import TestReportSerializer
from .serializers import BuildReportSerializer
from .serializers import BOMReportSerializer from .serializers import BOMReportSerializer
@ -81,6 +84,39 @@ class StockItemReportMixin:
return valid_items 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: class PartReportMixin:
""" """
Mixin for extracting part items from query params Mixin for extracting part items from query params
@ -349,7 +385,7 @@ class BOMReportDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = BOMReportSerializer serializer_class = BOMReportSerializer
class BOMReportPrint(generics.RetrieveUpdateDestroyAPIView, PartReportMixin, ReportPrintMixin): class BOMReportPrint(generics.RetrieveAPIView, PartReportMixin, ReportPrintMixin):
""" """
API endpoint for printing a BillOfMaterialReport object API endpoint for printing a BillOfMaterialReport object
""" """
@ -367,8 +403,108 @@ class BOMReportPrint(generics.RetrieveUpdateDestroyAPIView, PartReportMixin, Rep
return self.print(request, parts) 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 = [ report_api_urls = [
# Build reports
url(r'build/', include([
# Detail views
url(r'^(?P<pk>\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 # Bill of Material reports
url(r'bom/', include([ url(r'bom/', include([

View File

@ -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,
},
),
]

View File

@ -20,9 +20,10 @@ from django.template.loader import render_to_string
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
import build.models
import common.models
import part.models import part.models
import stock.models import stock.models
import common.models
from InvenTree.helpers import validateFilterString from InvenTree.helpers import validateFilterString
@ -86,6 +87,14 @@ def validate_part_report_filters(filters):
return validateFilterString(filters, model=part.models.Part) 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 WeasyprintReportMixin(WeasyTemplateResponseMixin):
""" """
Class for rendering a HTML template to a PDF. 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, def get_context_data(self, request):
# delete the original one from the media directory """
if str(filename) == str(instance.snippet): Custom context data for the build report
fullpath = os.path.join(settings.MEDIA_ROOT, path) """
fullpath = os.path.abspath(fullpath)
if os.path.exists(fullpath): my_build = self.object_to_print
logger.info(f"Deleting existing snippet file: '{filename}'")
os.remove(fullpath)
return path 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): 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): class ReportSnippet(models.Model):
""" """
Report template 'snippet' which can be used to make templates Report template 'snippet' which can be used to make templates

View File

@ -5,6 +5,7 @@ from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializerField from InvenTree.serializers import InvenTreeAttachmentSerializerField
from .models import TestReport from .models import TestReport
from .models import BuildReport
from .models import BillOfMaterialsReport 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): class BOMReportSerializer(InvenTreeModelSerializer):
template = InvenTreeAttachmentSerializerField(required=True) template = InvenTreeAttachmentSerializerField(required=True)