lims_report_html: add sections to results report

This commit is contained in:
Adrián Bernardi 2020-07-15 20:40:19 -03:00
parent e7ddbaa2ab
commit d52eb42ab7
13 changed files with 378 additions and 43 deletions

View file

@ -15,10 +15,12 @@ def register():
action.ActionReport,
html_template.ReportTemplate,
html_template.ReportTemplateTranslation,
html_template.ReportTemplateSection,
html_template.ReportTemplateTrendChart,
sample.Sample,
sample.CreateSampleStart,
results_report.ResultsReportVersionDetail,
results_report.ResultsReportVersionDetailSection,
results_report.ResultsReportVersionDetailTrendChart,
results_report.ResultsReportVersionDetailSample,
notebook.Notebook,

View file

@ -1,15 +1,20 @@
# This file is part of lims_report_html module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from io import BytesIO
from PyPDF2 import PdfFileMerger
from PyPDF2.utils import PdfReadError
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.cache import Cache
from trytond.exceptions import UserError
from trytond.i18n import gettext
__all__ = ['ReportTemplate', 'ReportTemplateTranslation',
'ReportTemplateTrendChart']
'ReportTemplateSection', 'ReportTemplateTrendChart']
class ReportTemplate(ModelSQL, ModelView):
@ -31,6 +36,16 @@ class ReportTemplate(ModelSQL, ModelView):
'template', 'Translations')
_translation_cache = Cache('lims.result_report.template.translation',
size_limit=10240, context=False)
sections = fields.One2Many('lims.result_report.template.section',
'template', 'Sections')
previous_sections = fields.Function(fields.One2Many(
'lims.result_report.template.section', 'template',
'Previous Sections', domain=[('position', '=', 'previous')]),
'get_previous_sections', setter='set_previous_sections')
following_sections = fields.Function(fields.One2Many(
'lims.result_report.template.section', 'template',
'Following Sections', domain=[('position', '=', 'following')]),
'get_following_sections', setter='set_following_sections')
trend_charts = fields.One2Many('lims.result_report.template.trend.chart',
'template', 'Trend Charts')
charts_x_row = fields.Selection([
@ -51,7 +66,14 @@ class ReportTemplate(ModelSQL, ModelView):
return super(ReportTemplate, cls).view_attributes() + [
('//page[@id="header_footer"]', 'states', {
'invisible': Eval('type') != 'base',
})]
}),
('//page[@name="sections"]', 'states', {
'invisible': Eval('type') != 'base',
}),
('//page[@name="trend_charts"]', 'states', {
'invisible': Eval('type') != 'base',
}),
]
@classmethod
def gettext(cls, *args, **variables):
@ -79,6 +101,24 @@ class ReportTemplate(ModelSQL, ModelView):
cls._translation_cache.set(key, text)
return text if not variables else text % variables
def get_previous_sections(self, name):
return [s.id for s in self.sections if s.position == 'previous']
@classmethod
def set_previous_sections(cls, sections, name, value):
if not value:
return
cls.write(sections, {'sections': value})
def get_following_sections(self, name):
return [s.id for s in self.sections if s.position == 'following']
@classmethod
def set_following_sections(cls, sections, name, value):
if not value:
return
cls.write(sections, {'sections': value})
class ReportTemplateTranslation(ModelSQL, ModelView):
'Results Report Template Translation'
@ -126,6 +166,40 @@ class ReportTemplateTranslation(ModelSQL, ModelView):
return super(ReportTemplateTranslation, cls).delete(translations)
class ReportTemplateSection(ModelSQL, ModelView):
'Results Report Template Section'
__name__ = 'lims.result_report.template.section'
_order_name = 'order'
template = fields.Many2One('lims.result_report.template', 'Template',
ondelete='CASCADE', select=True, required=True)
name = fields.Char('Name', required=True)
data = fields.Binary('File', filename='name', required=True,
file_id='data_id', store_prefix='results_report_template_section')
data_id = fields.Char('File ID', readonly=True)
position = fields.Selection([
('previous', 'Previous'),
('following', 'Following'),
], 'Position', required=True)
order = fields.Integer('Order')
@classmethod
def __setup__(cls):
super(ReportTemplateSection, cls).__setup__()
cls._order.insert(0, ('order', 'ASC'))
@classmethod
def validate(cls, sections):
super(ReportTemplateSection, cls).validate(sections)
merger = PdfFileMerger(strict=False)
for section in sections:
filedata = BytesIO(section.data)
try:
merger.append(filedata)
except PdfReadError:
raise UserError(gettext('lims_report_html.msg_section_pdf'))
class ReportTemplateTrendChart(ModelSQL, ModelView):
'Results Report Template Trend Chart'
__name__ = 'lims.result_report.template.trend.chart'

