From 3d8c059a43a2ef2cbb5751bc422ed3cadb5e9884 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 13:51:13 +1000 Subject: [PATCH 01/19] Add "report" app - Define ReportTemplate model which contains a report file template --- InvenTree/InvenTree/settings.py | 9 ++--- InvenTree/report/__init__.py | 0 InvenTree/report/admin.py | 14 ++++++++ InvenTree/report/apps.py | 5 +++ InvenTree/report/migrations/0001_initial.py | 23 ++++++++++++ .../migrations/0002_auto_20200521_0350.py | 20 +++++++++++ InvenTree/report/migrations/__init__.py | 0 InvenTree/report/models.py | 36 +++++++++++++++++++ InvenTree/report/tests.py | 3 ++ InvenTree/report/views.py | 3 ++ Makefile | 4 +-- 11 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 InvenTree/report/__init__.py create mode 100644 InvenTree/report/admin.py create mode 100644 InvenTree/report/apps.py create mode 100644 InvenTree/report/migrations/0001_initial.py create mode 100644 InvenTree/report/migrations/0002_auto_20200521_0350.py create mode 100644 InvenTree/report/migrations/__init__.py create mode 100644 InvenTree/report/models.py create mode 100644 InvenTree/report/tests.py create mode 100644 InvenTree/report/views.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index dda110e834..3d87527216 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -106,12 +106,13 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', # InvenTree apps - 'common.apps.CommonConfig', - 'part.apps.PartConfig', - 'stock.apps.StockConfig', - 'company.apps.CompanyConfig', 'build.apps.BuildConfig', + 'common.apps.CommonConfig', + 'company.apps.CompanyConfig', 'order.apps.OrderConfig', + 'part.apps.PartConfig', + 'report.apps.ReportConfig', + 'stock.apps.StockConfig', # Third part add-ons 'django_filters', # Extended filter functionality diff --git a/InvenTree/report/__init__.py b/InvenTree/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py new file mode 100644 index 0000000000..1d5d1fee01 --- /dev/null +++ b/InvenTree/report/admin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +from .models import ReportTemplate + + +class ReportTemplateAdmin(admin.ModelAdmin): + + list_display = ('template', 'description') + + +admin.site.register(ReportTemplate, ReportTemplateAdmin) diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py new file mode 100644 index 0000000000..138ba20404 --- /dev/null +++ b/InvenTree/report/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReportConfig(AppConfig): + name = 'report' diff --git a/InvenTree/report/migrations/0001_initial.py b/InvenTree/report/migrations/0001_initial.py new file mode 100644 index 0000000000..5d50070c64 --- /dev/null +++ b/InvenTree/report/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-05-21 03:43 + +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ReportTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template)), + ('description', models.CharField(help_text='Report template description', max_length=250)), + ], + ), + ] diff --git a/InvenTree/report/migrations/0002_auto_20200521_0350.py b/InvenTree/report/migrations/0002_auto_20200521_0350.py new file mode 100644 index 0000000000..6a88531438 --- /dev/null +++ b/InvenTree/report/migrations/0002_auto_20200521_0350.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.5 on 2020-05-21 03:50 + +import django.core.validators +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='reporttemplate', + name='template', + field=models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'tex'])]), + ), + ] diff --git a/InvenTree/report/migrations/__init__.py b/InvenTree/report/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py new file mode 100644 index 0000000000..f155407573 --- /dev/null +++ b/InvenTree/report/models.py @@ -0,0 +1,36 @@ +""" +Report template model definitions +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +from django.db import models +from django.core.validators import FileExtensionValidator + +from django.utils.translation import gettext_lazy as _ + + +def rename_template(instance, filename): + + filename = os.path.basename(filename) + + return os.path.join('report', 'template', filename) + +class ReportTemplate(models.Model): + """ + Reporting template model. + """ + + def __str__(self): + return os.path.basename(self.template.name) + + template = models.FileField( + upload_to=rename_template, + help_text=_("Report template file"), + validators=[FileExtensionValidator(allowed_extensions=['html', 'tex'])], + ) + + description = models.CharField(max_length=250, help_text=_("Report template description")) diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/InvenTree/report/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/InvenTree/report/views.py b/InvenTree/report/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/InvenTree/report/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/Makefile b/Makefile index 630784becf..556813a149 100644 --- a/Makefile +++ b/Makefile @@ -51,12 +51,12 @@ style: # Run unit tests test: cd InvenTree && python3 manage.py check - cd InvenTree && python3 manage.py test build common company order part stock + cd InvenTree && python3 manage.py test build common company order part report stock InvenTree # Run code coverage coverage: cd InvenTree && python3 manage.py check - coverage run InvenTree/manage.py test build common company order part stock InvenTree + coverage run InvenTree/manage.py test build common company order part report stock InvenTree coverage html # Install packages required to generate code docs From b78fe88c269cf26b7483972562adb274f968bfc4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 13:53:17 +1000 Subject: [PATCH 02/19] PEP fixes --- InvenTree/report/models.py | 1 + InvenTree/report/tests.py | 5 ++--- InvenTree/report/views.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index f155407573..2658a28f4d 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -19,6 +19,7 @@ def rename_template(instance, filename): return os.path.join('report', 'template', filename) + class ReportTemplate(models.Model): """ Reporting template model. diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py index 7ce503c2dd..a2b5079b33 100644 --- a/InvenTree/report/tests.py +++ b/InvenTree/report/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase - -# Create your tests here. +# -*- coding: utf-8 -*- +from __future__ import unicode_literals diff --git a/InvenTree/report/views.py b/InvenTree/report/views.py index 91ea44a218..a2b5079b33 100644 --- a/InvenTree/report/views.py +++ b/InvenTree/report/views.py @@ -1,3 +1,2 @@ -from django.shortcuts import render - -# Create your views here. +# -*- coding: utf-8 -*- +from __future__ import unicode_literals From 70c5b27d2243c912b22dd00c04e87446fe037e5c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 14:05:25 +1000 Subject: [PATCH 03/19] Add ReportAsset model - Files which can be embedded into a report --- InvenTree/InvenTree/settings.py | 52 +++++++++---------- InvenTree/report/admin.py | 8 ++- .../report/migrations/0003_reportasset.py | 22 ++++++++ InvenTree/report/models.py | 28 +++++++++- .../stock/templates/stock/item_base.html | 2 + 5 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 InvenTree/report/migrations/0003_reportasset.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 3d87527216..6cadb09150 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -72,6 +72,27 @@ if DEBUG: format='%(asctime)s %(levelname)s %(message)s', ) +# Web URL endpoint for served static files +STATIC_URL = '/static/' + +# The filesystem location for served static files +STATIC_ROOT = os.path.abspath(CONFIG.get('static_root', os.path.join(BASE_DIR, 'static'))) + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'InvenTree', 'static'), +] + +# Web URL endpoint for served media files +MEDIA_URL = '/media/' + +# The filesystem location for served static files +MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))) + +if DEBUG: + print("InvenTree running in DEBUG mode") + print("MEDIA_ROOT:", MEDIA_ROOT) + print("STATIC_ROOT:", STATIC_ROOT) + # Does the user wish to use the sentry.io integration? sentry_opts = CONFIG.get('sentry', {}) @@ -161,7 +182,11 @@ ROOT_URLCONF = 'InvenTree.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + # Allow templates in the reporting directory to be accessed + os.path.join(MEDIA_ROOT, 'report'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -316,31 +341,6 @@ DATE_INPUT_FORMATS = [ "%Y-%m-%d", ] - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - -# Web URL endpoint for served static files -STATIC_URL = '/static/' - -# The filesystem location for served static files -STATIC_ROOT = os.path.abspath(CONFIG.get('static_root', os.path.join(BASE_DIR, 'static'))) - -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'InvenTree', 'static'), -] - -# Web URL endpoint for served media files -MEDIA_URL = '/media/' - -# The filesystem location for served static files -MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))) - -if DEBUG: - print("InvenTree running in DEBUG mode") - print("MEDIA_ROOT:", MEDIA_ROOT) - print("STATIC_ROOT:", STATIC_ROOT) - # crispy forms use the bootstrap templates CRISPY_TEMPLATE_PACK = 'bootstrap3' diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 1d5d1fee01..fc4c914358 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import ReportTemplate +from .models import ReportTemplate, ReportAsset class ReportTemplateAdmin(admin.ModelAdmin): @@ -11,4 +11,10 @@ class ReportTemplateAdmin(admin.ModelAdmin): list_display = ('template', 'description') +class ReportAssetAdmin(admin.ModelAdmin): + + list_display = ('asset', 'description') + + admin.site.register(ReportTemplate, ReportTemplateAdmin) +admin.site.register(ReportAsset, ReportAssetAdmin) diff --git a/InvenTree/report/migrations/0003_reportasset.py b/InvenTree/report/migrations/0003_reportasset.py new file mode 100644 index 0000000000..6f9c8efd03 --- /dev/null +++ b/InvenTree/report/migrations/0003_reportasset.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.5 on 2020-05-21 04:04 + +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0002_auto_20200521_0350'), + ] + + operations = [ + migrations.CreateModel( + name='ReportAsset', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('asset', models.FileField(help_text='Report asset file', upload_to=report.models.rename_asset)), + ('description', models.CharField(help_text='Asset file description', max_length=250)), + ], + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 2658a28f4d..eeebbfbee9 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -17,7 +17,7 @@ def rename_template(instance, filename): filename = os.path.basename(filename) - return os.path.join('report', 'template', filename) + return os.path.join('report', 'report_template', filename) class ReportTemplate(models.Model): @@ -35,3 +35,29 @@ class ReportTemplate(models.Model): ) description = models.CharField(max_length=250, help_text=_("Report template description")) + + +def rename_asset(instance, filename): + + filename = os.path.basename(filename) + + return os.path.join('report', 'assets', filename) + + +class ReportAsset(models.Model): + """ + Asset file for use in report templates. + For example, an image to use in a header file. + Uploaded asset files appear in MEDIA_ROOT/report/assets, + and can be loaded in a template using the {% report_asset %} tag. + """ + + def __str__(self): + return os.path.basename(self.asset.name) + + asset = models.FileField( + upload_to=rename_asset, + help_text=_("Report asset file"), + ) + + description = models.CharField(max_length=250, help_text=_("Asset file description")) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 782e51a2b8..4a12faff5e 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,6 +15,8 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} +{% include "report_template/test.html" with item=item %} + {% if item.hasRequiredTests and not item.passedAllRequiredTests %}
{% trans "This stock item has not passed all required tests" %} From 05be4da25cbafd40727e0e3b7f23e97d08026f0b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 14:06:29 +1000 Subject: [PATCH 04/19] remove test code --- InvenTree/stock/templates/stock/item_base.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 4a12faff5e..782e51a2b8 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,8 +15,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} -{% include "report_template/test.html" with item=item %} - {% if item.hasRequiredTests and not item.passedAllRequiredTests %}
{% trans "This stock item has not passed all required tests" %} From c3dcabcaad05f48e0c835bdc77770458d4a501b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 23:03:01 +1000 Subject: [PATCH 05/19] Render an uploaded template to LaTeX --- InvenTree/InvenTree/settings.py | 20 ++++++++++++ InvenTree/config_template.yaml | 9 ++++++ .../migrations/0004_auto_20200521_1217.py | 25 +++++++++++++++ InvenTree/report/models.py | 31 +++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 InvenTree/report/migrations/0004_auto_20200521_1217.py diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 6cadb09150..e325e092da 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -148,6 +148,7 @@ INSTALLED_APPS = [ 'mptt', # Modified Preorder Tree Traversal 'markdownx', # Markdown editing 'markdownify', # Markdown template rendering + 'django_tex', # LaTeX output ] LOGGING = { @@ -199,6 +200,14 @@ TEMPLATES = [ ], }, }, + # Backend for LaTeX report rendering + { + 'NAME': 'tex', + 'BACKEND': 'django_tex.engine.TeXEngine', + 'DIRS': [ + os.path.join(MEDIA_ROOT, 'report'), + ] + }, ] REST_FRAMEWORK = { @@ -341,6 +350,17 @@ DATE_INPUT_FORMATS = [ "%Y-%m-%d", ] +# LaTeX rendering settings (django-tex) +latex_settings = CONFIG.get('latex', {}) + +# Set the latex interpreter in the config.yaml settings file +LATEX_INTERPRETER = latex_settings.get('interpreter', 'pdflatex') + +LATEX_GRAPHICSPATH = [ + # Allow LaTeX files to access the report assets directory + os.path.join(MEDIA_ROOT, "report", "assets"), +] + # crispy forms use the bootstrap templates CRISPY_TEMPLATE_PACK = 'bootstrap3' diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 64c5db0a06..686f910fbf 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -73,3 +73,12 @@ log_queries: False sentry: enabled: False # dsn: add-your-sentry-dsn-here + +# LaTeX report rendering +# InvenTree uses the django-tex plugin to enable LaTeX report rendering +# Ref: https://pypi.org/project/django-tex/ +latex: + # Select the LaTeX interpreter to use for PDF rendering + # Note: The intepreter needs to be installed on the system! + # e.g. to install pdflatx: apt-get texlive-latex-base + interpreter: pdflatex diff --git a/InvenTree/report/migrations/0004_auto_20200521_1217.py b/InvenTree/report/migrations/0004_auto_20200521_1217.py new file mode 100644 index 0000000000..c4342ada3d --- /dev/null +++ b/InvenTree/report/migrations/0004_auto_20200521_1217.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.5 on 2020-05-21 12:17 + +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0003_reportasset'), + ] + + operations = [ + migrations.AddField( + model_name='reporttemplate', + name='filters', + field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_filter_string]), + ), + migrations.AddField( + model_name='reporttemplate', + name='name', + field=models.CharField(default='Test', help_text='Template name', max_length=100, unique=True), + preserve_default=False, + ), + ] diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index eeebbfbee9..4785f79ab8 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -12,6 +12,8 @@ from django.core.validators import FileExtensionValidator from django.utils.translation import gettext_lazy as _ +from django_tex.shortcuts import render_to_pdf + def rename_template(instance, filename): @@ -28,6 +30,28 @@ class ReportTemplate(models.Model): def __str__(self): return os.path.basename(self.template.name) + @property + def extension(self): + return os.path.splitext(self.template.name)[1].lower() + + def render(self, request, context={}, **kwargs): + """ + Render to template. + """ + + filename = kwargs.get('filename', 'report.pdf') + + template = os.path.join('report_template', os.path.basename(self.template.name)) + + if 1 or self.extension == '.tex': + return render_to_pdf(request, template, context, filename=filename) + + name = models.CharField( + blank=False, max_length=100, + help_text=_('Template name'), + unique=True, + ) + template = models.FileField( upload_to=rename_template, help_text=_("Report template file"), @@ -36,6 +60,13 @@ class ReportTemplate(models.Model): description = models.CharField(max_length=250, help_text=_("Report template description")) + filters = models.CharField( + blank=True, + max_length=250, + help_text=_("Query filters (comma-separated list of key=value pairs)"), + validators=[validate_filter_string] + ) + def rename_asset(instance, filename): From b93ba6339a3d0414dd00a815a004c416e0a3577e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 21 May 2020 23:41:47 +1000 Subject: [PATCH 06/19] Option for rendering HTML template --- InvenTree/report/models.py | 38 +++++++++++++++++++++++++++++++++----- requirements.txt | 4 +++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 4785f79ab8..1e30fd1c3e 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -13,6 +13,7 @@ from django.core.validators import FileExtensionValidator from django.utils.translation import gettext_lazy as _ from django_tex.shortcuts import render_to_pdf +from django_weasyprint import WeasyTemplateResponseMixin def rename_template(instance, filename): @@ -22,6 +23,21 @@ def rename_template(instance, filename): return os.path.join('report', 'report_template', filename) +class WeasyprintReportMixin(WeasyTemplateResponseMixin): + """ + Class for rendering a HTML template to a PDF. + """ + + pdf_filename = 'report.pdf' + pdf_attachment = True + + def __init__(self, request, template, **kwargs): + + self.request = request + self.template_name = template + self.pdf_filename = kwargs.get('filename', 'report.pdf') + + class ReportTemplate(models.Model): """ Reporting template model. @@ -34,17 +50,29 @@ class ReportTemplate(models.Model): def extension(self): return os.path.splitext(self.template.name)[1].lower() + @property + def template_name(self): + return os.path.join('report_template', os.path.basename(self.template.name)) + def render(self, request, context={}, **kwargs): """ - Render to template. + Render the template to a PDF file. + + Supported template formats: + .tex - Uses django-tex plugin to render LaTeX template against an installed LaTeX engine + .html - Uses django-weasyprint plugin to render HTML template against Weasyprint """ filename = kwargs.get('filename', 'report.pdf') - template = os.path.join('report_template', os.path.basename(self.template.name)) + context['request'] = request + + if self.extension == '.tex': + return render_to_pdf(request, self.template_name, context, filename=filename) + elif self.extension in ['.htm', '.html']: + wp = WeasyprintReportMixin(request, self.template_name, **kwargs) + return wp.render_to_response(context, **kwargs) - if 1 or self.extension == '.tex': - return render_to_pdf(request, template, context, filename=filename) name = models.CharField( blank=False, max_length=100, @@ -55,7 +83,7 @@ class ReportTemplate(models.Model): template = models.FileField( upload_to=rename_template, help_text=_("Report template file"), - validators=[FileExtensionValidator(allowed_extensions=['html', 'tex'])], + validators=[FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])], ) description = models.CharField(max_length=250, help_text=_("Report template description")) diff --git a/requirements.txt b/requirements.txt index 9d6ad3e3af..33f70b4bcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,6 @@ flake8==3.3.0 # PEP checking coverage==4.0.3 # Unit test coverage python-coveralls==2.9.1 # Coveralls linking (for Travis) rapidfuzz==0.7.6 # Fuzzy string matching -django-stdimage==5.1.1 # Advanced ImageField management \ No newline at end of file +django-stdimage==5.1.1 # Advanced ImageField management +django-tex==1.1.7 # LaTeX PDF export +django-weasyprint==1.0.1 # HTML PDF export \ No newline at end of file From cab87a686046e7a2c881e8ed5eddc573cb6441f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 00:01:36 +1000 Subject: [PATCH 07/19] Update admin --- InvenTree/report/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index fc4c914358..683a188c80 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -8,7 +8,7 @@ from .models import ReportTemplate, ReportAsset class ReportTemplateAdmin(admin.ModelAdmin): - list_display = ('template', 'description') + list_display = ('name', 'description', 'template') class ReportAssetAdmin(admin.ModelAdmin): From 251a23d1275fe05137710b8d3b0fe993149a2d7d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 00:09:51 +1000 Subject: [PATCH 08/19] Cleanup --- InvenTree/InvenTree/settings.py | 4 +++- InvenTree/config_template.yaml | 2 ++ InvenTree/report/models.py | 14 ++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e325e092da..8e4feade23 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -203,7 +203,7 @@ TEMPLATES = [ # Backend for LaTeX report rendering { 'NAME': 'tex', - 'BACKEND': 'django_tex.engine.TeXEngine', + 'BACKEND': 'django_tex.engine.TeXEngine', 'DIRS': [ os.path.join(MEDIA_ROOT, 'report'), ] @@ -356,6 +356,8 @@ latex_settings = CONFIG.get('latex', {}) # Set the latex interpreter in the config.yaml settings file LATEX_INTERPRETER = latex_settings.get('interpreter', 'pdflatex') +LATEX_INTERPRETER_OPTIONS = latex_settings.get('options', '') + LATEX_GRAPHICSPATH = [ # Allow LaTeX files to access the report assets directory os.path.join(MEDIA_ROOT, "report", "assets"), diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 686f910fbf..b5446b8e89 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -82,3 +82,5 @@ latex: # Note: The intepreter needs to be installed on the system! # e.g. to install pdflatx: apt-get texlive-latex-base interpreter: pdflatex + # Extra options to pass through to the LaTeX interpreter + options: '' \ No newline at end of file diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 1e30fd1c3e..a2dae298e7 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -54,7 +54,14 @@ class ReportTemplate(models.Model): def template_name(self): return os.path.join('report_template', os.path.basename(self.template.name)) - def render(self, request, context={}, **kwargs): + def get_context_data(self, request): + """ + Supply context data to the template for rendering + """ + + return {} + + def render(self, request, **kwargs): """ Render the template to a PDF file. @@ -65,15 +72,18 @@ class ReportTemplate(models.Model): filename = kwargs.get('filename', 'report.pdf') + context = self.get_context_data(request) + context['request'] = request if self.extension == '.tex': + # Render LaTeX template to PDF return render_to_pdf(request, self.template_name, context, filename=filename) elif self.extension in ['.htm', '.html']: + # Render HTML template to PDF wp = WeasyprintReportMixin(request, self.template_name, **kwargs) return wp.render_to_response(context, **kwargs) - name = models.CharField( blank=False, max_length=100, help_text=_('Template name'), From 174c4cc591d21ebfafe4a78a6a3d0252273b8697 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 13:01:21 +1000 Subject: [PATCH 09/19] Add subclass models for report types --- InvenTree/config_template.yaml | 2 +- InvenTree/report/admin.py | 3 ++- InvenTree/report/models.py | 36 ++++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index b5446b8e89..5cb0b1073c 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -80,7 +80,7 @@ sentry: latex: # Select the LaTeX interpreter to use for PDF rendering # Note: The intepreter needs to be installed on the system! - # e.g. to install pdflatx: apt-get texlive-latex-base + # e.g. to install pdflatex: apt-get texlive-latex-base interpreter: pdflatex # Extra options to pass through to the LaTeX interpreter options: '' \ No newline at end of file diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 683a188c80..15d44931df 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ReportTemplate, ReportAsset - +from .models import TestReport class ReportTemplateAdmin(admin.ModelAdmin): @@ -17,4 +17,5 @@ class ReportAssetAdmin(admin.ModelAdmin): admin.site.register(ReportTemplate, ReportTemplateAdmin) +admin.site.register(TestReport, ReportTemplateAdmin) admin.site.register(ReportAsset, ReportAssetAdmin) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index a2dae298e7..626d8a5923 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -38,7 +38,7 @@ class WeasyprintReportMixin(WeasyTemplateResponseMixin): self.pdf_filename = kwargs.get('filename', 'report.pdf') -class ReportTemplate(models.Model): +class ReportTemplateBase(models.Model): """ Reporting template model. """ @@ -46,13 +46,16 @@ class ReportTemplate(models.Model): def __str__(self): return os.path.basename(self.template.name) + def getSubdir(self): + return '' + @property def extension(self): return os.path.splitext(self.template.name)[1].lower() @property def template_name(self): - return os.path.join('report_template', os.path.basename(self.template.name)) + return os.path.join('report_template', self.getSubdir(), os.path.basename(self.template.name)) def get_context_data(self, request): """ @@ -105,6 +108,35 @@ class ReportTemplate(models.Model): validators=[validate_filter_string] ) + class Meta: + abstract = True + + +class ReportTemplate(ReportTemplateBase): + """ + A simple reporting template which is used to upload template files, + which can then be used in other concrete template classes. + """ + + pass + + +class TestReport(ReportTemplateBase): + """ + Render a TestReport against a StockItem object. + """ + + def getSubdir(self): + return 'test' + + stock_item = None + + def get_context_data(self, request): + return { + 'stock_item': self.stock_item, + 'results': self.stock_item.testResultMap() + } + def rename_asset(instance, filename): From 865a6db828231b32c5be66ff4a65c091ae8efbcf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 13:05:12 +1000 Subject: [PATCH 10/19] Fix subdirectory lookup --- InvenTree/report/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 626d8a5923..46fdd965e7 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -20,7 +20,7 @@ def rename_template(instance, filename): filename = os.path.basename(filename) - return os.path.join('report', 'report_template', filename) + return os.path.join('report', 'report_template', instance.getSubdir(), filename) class WeasyprintReportMixin(WeasyTemplateResponseMixin): From d6cad372db485e6bd3fa66b22c0d57bb13109ddc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 13:05:30 +1000 Subject: [PATCH 11/19] Add migration --- .../migrations/0005_auto_20200522_0220.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 InvenTree/report/migrations/0005_auto_20200522_0220.py diff --git a/InvenTree/report/migrations/0005_auto_20200522_0220.py b/InvenTree/report/migrations/0005_auto_20200522_0220.py new file mode 100644 index 0000000000..d3c2d52520 --- /dev/null +++ b/InvenTree/report/migrations/0005_auto_20200522_0220.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.5 on 2020-05-22 02:20 + +import django.core.validators +from django.db import migrations, models +import report.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('report', '0004_auto_20200521_1217'), + ] + + operations = [ + migrations.CreateModel( + name='TestReport', + 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, unique=True)), + ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])])), + ('description', models.CharField(help_text='Report template description', max_length=250)), + ('filters', models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_filter_string])), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='reporttemplate', + name='template', + field=models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])]), + ), + ] From 616f17d08a8f2137e5b10ff0ea8281e482e3b281 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 21:01:08 +1000 Subject: [PATCH 12/19] Reset the report app migrations --- InvenTree/report/migrations/0001_initial.py | 30 +++++++++++++++-- .../migrations/0002_auto_20200521_0350.py | 20 ----------- .../report/migrations/0003_reportasset.py | 22 ------------- .../migrations/0004_auto_20200521_1217.py | 25 -------------- .../migrations/0005_auto_20200522_0220.py | 33 ------------------- 5 files changed, 28 insertions(+), 102 deletions(-) delete mode 100644 InvenTree/report/migrations/0002_auto_20200521_0350.py delete mode 100644 InvenTree/report/migrations/0003_reportasset.py delete mode 100644 InvenTree/report/migrations/0004_auto_20200521_1217.py delete mode 100644 InvenTree/report/migrations/0005_auto_20200522_0220.py diff --git a/InvenTree/report/migrations/0001_initial.py b/InvenTree/report/migrations/0001_initial.py index 5d50070c64..8b5c2af09f 100644 --- a/InvenTree/report/migrations/0001_initial.py +++ b/InvenTree/report/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 3.0.5 on 2020-05-21 03:43 +# Generated by Django 3.0.5 on 2020-05-22 11:00 +import django.core.validators from django.db import migrations, models import report.models @@ -12,12 +13,37 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='ReportAsset', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('asset', models.FileField(help_text='Report asset file', upload_to=report.models.rename_asset)), + ('description', models.CharField(help_text='Asset file description', max_length=250)), + ], + ), migrations.CreateModel( name='ReportTemplate', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template)), + ('name', models.CharField(help_text='Template name', max_length=100, unique=True)), + ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])])), ('description', models.CharField(help_text='Report template description', max_length=250)), ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TestReport', + 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, unique=True)), + ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])])), + ('description', models.CharField(help_text='Report template description', max_length=250)), + ('part_filters', models.CharField(blank=True, help_text='Part query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validateFilterString])), + ], + options={ + 'abstract': False, + }, ), ] diff --git a/InvenTree/report/migrations/0002_auto_20200521_0350.py b/InvenTree/report/migrations/0002_auto_20200521_0350.py deleted file mode 100644 index 6a88531438..0000000000 --- a/InvenTree/report/migrations/0002_auto_20200521_0350.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.0.5 on 2020-05-21 03:50 - -import django.core.validators -from django.db import migrations, models -import report.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('report', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='reporttemplate', - name='template', - field=models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'tex'])]), - ), - ] diff --git a/InvenTree/report/migrations/0003_reportasset.py b/InvenTree/report/migrations/0003_reportasset.py deleted file mode 100644 index 6f9c8efd03..0000000000 --- a/InvenTree/report/migrations/0003_reportasset.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.0.5 on 2020-05-21 04:04 - -from django.db import migrations, models -import report.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('report', '0002_auto_20200521_0350'), - ] - - operations = [ - migrations.CreateModel( - name='ReportAsset', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('asset', models.FileField(help_text='Report asset file', upload_to=report.models.rename_asset)), - ('description', models.CharField(help_text='Asset file description', max_length=250)), - ], - ), - ] diff --git a/InvenTree/report/migrations/0004_auto_20200521_1217.py b/InvenTree/report/migrations/0004_auto_20200521_1217.py deleted file mode 100644 index c4342ada3d..0000000000 --- a/InvenTree/report/migrations/0004_auto_20200521_1217.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.0.5 on 2020-05-21 12:17 - -from django.db import migrations, models -import report.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('report', '0003_reportasset'), - ] - - operations = [ - migrations.AddField( - model_name='reporttemplate', - name='filters', - field=models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_filter_string]), - ), - migrations.AddField( - model_name='reporttemplate', - name='name', - field=models.CharField(default='Test', help_text='Template name', max_length=100, unique=True), - preserve_default=False, - ), - ] diff --git a/InvenTree/report/migrations/0005_auto_20200522_0220.py b/InvenTree/report/migrations/0005_auto_20200522_0220.py deleted file mode 100644 index d3c2d52520..0000000000 --- a/InvenTree/report/migrations/0005_auto_20200522_0220.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.5 on 2020-05-22 02:20 - -import django.core.validators -from django.db import migrations, models -import report.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('report', '0004_auto_20200521_1217'), - ] - - operations = [ - migrations.CreateModel( - name='TestReport', - 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, unique=True)), - ('template', models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])])), - ('description', models.CharField(help_text='Report template description', max_length=250)), - ('filters', models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs)', max_length=250, validators=[report.models.validate_filter_string])), - ], - options={ - 'abstract': False, - }, - ), - migrations.AlterField( - model_name='reporttemplate', - name='template', - field=models.FileField(help_text='Report template file', upload_to=report.models.rename_template, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])]), - ), - ] From 7215a563b19cf470d77adc299109861e0166a098 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 21:22:43 +1000 Subject: [PATCH 13/19] Add PartFilterMixin --- InvenTree/report/models.py | 100 ++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 46fdd965e7..ffd77a09b1 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -9,9 +9,12 @@ import os from django.db import models from django.core.validators import FileExtensionValidator +from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from part.models import Part + from django_tex.shortcuts import render_to_pdf from django_weasyprint import WeasyTemplateResponseMixin @@ -23,6 +26,59 @@ def rename_template(instance, filename): return os.path.join('report', 'report_template', instance.getSubdir(), filename) +def validateFilterString(value): + """ + Validate that a provided filter string looks like a list of comma-separated key=value pairs + + These should nominally match to a valid database filter based on the model being filtered. + + e.g. "category=6, IPN=12" + e.g. "part__name=widget" + + The ReportTemplate class uses the filter string to work out which items a given report applies to. + For example, an acceptance test report template might only apply to stock items with a given IPN, + so the string could be set to: + + filters = "IPN = ACME0001" + + Returns a map of key:value pairs + """ + + # Empty results map + results = {} + + value = str(value).strip() + + if not value or len(value) == 0: + return results + + groups = value.split(',') + + for group in groups: + group = group.strip() + + pair = group.split('=') + + if not len(pair) == 2: + raise ValidationError( + "Invalid group: {g}".format(g=group) + ) + + k, v = pair + + k = k.strip() + v = v.strip() + + if not k or not v: + raise ValidationError( + "Invalid group: {g}".format(g=group) + ) + + results[k] = v + + return results + + class WeasyprintReportMixin(WeasyTemplateResponseMixin): """ Class for rendering a HTML template to a PDF. @@ -101,13 +157,6 @@ class ReportTemplateBase(models.Model): description = models.CharField(max_length=250, help_text=_("Report template description")) - filters = models.CharField( - blank=True, - max_length=250, - help_text=_("Query filters (comma-separated list of key=value pairs)"), - validators=[validate_filter_string] - ) - class Meta: abstract = True @@ -121,7 +170,42 @@ class ReportTemplate(ReportTemplateBase): pass -class TestReport(ReportTemplateBase): +class PartFilterMixin(models.Model): + """ + A model mixin used for matching a report type against a Part object. + Used to assign a report to a given part using custom filters. + """ + + class Meta: + abstract = True + + def matches_part(self, part): + """ + Test if this report matches a given part. + """ + + filters = self.get_part_filters() + + parts = Part.objects.filter(**filters) + + parts = parts.filter(pk=part.pk) + + return parts.exists() + + + def get_part_filters(self): + """ Return a map of filters to be used for Part filtering """ + return validateFilterString(self.part_filters) + + part_filters = models.CharField( + blank=True, + max_length=250, + help_text=_("Part query filters (comma-separated list of key=value pairs)"), + validators=[validateFilterString] + ) + + +class TestReport(ReportTemplateBase, PartFilterMixin): """ Render a TestReport against a StockItem object. """ From 0ec880290b3858b26014fcb71f5c71d7b85face5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 21:29:58 +1000 Subject: [PATCH 14/19] Functionality for retrieving test templates associated with a given part --- InvenTree/part/models.py | 15 +++++++++++++++ InvenTree/report/models.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4ca77739ac..58eeaef82a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -41,6 +41,7 @@ from InvenTree.helpers import decimal2string, normalize from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus +from report import models as ReportModels from build import models as BuildModels from order import models as OrderModels from company.models import SupplierPart @@ -358,6 +359,20 @@ class Part(MPTTModel): self.category = category self.save() + def get_test_report_templates(self): + """ + Return all the TestReport template objects which map to this Part. + """ + + templates = [] + + for report in ReportModels.TestReport.objects.all(): + if report.matches_part(self): + templates.append(report) + + return templates + + def get_absolute_url(self): """ Return the web URL for viewing this part """ return reverse('part-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index ffd77a09b1..1c363059e7 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -13,7 +13,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from part.models import Part +from part import models as PartModels from django_tex.shortcuts import render_to_pdf from django_weasyprint import WeasyTemplateResponseMixin @@ -186,7 +186,7 @@ class PartFilterMixin(models.Model): filters = self.get_part_filters() - parts = Part.objects.filter(**filters) + parts = PartModels.Part.objects.filter(**filters) parts = parts.filter(pk=part.pk) From 1ad7e699a9fa5c4467ccbe2fcd028459845da365 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 21:31:21 +1000 Subject: [PATCH 15/19] PEP --- InvenTree/part/models.py | 1 - InvenTree/report/admin.py | 1 + InvenTree/report/models.py | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 58eeaef82a..31a35d8cd3 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -372,7 +372,6 @@ class Part(MPTTModel): return templates - def get_absolute_url(self): """ Return the web URL for viewing this part """ return reverse('part-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index 15d44931df..4183e8ee83 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -6,6 +6,7 @@ from django.contrib import admin from .models import ReportTemplate, ReportAsset from .models import TestReport + class ReportTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'template') diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 1c363059e7..60580b900d 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -192,7 +192,6 @@ class PartFilterMixin(models.Model): return parts.exists() - def get_part_filters(self): """ Return a map of filters to be used for Part filtering """ return validateFilterString(self.part_filters) From 71681bfda1c3ab8e2c3549c8c26f4197c88eff87 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 22 May 2020 21:38:05 +1000 Subject: [PATCH 16/19] Add a button if a stock item test report is available --- InvenTree/part/models.py | 5 +++++ InvenTree/report/models.py | 1 + InvenTree/stock/templates/stock/item_base.html | 7 ++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 31a35d8cd3..735a6de574 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -372,6 +372,11 @@ class Part(MPTTModel): return templates + def has_test_report_templates(self): + """ Return True if this part has a TestReport defined """ + + return len(self.get_test_report_templates()) > 0 + def get_absolute_url(self): """ Return the web URL for viewing this part """ return reverse('part-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 60580b900d..9cf7778641 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -212,6 +212,7 @@ class TestReport(ReportTemplateBase, PartFilterMixin): def getSubdir(self): return 'test' + # Requires a stock_item object to be given to it before rendering stock_item = None def get_context_data(self, request): diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 782e51a2b8..b01442f3b5 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -93,8 +93,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} + {% if item.part.has_test_report_templates %} + + {% endif %} {% if item.can_delete %}