mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1270 from SchrodingersGat/report-templates
Adds "report snippet" class allowing re-usable report snippets
This commit is contained in:
commit
4d91a34136
@ -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'
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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'),
|
||||
|
@ -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'
|
||||
|
@ -44,7 +44,6 @@
|
||||
<span id='part-star-icon' class='fas fa-star {% if starred %}icon-yellow{% endif %}'/>
|
||||
</button>
|
||||
|
||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group'>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
23
InvenTree/report/migrations/0006_reportsnippet.py
Normal file
23
InvenTree/report/migrations/0006_reportsnippet.py
Normal file
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
20
InvenTree/report/migrations/0007_auto_20210204_1617.py
Normal file
20
InvenTree/report/migrations/0007_auto_20210204_1617.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
18
InvenTree/report/migrations/0008_auto_20210204_2100.py
Normal file
18
InvenTree/report/migrations/0008_auto_20210204_2100.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
@ -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)
|
||||
|
103
InvenTree/report/templates/report/inventree_report_base.html
Normal file
103
InvenTree/report/templates/report/inventree_report_base.html
Normal file
@ -0,0 +1,103 @@
|
||||
{% load report %}
|
||||
|
||||
<head>
|
||||
<style>
|
||||
@page {
|
||||
{% block page_size %}
|
||||
size: A4;
|
||||
{% endblock %}
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
{% endblock %}
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 75%;
|
||||
|
||||
@top-left {
|
||||
{% block top_left %}
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
@top-center {
|
||||
{% block top_center %}
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
@top-right {
|
||||
{% block top_right %}
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
@bottom-left {
|
||||
{% block bottom_left %}
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
@bottom-center {
|
||||
{% block bottom_center %}
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
@bottom-right {
|
||||
{% block bottom_right %}
|
||||
content: "Page " counter(page) " of " counter(pages);
|
||||
{% endblock %}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
{% block header_style %}
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
margin-top: -2.5cm;
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
.content {
|
||||
{% block content_style %}
|
||||
width: 100%;
|
||||
page-break-inside: auto;
|
||||
position: relative;
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
.footer {
|
||||
{% block footer_style %}
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
margin-bottom: -20mm;
|
||||
{% endblock %}
|
||||
}
|
||||
|
||||
{% block style %}
|
||||
/* User defined styles go here */
|
||||
{% endblock %}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class='header'>
|
||||
{% block header_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class='content'>
|
||||
{% block page_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class='footer'>
|
||||
{% block footer_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
112
InvenTree/report/templates/report/inventree_test_report.html
Normal file
112
InvenTree/report/templates/report/inventree_test_report.html
Normal file
@ -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 %}
|
||||
|
||||
<div class='container'>
|
||||
<div class='text-left'>
|
||||
<h2>
|
||||
{{ part.full_name }}
|
||||
</h2>
|
||||
<p>{{ part.description }}</p>
|
||||
<p><i>{{ stock_item.location }}</i></p>
|
||||
<p><i>Stock Item ID: {{ stock_item.pk }}</i></p>
|
||||
</div>
|
||||
<div class='img-right'>
|
||||
<img src="{% part_image part %}">
|
||||
<hr>
|
||||
<h4>
|
||||
{% if stock_item.is_serialized %}
|
||||
{% trans "Serial Number" %}: {{ stock_item.serial }}
|
||||
{% else %}
|
||||
{% trans "Quantity" %}: {% decimal stock_item.quantity %}
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{% trans "Test Results" %}</h3>
|
||||
|
||||
<table class='table test-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Test" %}</th>
|
||||
<th>{% trans "Result" %}</th>
|
||||
<th>{% trans "Value" %}</th>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan='5'><hr></td>
|
||||
</tr>
|
||||
{% for test in result_list %}
|
||||
<tr class='test-row'>
|
||||
<td>{{ test.test }}</td>
|
||||
{% if test.result %}
|
||||
<td class='test-pass'>{% trans "Pass" %}</td>
|
||||
{% else %}
|
||||
<td class='test-fail'>{% trans "Fail" %}</td>
|
||||
{% endif %}
|
||||
<td>{{ test.value }}</td>
|
||||
<td>{{ test.user.username }}</td>
|
||||
<td>{{ test.date.date.isoformat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
52
InvenTree/report/templatetags/report.py
Normal file
52
InvenTree/report/templatetags/report.py
Normal file
@ -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}"
|
@ -120,7 +120,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
</div>
|
||||
|
||||
<div class='btn-group action-buttons' role='group'>
|
||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group'>
|
||||
@ -139,19 +139,15 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Document / label menu -->
|
||||
{% if item.has_labels or item.has_test_reports %}
|
||||
<div class='btn-group'>
|
||||
<button id='document-options' title='{% trans "Printing actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-print'></span> <span class='caret'></span></button>
|
||||
<ul class='dropdown-menu' role='menu'>
|
||||
{% if item.has_labels %}
|
||||
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
|
||||
{% endif %}
|
||||
{% if item.has_test_reports %}
|
||||
{% if test_report_enabled %}
|
||||
<li><a href='#' id='stock-test-report'><span class='fas fa-file-pdf'></span> {% trans "Test Report" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Stock adjustment menu -->
|
||||
<!-- Check permissions and owner -->
|
||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
||||
|
@ -36,8 +36,7 @@
|
||||
<span class='fas fa-plus-circle icon-green'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
{% if location %}
|
||||
|
22
InvenTree/templates/InvenTree/settings/report.html
Normal file
22
InvenTree/templates/InvenTree/settings/report.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "InvenTree/settings/settings.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include "InvenTree/settings/tabs.html" with tab='report' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{% trans "Report Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% include "InvenTree/settings/header.html" %}
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
@ -15,6 +15,9 @@
|
||||
<li {% if tab == 'global' %} class='active' {% endif %}>
|
||||
<a href='{% url "settings-global" %}'><span class='fas fa-globe'></span> {% trans "Global" %}</a>
|
||||
</li>
|
||||
<li {% if tab == 'report' %} class='active' {% endif %}>
|
||||
<a href='{% url "settings-report" %}'><span class='fas fa-file-pdf'></span> {% trans "Report" %}</a>
|
||||
</li>
|
||||
<li{% ifequal tab 'category' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'settings-category' %}"><span class='fa fa-sitemap'></span> {% trans "Categories" %}</a>
|
||||
</li>
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -10,6 +10,15 @@ function selectTestReport(reports, items, options={}) {
|
||||
* (via AJAX) from the server.
|
||||
*/
|
||||
|
||||
// If there is only a single report available, just print!
|
||||
if (reports.length == 1) {
|
||||
if (options.success) {
|
||||
options.success(reports[0].pk);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var modal = options.modal || '#modal-form';
|
||||
|
||||
var report_list = makeOptionsList(
|
||||
|
@ -38,7 +38,9 @@
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
<li><a href='#' id='multi-item-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
|
||||
{% if test_report_enabled %}
|
||||
<li><a href='#' id='multi-item-print-test-report' title='{% trans "Print test reports" %}'><span class='fas fa-file-pdf'></span> {% trans "Print test reports" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if roles.stock.change or roles.stock.delete %}
|
||||
|
@ -118,6 +118,7 @@ class RuleSet(models.Model):
|
||||
'label_stockitemlabel',
|
||||
'label_stocklocationlabel',
|
||||
'report_reportasset',
|
||||
'report_reportsnippet',
|
||||
'report_testreport',
|
||||
'part_partstar',
|
||||
'users_owner',
|
||||
|
@ -23,7 +23,6 @@ coverage==5.3 # Unit test coverage
|
||||
coveralls==2.1.2 # Coveralls linking (for Travis)
|
||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||
django-stdimage==5.1.1 # Advanced ImageField management
|
||||
django-tex==1.1.7 # LaTeX PDF export
|
||||
django-weasyprint==1.0.1 # HTML PDF export
|
||||
django-debug-toolbar==2.2 # Debug / profiling toolbar
|
||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||
|
Loading…
Reference in New Issue
Block a user