diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 06908e76bf..d48743f166 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -208,7 +208,6 @@ INSTALLED_APPS = [
'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing
'markdownify', # Markdown template rendering
- 'django_tex', # LaTeX output
'django_admin_shell', # Python shell for the admin interface
'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates
@@ -265,14 +264,6 @@ TEMPLATES = [
],
},
},
- # Backend for LaTeX report rendering
- {
- 'NAME': 'tex',
- 'BACKEND': 'django_tex.engine.TeXEngine',
- 'DIRS': [
- os.path.join(MEDIA_ROOT, 'report'),
- ]
- },
]
REST_FRAMEWORK = {
@@ -485,22 +476,6 @@ DATE_INPUT_FORMATS = [
"%Y-%m-%d",
]
-# LaTeX rendering settings (django-tex)
-LATEX_SETTINGS = CONFIG.get('latex', {})
-
-# Is LaTeX rendering enabled? (Off by default)
-LATEX_ENABLED = LATEX_SETTINGS.get('enabled', False)
-
-# 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"),
-]
-
# crispy forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap3'
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 2c56b8eeb1..1df5e83d5d 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -79,6 +79,7 @@ settings_urls = [
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
+ url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'),
url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'),
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 053cc12864..a81d06ed25 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -174,6 +174,13 @@ class InvenTreeSetting(models.Model):
'validator': bool,
},
+ 'REPORT_ENABLE_TEST_REPORT': {
+ 'name': _('Test Reports'),
+ 'description': _('Enable generation of test reports'),
+ 'default': True,
+ 'validator': bool,
+ },
+
'STOCK_ENABLE_EXPIRY': {
'name': _('Stock Expiry'),
'description': _('Enable stock expiry functionality'),
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index c655ffdc5c..18e3197cca 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -107,19 +107,6 @@ static_root: '../inventree_static'
# If unspecified, the local user's temp directory will be used
#backup_dir: '/home/inventree/backup/'
-# LaTeX report rendering
-# InvenTree uses the django-tex plugin to enable LaTeX report rendering
-# Ref: https://pypi.org/project/django-tex/
-# Note: Ensure that a working LaTeX toolchain is installed and working *before* starting the server
-latex:
- # Select the LaTeX interpreter to use for PDF rendering
- # Note: The intepreter needs to be installed on the system!
- # e.g. to install pdflatex: apt-get texlive-latex-base
- enabled: False
- interpreter: pdflatex
- # Extra options to pass through to the LaTeX interpreter
- options: ''
-
# Permit custom authentication backends
#authentication_backends:
# - 'django.contrib.auth.backends.ModelBackend'
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 84eac7e7f4..f35d616a91 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -44,7 +44,6 @@
- {% settings_value 'BARCODE_ENABLE' as barcodes %}
{% if barcodes %}
diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py
index 7d6403f5d9..390964b590 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 TestReport, ReportAsset
+from .models import ReportSnippet, TestReport, ReportAsset
class ReportTemplateAdmin(admin.ModelAdmin):
@@ -11,10 +11,16 @@ class ReportTemplateAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'template', 'filters', 'enabled')
+class ReportSnippetAdmin(admin.ModelAdmin):
+
+ list_display = ('id', 'snippet', 'description')
+
+
class ReportAssetAdmin(admin.ModelAdmin):
- list_display = ('asset', 'description')
+ list_display = ('id', 'asset', 'description')
+admin.site.register(ReportSnippet, ReportSnippetAdmin)
admin.site.register(TestReport, ReportTemplateAdmin)
admin.site.register(ReportAsset, ReportAssetAdmin)
diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py
index 138ba20404..bb1c5f0cb7 100644
--- a/InvenTree/report/apps.py
+++ b/InvenTree/report/apps.py
@@ -1,5 +1,92 @@
+import os
+import shutil
+import logging
+
from django.apps import AppConfig
+from django.conf import settings
+
+
+logger = logging.getLogger(__name__)
class ReportConfig(AppConfig):
name = 'report'
+
+ def ready(self):
+ """
+ This function is called whenever the report app is loaded
+ """
+
+ self.create_default_test_reports()
+
+ def create_default_test_reports(self):
+ """
+ Create database entries for the default TestReport templates,
+ if they do not already exist
+ """
+
+ try:
+ from .models import TestReport
+ except:
+ # Database is not ready yet
+ return
+
+ src_dir = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ 'templates',
+ 'report',
+ )
+
+ dst_dir = os.path.join(
+ settings.MEDIA_ROOT,
+ 'report',
+ 'inventree', # Stored in secret directory!
+ 'test',
+ )
+
+ if not os.path.exists(dst_dir):
+ logger.info(f"Creating missing directory: '{dst_dir}'")
+ os.makedirs(dst_dir, exist_ok=True)
+
+ # List of test reports to copy across
+ reports = [
+ {
+ 'file': 'inventree_test_report.html',
+ 'name': 'InvenTree Test Report',
+ 'description': 'Stock item test report',
+ },
+ ]
+
+ for report in reports:
+
+ # Create destination file name
+ filename = os.path.join(
+ 'report',
+ 'inventree',
+ 'test',
+ report['file']
+ )
+
+ src_file = os.path.join(src_dir, report['file'])
+ dst_file = os.path.join(settings.MEDIA_ROOT, filename)
+
+ if not os.path.exists(dst_file):
+ logger.info(f"Copying test report template '{dst_file}'")
+ shutil.copyfile(src_file, dst_file)
+
+ try:
+ # Check if a report matching the template already exists
+ if TestReport.objects.filter(template=filename).exists():
+ continue
+
+ logger.info(f"Creating new TestReport for '{report['name']}'")
+
+ TestReport.objects.create(
+ name=report['name'],
+ description=report['description'],
+ template=filename,
+ filters='',
+ enabled=True
+ )
+ except:
+ pass
diff --git a/InvenTree/report/migrations/0006_reportsnippet.py b/InvenTree/report/migrations/0006_reportsnippet.py
new file mode 100644
index 0000000000..6875ebb530
--- /dev/null
+++ b/InvenTree/report/migrations/0006_reportsnippet.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2021-02-04 04:37
+
+import django.core.validators
+from django.db import migrations, models
+import report.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('report', '0005_auto_20210119_0815'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ReportSnippet',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('snippet', models.FileField(help_text='Report snippet file', upload_to=report.models.rename_snippet, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html', 'htm'])])),
+ ('description', models.CharField(help_text='Snippet file description', max_length=250)),
+ ],
+ ),
+ ]
diff --git a/InvenTree/report/migrations/0007_auto_20210204_1617.py b/InvenTree/report/migrations/0007_auto_20210204_1617.py
new file mode 100644
index 0000000000..b110f63365
--- /dev/null
+++ b/InvenTree/report/migrations/0007_auto_20210204_1617.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.0.7 on 2021-02-04 05:17
+
+import django.core.validators
+from django.db import migrations, models
+import report.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('report', '0006_reportsnippet'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='testreport',
+ 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'])], verbose_name='Template'),
+ ),
+ ]
diff --git a/InvenTree/report/migrations/0008_auto_20210204_2100.py b/InvenTree/report/migrations/0008_auto_20210204_2100.py
new file mode 100644
index 0000000000..35136676d4
--- /dev/null
+++ b/InvenTree/report/migrations/0008_auto_20210204_2100.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.7 on 2021-02-04 10:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('report', '0007_auto_20210204_1617'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='testreport',
+ name='name',
+ field=models.CharField(help_text='Template name', max_length=100, verbose_name='Name'),
+ ),
+ ]
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index 224f2800d8..422216f9a9 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -14,7 +14,6 @@ from django.db import models
from django.conf import settings
from django.core.validators import FileExtensionValidator
-from django.core.exceptions import ValidationError
import stock.models
@@ -29,32 +28,10 @@ except OSError as err:
print("You may require some further system packages to be installed.")
sys.exit(1)
-# Conditional import if LaTeX templating is enabled
-if settings.LATEX_ENABLED:
- try:
- from django_tex.shortcuts import render_to_pdf
- from django_tex.core import render_template_with_context
- from django_tex.exceptions import TexError
- except OSError as err:
- print("OSError: {e}".format(e=err))
- print("You may not have a working LaTeX toolchain installed?")
- sys.exit(1)
-
-from django.http import HttpResponse
-
-
-class TexResponse(HttpResponse):
- def __init__(self, content, filename=None):
- super().__init__(content_type="application/txt")
- self["Content-Disposition"] = 'filename="{}"'.format(filename)
- self.write(content)
-
def rename_template(instance, filename):
- filename = os.path.basename(filename)
-
- return os.path.join('report', 'report_template', instance.getSubdir(), filename)
+ return instance.rename_file(filename)
def validate_stock_item_report_filters(filters):
@@ -77,17 +54,27 @@ class WeasyprintReportMixin(WeasyTemplateResponseMixin):
self.pdf_filename = kwargs.get('filename', 'report.pdf')
-class ReportTemplateBase(models.Model):
+class ReportBase(models.Model):
"""
- Reporting template model.
+ Base class for uploading html templates
"""
+ class Meta:
+ abstract = True
+
def __str__(self):
return "{n} - {d}".format(n=self.name, d=self.description)
def getSubdir(self):
return ''
+ def rename_file(self, filename):
+ # Function for renaming uploaded file
+
+ filename = os.path.basename(filename)
+
+ return os.path.join('report', 'report_template', self.getSubdir(), filename)
+
@property
def extension(self):
return os.path.splitext(self.template.name)[1].lower()
@@ -96,15 +83,45 @@ class ReportTemplateBase(models.Model):
def template_name(self):
"""
Returns the file system path to the template file.
- Required for passing the file to an external process (e.g. LaTeX)
+ Required for passing the file to an external process
"""
- template = os.path.join('report_template', self.getSubdir(), os.path.basename(self.template.name))
+ template = self.template.name
template = template.replace('/', os.path.sep)
template = template.replace('\\', os.path.sep)
+ template = os.path.join(settings.MEDIA_ROOT, template)
+
return template
+ name = models.CharField(
+ blank=False, max_length=100,
+ verbose_name=_('Name'),
+ help_text=_('Template name'),
+ )
+
+ template = models.FileField(
+ upload_to=rename_template,
+ verbose_name=_('Template'),
+ help_text=_("Report template file"),
+ validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])],
+ )
+
+ description = models.CharField(
+ max_length=250,
+ verbose_name=_('Description'),
+ help_text=_("Report template description")
+ )
+
+
+class ReportTemplateBase(ReportBase):
+ """
+ Reporting template model.
+
+ Able to be passed context data
+
+ """
+
def get_context_data(self, request):
"""
Supply context data to the template for rendering
@@ -116,56 +133,34 @@ class ReportTemplateBase(models.Model):
"""
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
+ Uses django-weasyprint plugin to render HTML template against Weasyprint
"""
- filename = kwargs.get('filename', 'report.pdf')
+ # TODO: Support custom filename generation!
+ # filename = kwargs.get('filename', 'report.pdf')
context = self.get_context_data(request)
+ context['media'] = settings.MEDIA_ROOT
+
+ context['report_name'] = self.name
+ context['report_description'] = self.description
context['request'] = request
context['user'] = request.user
+ context['date'] = datetime.datetime.now().date()
context['datetime'] = datetime.datetime.now()
- if self.extension == '.tex':
- # Render LaTeX template to PDF
- if settings.LATEX_ENABLED:
- # Attempt to render to LaTeX template
- # If there is a rendering error, return the (partially rendered) template,
- # so at least we can debug what is going on
- try:
- rendered = render_template_with_context(self.template_name, context)
- return render_to_pdf(request, self.template_name, context, filename=filename)
- except TexError:
- return TexResponse(rendered, filename="error.tex")
- else:
- raise ValidationError("Enable LaTeX support in config.yaml")
- 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)
+ # Render HTML template to PDF
+ wp = WeasyprintReportMixin(
+ request,
+ self.template_name,
+ base_url=request.build_absolute_uri("/"),
+ presentational_hints=True,
+ **kwargs)
- name = models.CharField(
- blank=False, max_length=100,
- verbose_name=_('Name'),
- help_text=_('Template name'),
- unique=True,
- )
-
- template = models.FileField(
- upload_to=rename_template,
- verbose_name=_('Template'),
- help_text=_("Report template file"),
- validators=[FileExtensionValidator(allowed_extensions=['html', 'htm', 'tex'])],
- )
-
- description = models.CharField(
- max_length=250,
- verbose_name=_('Description'),
- help_text=_("Report template description")
- )
+ return wp.render_to_response(
+ context,
+ **kwargs)
enabled = models.BooleanField(
default=True,
@@ -221,6 +216,30 @@ class TestReport(ReportTemplateBase):
}
+def rename_snippet(instance, filename):
+
+ filename = os.path.basename(filename)
+
+ return os.path.join('report', 'snippets', filename)
+
+
+class ReportSnippet(models.Model):
+ """
+ Report template 'snippet' which can be used to make templates
+ that can then be included in other reports.
+
+ Useful for 'common' template actions, sub-templates, etc
+ """
+
+ snippet = models.FileField(
+ upload_to=rename_snippet,
+ help_text=_('Report snippet file'),
+ validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])],
+ )
+
+ description = models.CharField(max_length=250, help_text=_("Snippet file description"))
+
+
def rename_asset(instance, filename):
filename = os.path.basename(filename)
diff --git a/InvenTree/report/templates/report/inventree_report_base.html b/InvenTree/report/templates/report/inventree_report_base.html
new file mode 100644
index 0000000000..084b100cab
--- /dev/null
+++ b/InvenTree/report/templates/report/inventree_report_base.html
@@ -0,0 +1,103 @@
+{% load report %}
+
+
+
+
+
+
+
+
+
+
+ {% block page_content %}
+ {% endblock %}
+
+
+
+
+
\ No newline at end of file
diff --git a/InvenTree/report/templates/report/inventree_test_report.html b/InvenTree/report/templates/report/inventree_test_report.html
new file mode 100644
index 0000000000..e076a7919e
--- /dev/null
+++ b/InvenTree/report/templates/report/inventree_test_report.html
@@ -0,0 +1,112 @@
+{% extends "report/inventree_report_base.html" %}
+
+{% load i18n %}
+{% load report %}
+{% load inventree_extras %}
+
+{% block style %}
+.test-table {
+ width: 100%;
+}
+
+{% block bottom_left %}
+content: "{{ date.isoformat }}";
+{% endblock %}
+
+{% block bottom_center %}
+content: "InvenTree v{% inventree_version %}";
+{% endblock %}
+
+{% block top_center %}
+content: "{% trans 'Stock Item Test Report' %}";
+{% endblock %}
+
+.test-row {
+ padding: 3px;
+}
+
+.test-pass {
+ color: #5f5;
+}
+
+.test-fail {
+ color: #F55;
+}
+
+.container {
+ padding: 5px;
+ border: 1px solid;
+}
+
+.text-left {
+ display: inline-block;
+ width: 50%;
+}
+
+.img-right {
+ display: inline;
+ align-content: right;
+ align-items: right;
+ width: 50%;
+}
+
+{% endblock %}
+
+{% block page_content %}
+
+
+
+
+ {{ part.full_name }}
+
+
{{ part.description }}
+
{{ stock_item.location }}
+
Stock Item ID: {{ stock_item.pk }}
+
+
+

+
+
+ {% if stock_item.is_serialized %}
+ {% trans "Serial Number" %}: {{ stock_item.serial }}
+ {% else %}
+ {% trans "Quantity" %}: {% decimal stock_item.quantity %}
+ {% endif %}
+
+
+
+
+
{% trans "Test Results" %}
+
+
+
+
+ {% trans "Test" %} |
+ {% trans "Result" %} |
+ {% trans "Value" %} |
+ {% trans "User" %} |
+ {% trans "Date" %} |
+
+
+
+
+
|
+
+ {% for test in result_list %}
+
+ {{ test.test }} |
+ {% if test.result %}
+ {% trans "Pass" %} |
+ {% else %}
+ {% trans "Fail" %} |
+ {% endif %}
+ {{ test.value }} |
+ {{ test.user.username }} |
+ {{ test.date.date.isoformat }} |
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py
new file mode 100644
index 0000000000..0383aad562
--- /dev/null
+++ b/InvenTree/report/templatetags/report.py
@@ -0,0 +1,52 @@
+"""
+Custom template tags for report generation
+"""
+
+import os
+
+from django import template
+from django.conf import settings
+
+from part.models import Part
+from stock.models import StockItem
+
+register = template.Library()
+
+
+@register.simple_tag()
+def asset(filename):
+ """
+ Return fully-qualified path for an upload report asset file.
+ """
+
+ path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename)
+ path = os.path.abspath(path)
+
+ return f"file://{path}"
+
+
+@register.simple_tag()
+def part_image(part):
+ """
+ Return a fully-qualified path for a part image
+ """
+
+ if type(part) is Part:
+ img = part.image.name
+
+ elif type(part) is StockItem:
+ img = part.part.image.name
+
+ else:
+ img = ''
+
+ path = os.path.join(settings.MEDIA_ROOT, img)
+ path = os.path.abspath(path)
+
+ if not os.path.exists(path) or not os.path.isfile(path):
+ # Image does not exist
+ # Return the 'blank' image
+ path = os.path.join(settings.STATIC_ROOT, 'img', 'blank_image.png')
+ path = os.path.abspath(path)
+
+ return f"file://{path}"
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index d284a74e98..93d60fb3d1 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -120,7 +120,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}