mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds model for BuildReport
- List / Detail / Print
This commit is contained in:
parent
e8fd336612
commit
a349e77866
@ -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)
|
||||
|
@ -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<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
|
||||
url(r'bom/', include([
|
||||
|
||||
|
30
InvenTree/report/migrations/0012_buildreport.py
Normal file
30
InvenTree/report/migrations/0012_buildreport.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
@ -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)
|
||||
def get_context_data(self, request):
|
||||
"""
|
||||
Custom context data for the build report
|
||||
"""
|
||||
|
||||
if os.path.exists(fullpath):
|
||||
logger.info(f"Deleting existing snippet file: '{filename}'")
|
||||
os.remove(fullpath)
|
||||
my_build = self.object_to_print
|
||||
|
||||
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):
|
||||
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user