View file

@ -47,6 +47,19 @@
<field name="name">template_translation_list</field>
</record>
<!-- Results Report Template Section -->
<record model="ir.ui.view" id="template_section_view_form">
<field name="model">lims.result_report.template.section</field>
<field name="type">form</field>
<field name="name">template_section_form</field>
</record>
<record model="ir.ui.view" id="template_section_view_list">
<field name="model">lims.result_report.template.section</field>
<field name="type">tree</field>
<field name="name">template_section_list</field>
</record>
<!-- Results Report Template Trend Chart -->
<record model="ir.ui.view" id="template_trend_chart_view_form">

View file

@ -26,6 +26,10 @@ msgctxt "field:lims.result_report.template,content:"
msgid "Content"
msgstr "Contenido"
msgctxt "field:lims.result_report.template,following_sections:"
msgid "Following Sections"
msgstr "Secciones siguientes"
msgctxt "field:lims.result_report.template,footer:"
msgid "Footer"
msgstr "Pie de página"
@ -38,6 +42,14 @@ msgctxt "field:lims.result_report.template,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:lims.result_report.template,previous_sections:"
msgid "Previous Sections"
msgstr "Secciones anteriores"
msgctxt "field:lims.result_report.template,sections:"
msgid "Sections"
msgstr "Secciones"
msgctxt "field:lims.result_report.template,translations:"
msgid "Translations"
msgstr "Traducciones"
@ -50,6 +62,30 @@ msgctxt "field:lims.result_report.template,type:"
msgid "Type"
msgstr "Tipo"
msgctxt "field:lims.result_report.template.section,data:"
msgid "File"
msgstr "Archivo"
msgctxt "field:lims.result_report.template.section,data_id:"
msgid "File ID"
msgstr "ID Archivo"
msgctxt "field:lims.result_report.template.section,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:lims.result_report.template.section,order:"
msgid "Order"
msgstr "Orden"
msgctxt "field:lims.result_report.template.section,position:"
msgid "Position"
msgstr "Posición"
msgctxt "field:lims.result_report.template.section,template:"
msgid "Template"
msgstr "Plantilla"
msgctxt "field:lims.result_report.template.translation,lang:"
msgid "Language"
msgstr "Idioma"
@ -82,6 +118,18 @@ msgctxt "field:lims.results_report.version.detail,charts_x_row:"
msgid "Charts per Row"
msgstr "Gráficos por fila"
msgctxt "field:lims.results_report.version.detail,following_sections:"
msgid "Following Sections"
msgstr "Secciones siguientes"
msgctxt "field:lims.results_report.version.detail,previous_sections:"
msgid "Previous Sections"
msgstr "Secciones anteriores"
msgctxt "field:lims.results_report.version.detail,sections:"
msgid "Sections"
msgstr "Secciones"
msgctxt "field:lims.results_report.version.detail,template:"
msgid "Report Template"
msgstr "Plantilla de Informe"
@ -94,6 +142,30 @@ msgctxt "field:lims.results_report.version.detail.sample,trend_charts:"
msgid "Trend Charts"
msgstr "Gráficos de tendencia"
msgctxt "field:lims.results_report.version.detail.section,data:"
msgid "File"
msgstr "Archivo"
msgctxt "field:lims.results_report.version.detail.section,data_id:"
msgid "File ID"
msgstr "ID Archivo"
msgctxt "field:lims.results_report.version.detail.section,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:lims.results_report.version.detail.section,order:"
msgid "Order"
msgstr "Orden"
msgctxt "field:lims.results_report.version.detail.section,position:"
msgid "Position"
msgstr "Posición"
msgctxt "field:lims.results_report.version.detail.section,version_detail:"
msgid "Report Detail"
msgstr "Detalle de informe"
msgctxt "field:lims.results_report.version.detail.trend.chart,chart:"
msgid "Trend Chart"
msgstr "Gráfico de tendencia"
@ -126,6 +198,10 @@ msgctxt "model:ir.message,text:msg_no_template"
msgid "The report has no template"
msgstr "El informe no tiene ninguna plantilla"
msgctxt "model:ir.message,text:msg_section_pdf"
msgid "Section files must be in PDF format"
msgstr "Los archivos de secciones deben ser PDF"
msgctxt "model:ir.ui.menu,name:menu_html_template_list"
msgid "Results Report Templates"
msgstr "Plantillas de Informe"
@ -134,6 +210,10 @@ msgctxt "model:lims.result_report.template,name:"
msgid "Results Report Template"
msgstr "Plantilla de Informe de resultados"
msgctxt "model:lims.result_report.template.section,name:"
msgid "Results Report Template Section"
msgstr "Sección de Plantilla de Informe de resultados"
msgctxt "model:lims.result_report.template.translation,name:"
msgid "Results Report Template Translation"
msgstr "Traducción de Plantilla de Informe de resultados"
@ -142,9 +222,13 @@ msgctxt "model:lims.result_report.template.trend.chart,name:"
msgid "Results Report Template Trend Chart"
msgstr "Gráfico de tendencia de Plantilla de Informe de resultados"
msgctxt "model:lims.results_report.version.detail.section,name:"
msgid "Results Report Version Detail Section"
msgstr "Sección de Detalle de versión de Informe de resultados"
msgctxt "model:lims.results_report.version.detail.trend.chart,name:"
msgid "Results Report Version Detail Trend Chart"
msgstr "Gráfico de tendencia de detalle de versión de informe de resultados"
msgstr "Gráfico de tendencia de Detalle de versión de Informe de resultados"
msgctxt "selection:lims.result_report.template,charts_x_row:"
msgid "1"
@ -166,6 +250,14 @@ msgctxt "selection:lims.result_report.template,type:"
msgid "Header"
msgstr "Encabezado"
msgctxt "selection:lims.result_report.template.section,position:"
msgid "Following"
msgstr "Siguiente"
msgctxt "selection:lims.result_report.template.section,position:"
msgid "Previous"
msgstr "Anterior"
msgctxt "selection:lims.results_report.version.detail,charts_x_row:"
msgid "1"
msgstr "1"
@ -174,6 +266,14 @@ msgctxt "selection:lims.results_report.version.detail,charts_x_row:"
msgid "2"
msgstr "2"
msgctxt "selection:lims.results_report.version.detail.section,position:"
msgid "Following"
msgstr "Siguiente"
msgctxt "selection:lims.results_report.version.detail.section,position:"
msgid "Previous"
msgstr "Anterior"
msgctxt "view:lims.result_report.template:"
msgid "Header and Footer"
msgstr "Encabezado y Pie de página"

View file

@ -4,5 +4,8 @@
<record model="ir.message" id="msg_no_template">
<field name="text">The report has no template</field>
</record>
<record model="ir.message" id="msg_section_pdf">
<field name="text">Section files must be in PDF format</field>
</record>
</data>
</tryton>

View file

@ -6,6 +6,9 @@ from lxml import html as lxml_html
from base64 import b64encode
from babel.support import Translations as BabelTranslations
from jinja2 import contextfilter, Markup
from io import BytesIO
from PyPDF2 import PdfFileMerger
from PyPDF2.utils import PdfReadError
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool, PoolMeta
@ -15,7 +18,7 @@ from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.html_report.generator import PdfGenerator
__all__ = ['ResultsReportVersionDetail',
__all__ = ['ResultsReportVersionDetail', 'ResultsReportVersionDetailSection',
'ResultsReportVersionDetailTrendChart',
'ResultsReportVersionDetailSample', 'ResultReport']
@ -26,6 +29,16 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', '=', 'base')],
states={'readonly': Eval('state') != 'draft'}, depends=['state'])
sections = fields.One2Many('lims.results_report.version.detail.section',
'version_detail', 'Sections')
previous_sections = fields.Function(fields.One2Many(
'lims.results_report.version.detail.section', 'version_detail',
'Previous Sections', domain=[('position', '=', 'previous')]),
'get_previous_sections', setter='set_previous_sections')
following_sections = fields.Function(fields.One2Many(
'lims.results_report.version.detail.section', 'version_detail',
'Following Sections', domain=[('position', '=', 'following')]),
'get_following_sections', setter='set_following_sections')
trend_charts = fields.One2Many(
'lims.results_report.version.detail.trend.chart',
'version_detail', 'Trend Charts')
@ -46,7 +59,8 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
def default_charts_x_row():
return '1'
@fields.depends('template', '_parent_template.trend_charts')
@fields.depends('template', '_parent_template.trend_charts',
'_parent_template.sections')
def on_change_template(self):
if self.template and self.template.trend_charts:
self.trend_charts = [{
@ -54,6 +68,32 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
'order': c.order,
} for c in self.template.trend_charts]
self.charts_x_row = self.template.charts_x_row
if self.template and self.template.sections:
self.sections = [{
'name': s.name,
'data': s.data,
'data_id': s.data_id,
'position': s.position,
'order': s.order,
} for s in self.template.sections]
def get_previous_sections(self, name):
return [s.id for s in self.sections if s.position == 'previous']
@classmethod
def set_previous_sections(cls, sections, name, value):
if not value:
return
cls.write(sections, {'sections': value})
def get_following_sections(self, name):
return [s.id for s in self.sections if s.position == 'following']
@classmethod
def set_following_sections(cls, sections, name, value):
if not value:
return
cls.write(sections, {'sections': value})
@classmethod
def _get_fields_from_samples(cls, samples):
@ -72,6 +112,14 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
} for c in result_template.trend_charts])]
detail_default['charts_x_row'] = (
result_template.charts_x_row)
if result_template.sections:
detail_default['sections'] = [('create', [{
'name': s.name,
'data': s.data,
'data_id': s.data_id,
'position': s.position,
'order': s.order,
} for s in result_template.sections])]
resultrange_origin = notebook.fraction.sample.resultrange_origin
if resultrange_origin:
detail_default['resultrange_origin'] = resultrange_origin.id
@ -89,9 +137,51 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
'order': c.order,
} for c in detail.trend_charts])]
detail_default['charts_x_row'] = detail.charts_x_row
if detail.sections:
detail_default['sections'] = [('create', [{
'name': s.name,
'data': s.data,
'data_id': s.data_id,
'position': s.position,
'order': s.order,
} for s in detail.sections])]
return detail_default
class ResultsReportVersionDetailSection(ModelSQL, ModelView):
'Results Report Version Detail Section'
__name__ = 'lims.results_report.version.detail.section'
_order_name = 'order'
version_detail = fields.Many2One('lims.results_report.version.detail',
'Report Detail', ondelete='CASCADE', select=True, required=True)
name = fields.Char('Name', required=True)
data = fields.Binary('File', filename='name', required=True,
file_id='data_id', store_prefix='results_report_section')
data_id = fields.Char('File ID', readonly=True)
position = fields.Selection([
('previous', 'Previous'),
('following', 'Following'),
], 'Position', required=True)
order = fields.Integer('Order')
@classmethod
def __setup__(cls):
super(ResultsReportVersionDetailSection, cls).__setup__()
cls._order.insert(0, ('order', 'ASC'))
@classmethod
def validate(cls, sections):
super(ResultsReportVersionDetailSection, cls).validate(sections)
merger = PdfFileMerger(strict=False)
for section in sections:
filedata = BytesIO(section.data)
try:
merger.append(filedata)
except PdfReadError:
raise UserError(gettext('lims_report_html.msg_section_pdf'))
class ResultsReportVersionDetailTrendChart(ModelSQL, ModelView):
'Results Report Version Detail Trend Chart'
__name__ = 'lims.results_report.version.detail.trend.chart'
@ -225,45 +315,53 @@ class ResultReport(metaclass=PoolMeta):
@classmethod
def _execute_html_results_report(cls, records, data, action):
documents = []
for record in records:
template_id, tcontent, theader, tfooter = (
cls.get_results_report_template(action, record.id))
context = Transaction().context
context['template'] = template_id
if not template_id:
context['default_translations'] = os.path.join(
os.path.dirname(__file__), 'report', 'translations')
with Transaction().set_context(**context):
content = cls.render_results_report_template(action,
tcontent, record=record, records=[record],
data=data)
header = theader and cls.render_results_report_template(action,
theader, record=record, records=[record],
data=data)
footer = tfooter and cls.render_results_report_template(action,
tfooter, record=record, records=[record],
data=data)
record = records[0]
template_id, tcontent, theader, tfooter = (
cls.get_results_report_template(action, record.id))
context = Transaction().context
context['template'] = template_id
if not template_id:
context['default_translations'] = os.path.join(
os.path.dirname(__file__), 'report', 'translations')
with Transaction().set_context(**context):
content = cls.render_results_report_template(action,
tcontent, record=record, records=[record],
data=data)
header = theader and cls.render_results_report_template(action,
theader, record=record, records=[record],
data=data)
footer = tfooter and cls.render_results_report_template(action,
tfooter, record=record, records=[record],
data=data)
stylesheets = cls.parse_stylesheets(tcontent)
if theader:
stylesheets += cls.parse_stylesheets(theader)
if tfooter:
stylesheets += cls.parse_stylesheets(tfooter)
stylesheets = cls.parse_stylesheets(tcontent)
if theader:
stylesheets += cls.parse_stylesheets(theader)
if tfooter:
stylesheets += cls.parse_stylesheets(tfooter)
if action.extension == 'pdf':
documents.append(PdfGenerator(content,
header_html=header, footer_html=footer,
side_margin=1, extra_vertical_margin=30,
stylesheets=stylesheets).render_html())
else:
documents.append(content)
if action.extension == 'pdf':
document = documents[0].copy([page for doc in documents
for page in doc.pages])
document = document.write_pdf()
else:
document = ''.join(documents)
return action.extension, document
document = PdfGenerator(content,
header_html=header, footer_html=footer,
side_margin=1, extra_vertical_margin=30,
stylesheets=stylesheets).render_html().write_pdf()
if record.previous_sections or record.following_sections:
merger = PdfFileMerger(strict=False)
# Previous Sections
for section in record.previous_sections:
filedata = BytesIO(section.data)
merger.append(filedata)
# Results Report
filedata = BytesIO(document)
merger.append(filedata)
# Following Sections
for section in record.following_sections:
filedata = BytesIO(section.data)
merger.append(filedata)
output = BytesIO()
merger.write(output)
document = output.getvalue()
return 'pdf', document
@classmethod
def get_results_report_template(cls, action, detail_id):

View file

@ -15,6 +15,19 @@
<field name="name">results_report_version_detail_form</field>
</record>
<!-- Results Report Version Detail Section -->
<record model="ir.ui.view" id="results_report_version_detail_section_view_form">
<field name="model">lims.results_report.version.detail.section</field>
<field name="type">form</field>
<field name="name">results_report_version_detail_section_form</field>
</record>
<record model="ir.ui.view" id="results_report_version_detail_section_view_list">
<field name="model">lims.results_report.version.detail.section</field>
<field name="type">tree</field>
<field name="name">results_report_version_detail_section_list</field>
</record>
<!-- Results Report Version Detail Trend Chart -->
<record model="ir.ui.view" id="results_report_version_detail_chart_view_form">

View file

@ -25,6 +25,10 @@
<field name="comments" colspan="4" widget="html"/>
</xpath>
<xpath expr="/form/notebook/page[@name='comments']" position="after">
<page name="sections">
<field name="previous_sections" colspan="4"/>
<field name="following_sections" colspan="4"/>
</page>
<page name="trend_charts">
<field name="trend_charts" colspan="4"/>
<label name="charts_x_row"/>

View file

@ -0,0 +1,6 @@
<form>
<label name="name"/>
<field name="data" filename_visible="1"/>
<label name="order"/>
<field name="order"/>
</form>

View file

@ -0,0 +1,6 @@
<tree editable="bottom">
<field name="data"/>
<field name="name" expand="2"/>
<field name="order"/>
<field name="position" tree_invisible="1"/>
</tree>

View file

@ -16,6 +16,10 @@
<page name="translations">
<field name="translations" colspan="4"/>
</page>
<page name="sections">
<field name="previous_sections" colspan="4"/>
<field name="following_sections" colspan="4"/>
</page>
<page name="trend_charts">
<field name="trend_charts" colspan="4"/>
<label name="charts_x_row"/>

View file

@ -0,0 +1,6 @@
<form>
<label name="name"/>
<field name="data" filename_visible="1"/>
<label name="order"/>
<field name="order"/>
</form>

View file

@ -0,0 +1,6 @@
<tree editable="bottom">
<field name="data"/>
<field name="name" expand="2"/>
<field name="order"/>
<field name="position" tree_invisible="1"/>
</tree>