lims_report_html, lims_diagnosis: report template refactoring

This commit is contained in:
Adrián Bernardi 2022-05-07 17:45:37 -03:00
parent 34b4375a78
commit cdaba55c14
30 changed files with 757 additions and 551 deletions

View File

@ -24,7 +24,7 @@ def register():
html_template.DiagnosisStateImage, html_template.DiagnosisStateImage,
html_template.DiagnosisTemplate, html_template.DiagnosisTemplate,
html_template.DiagnosisTemplateState, html_template.DiagnosisTemplateState,
html_template.ReportTemplate, html_template.ResultsReportTemplate,
sample.Fraction, sample.Fraction,
sample.Sample, sample.Sample,
sample.CreateSampleStart, sample.CreateSampleStart,

View File

@ -4,6 +4,7 @@
from trytond.model import ModelSQL, ModelView, fields, DictSchemaMixin from trytond.model import ModelSQL, ModelView, fields, DictSchemaMixin
from trytond.pool import PoolMeta from trytond.pool import PoolMeta
from trytond.pyson import Eval
class DiagnosisTemplate(ModelSQL, ModelView): class DiagnosisTemplate(ModelSQL, ModelView):
@ -51,10 +52,14 @@ class DiagnosisTemplateState(ModelSQL):
required=True, ondelete='CASCADE', select=True) required=True, ondelete='CASCADE', select=True)
class ReportTemplate(metaclass=PoolMeta): class ResultsReportTemplate(metaclass=PoolMeta):
__name__ = 'lims.result_report.template' __name__ = 'lims.report.template'
diagnosis_template = fields.Many2One('lims.diagnosis.template', diagnosis_template = fields.Many2One('lims.diagnosis.template',
'Diagnosis Template') 'Diagnosis Template',
states={'invisible': Eval('type') != 'base'},
depends=['type'])
diagnosis_length = fields.Integer('Diagnosis Length', diagnosis_length = fields.Integer('Diagnosis Length',
states={'invisible': Eval('type') != 'base'},
depends=['type'],
help='Maximum number of characters in diagnosis') help='Maximum number of characters in diagnosis')

View File

@ -86,10 +86,10 @@
<!-- Results Report Template --> <!-- Results Report Template -->
<record model="ir.ui.view" id="template_view_form"> <record model="ir.ui.view" id="result_template_view_form">
<field name="model">lims.result_report.template</field> <field name="model">lims.report.template</field>
<field name="inherit" ref="lims_report_html.template_view_form"/> <field name="inherit" ref="lims_report_html.result_template_view_form"/>
<field name="name">template_form</field> <field name="name">result_template_form</field>
</record> </record>
</data> </data>

View File

@ -110,11 +110,11 @@ msgctxt "field:lims.product.type,diagnostician:"
msgid "Diagnostician" msgid "Diagnostician"
msgstr "Diagnosticador" msgstr "Diagnosticador"
msgctxt "field:lims.result_report.template,diagnosis_length:" msgctxt "field:lims.report.template,diagnosis_length:"
msgid "Diagnosis Length" msgid "Diagnosis Length"
msgstr "Longitud del diagnóstico" msgstr "Longitud del diagnóstico"
msgctxt "field:lims.result_report.template,diagnosis_template:" msgctxt "field:lims.report.template,diagnosis_template:"
msgid "Diagnosis Template" msgid "Diagnosis Template"
msgstr "Plantilla de Diagnóstico" msgstr "Plantilla de Diagnóstico"
@ -267,7 +267,7 @@ msgctxt "help:lims.notebook.repeat_analysis.start,notify_acceptance:"
msgid "Notify when analysis is ready" msgid "Notify when analysis is ready"
msgstr "Notificar cuando el análisis esté terminado" msgstr "Notificar cuando el análisis esté terminado"
msgctxt "help:lims.result_report.template,diagnosis_length:" msgctxt "help:lims.report.template,diagnosis_length:"
msgid "Maximum number of characters in diagnosis" msgid "Maximum number of characters in diagnosis"
msgstr "Cantidad máxima de caracteres en diagnóstico" msgstr "Cantidad máxima de caracteres en diagnóstico"

View File

@ -110,7 +110,7 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
def _get_fields_from_samples(cls, samples, generate_report_form=None): def _get_fields_from_samples(cls, samples, generate_report_form=None):
pool = Pool() pool = Pool()
Notebook = pool.get('lims.notebook') Notebook = pool.get('lims.notebook')
ReportTemplate = pool.get('lims.result_report.template') ReportTemplate = pool.get('lims.report.template')
detail_default = super()._get_fields_from_samples(samples, detail_default = super()._get_fields_from_samples(samples,
generate_report_form) generate_report_form)
@ -476,7 +476,8 @@ class SamplesComparatorLine(ModelSQL, ModelView):
result[name] = {} result[name] = {}
if name == 'result': if name == 'result':
for l in lines: for l in lines:
result[name][l.id] = l.notebook_line.formated_result result[name][l.id] = (
l.notebook_line.get_formated_result())
elif name == 'converted_result': elif name == 'converted_result':
for l in lines: for l in lines:
result[name][l.id] = ( result[name][l.id] = (
@ -514,7 +515,7 @@ class SamplesComparatorLine(ModelSQL, ModelView):
]) ])
if not notebook_line: if not notebook_line:
return None return None
return notebook_line[0].formated_result return notebook_line[0].get_formated_result()
class Cron(metaclass=PoolMeta): class Cron(metaclass=PoolMeta):
@ -533,8 +534,8 @@ class ResultReport(metaclass=PoolMeta):
__name__ = 'lims.result_report' __name__ = 'lims.result_report'
@classmethod @classmethod
def get_context(cls, records, header, data): def get_context(cls, records, data):
report_context = super().get_context(records, header, data) report_context = super().get_context(records, data)
report_context['state_image'] = cls.get_state_image report_context['state_image'] = cls.get_state_image
return report_context return report_context

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<data> <data>
<xpath expr="/form/field[@name='report']" position="after"> <xpath expr="/form/field[@name='page_orientation']" position="after">
<label name="diagnosis_template"/> <label name="diagnosis_template"/>
<field name="diagnosis_template"/> <field name="diagnosis_template"/>
<group id="diagnosis_length" colspan="2"> <group id="diagnosis_length" colspan="2">

View File

@ -20,7 +20,8 @@ def register():
html_template.ReportTemplate, html_template.ReportTemplate,
html_template.ReportTemplateTranslation, html_template.ReportTemplateTranslation,
html_template.ReportTemplateSection, html_template.ReportTemplateSection,
html_template.ReportTemplateTrendChart, html_template.ResultsReportTemplate,
html_template.ResultsReportTemplateTrendChart,
configuration.Configuration, configuration.Configuration,
laboratory.Laboratory, laboratory.Laboratory,
party.Party, party.Party,

View File

@ -3,6 +3,7 @@
# the full copyright notices and license terms. # the full copyright notices and license terms.
from trytond.pool import PoolMeta from trytond.pool import PoolMeta
from trytond.transaction import Transaction
class ActionReport(metaclass=PoolMeta): class ActionReport(metaclass=PoolMeta):
@ -11,13 +12,21 @@ class ActionReport(metaclass=PoolMeta):
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
super().__setup__() super().__setup__()
results_option = ('results', 'Results Report') lims_option = ('lims', 'Lims Report')
if results_option not in cls.template_extension.selection: if lims_option not in cls.template_extension.selection:
cls.template_extension.selection.append(results_option) cls.template_extension.selection.append(lims_option)
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
table = cls.__table__()
super().__register__(module_name)
cursor.execute(*table.update([table.template_extension], ['lims'],
where=(table.template_extension == 'results')))
class ReportTranslationSet(metaclass=PoolMeta): class ReportTranslationSet(metaclass=PoolMeta):
__name__ = 'ir.translation.set' __name__ = 'ir.translation.set'
def extract_report_results(self, content): def extract_report_lims(self, content):
return [] return []

View File

@ -10,7 +10,10 @@ from trytond.pyson import Eval
class Analysis(metaclass=PoolMeta): class Analysis(metaclass=PoolMeta):
__name__ = 'lims.analysis' __name__ = 'lims.analysis'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])], 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
],
states={'readonly': Eval('type') != 'group'}, states={'readonly': Eval('type') != 'group'},
depends=['type']) depends=['type'])

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Configuration(metaclass=PoolMeta): class Configuration(metaclass=PoolMeta):
__name__ = 'lims.configuration' __name__ = 'lims.configuration'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Default Report Template', domain=[('type', 'in', [None, 'base'])]) 'Default Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])

View File

@ -1,78 +1,123 @@
# This file is part of lims_report_html module for Tryton. # This file is part of lims_report_html module for Tryton.
# The COPYRIGHT file at the top level of this repository contains # The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms. # the full copyright notices and license terms.
import os
import operator
from io import BytesIO from io import BytesIO
from decimal import Decimal
from datetime import date, datetime
from binascii import b2a_base64
from functools import partial
from PyPDF2 import PdfFileMerger from PyPDF2 import PdfFileMerger
from PyPDF2.utils import PdfReadError from PyPDF2.utils import PdfReadError
from jinja2 import contextfilter, Markup
from jinja2 import Environment, FunctionLoader
from lxml import html as lxml_html
from base64 import b64encode
from babel.support import Translations as BabelTranslations
from mimetypes import guess_type as mime_guess_type
from sql import Literal
from trytond.model import ModelSQL, ModelView, fields from trytond.model import ModelSQL, ModelView, fields
from trytond.report import Report
from trytond.pool import Pool from trytond.pool import Pool
from trytond.pyson import Eval, Bool from trytond.pyson import Eval, Bool, Or
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.cache import Cache from trytond.cache import Cache
from trytond.exceptions import UserError from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.tools import file_open
from trytond import backend
from .generator import PdfGenerator
class ReportTemplate(ModelSQL, ModelView): class ReportTemplate(ModelSQL, ModelView):
'Results Report Template' 'Report Template'
__name__ = 'lims.result_report.template' __name__ = 'lims.report.template'
_history = True
report_name = fields.Char('Internal Name', required=True)
name = fields.Char('Name', required=True) name = fields.Char('Name', required=True)
report = fields.Many2One('ir.action.report', 'Report',
domain=[
('report_name', '=', 'lims.result_report'),
('template_extension', '!=', 'results'),
],
states={'required': ~Eval('type')}, depends=['type'])
type = fields.Selection([ type = fields.Selection([
(None, ''), (None, ''),
('base', 'HTML'), ('base', 'HTML'),
('header', 'HTML - Header'), ('header', 'HTML - Header'),
('footer', 'HTML - Footer'), ('footer', 'HTML - Footer'),
], 'Type') ], 'Type')
report = fields.Many2One('ir.action.report', 'Report',
domain=[
('report_name', '=', Eval('report_name')),
('template_extension', '!=', 'lims'),
],
states={
'required': ~Eval('type'),
'invisible': Bool(Eval('type')),
},
depends=['report_name', 'type'])
content = fields.Text('Content', content = fields.Text('Content',
states={'required': Bool(Eval('type'))}, depends=['type']) states={'required': Bool(Eval('type'))}, depends=['type'])
header = fields.Many2One('lims.result_report.template', 'Header', header = fields.Many2One('lims.report.template', 'Header',
domain=[('type', '=', 'header')]) domain=[
footer = fields.Many2One('lims.result_report.template', 'Footer', ('report_name', '=', Eval('report_name')),
domain=[('type', '=', 'footer')]) ('type', '=', 'header'),
translations = fields.One2Many('lims.result_report.template.translation', ],
depends=['report_name'])
footer = fields.Many2One('lims.report.template', 'Footer',
domain=[
('report_name', '=', Eval('report_name')),
('type', '=', 'footer'),
],
depends=['report_name'])
translations = fields.One2Many('lims.report.template.translation',
'template', 'Translations') 'template', 'Translations')
_translation_cache = Cache('lims.result_report.template.translation', _translation_cache = Cache('lims.report.template.translation',
size_limit=10240, context=False) size_limit=10240, context=False)
sections = fields.One2Many('lims.result_report.template.section', sections = fields.One2Many('lims.report.template.section',
'template', 'Sections') 'template', 'Sections')
previous_sections = fields.Function(fields.One2Many( previous_sections = fields.Function(fields.One2Many(
'lims.result_report.template.section', 'template', 'lims.report.template.section', 'template',
'Previous Sections', domain=[('position', '=', 'previous')]), 'Previous Sections', domain=[('position', '=', 'previous')]),
'get_previous_sections', setter='set_previous_sections') 'get_previous_sections', setter='set_previous_sections')
following_sections = fields.Function(fields.One2Many( following_sections = fields.Function(fields.One2Many(
'lims.result_report.template.section', 'template', 'lims.report.template.section', 'template',
'Following Sections', domain=[('position', '=', 'following')]), 'Following Sections', domain=[('position', '=', 'following')]),
'get_following_sections', setter='set_following_sections') '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([
('1', '1'),
('2', '2'),
], 'Charts per Row')
page_orientation = fields.Selection([ page_orientation = fields.Selection([
('portrait', 'Portrait'), ('portrait', 'Portrait'),
('landscape', 'Landscape'), ('landscape', 'Landscape'),
], 'Page orientation', sort=False) ], 'Page orientation', sort=False,
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range', states={'invisible': Eval('type') != 'base'},
domain=[('use', '=', 'result_range')]) depends=['type'])
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
TableHandler = backend.TableHandler
sql_table = cls.__table__()
old_table_exist = TableHandler.table_exist(
'lims_result_report_template')
if old_table_exist:
cursor.execute('ALTER TABLE '
'lims_result_report_template '
'RENAME TO lims_report_template')
cursor.execute('ALTER INDEX '
'lims_result_report_template_pkey '
'RENAME TO lims_report_template_pkey')
cursor.execute('ALTER SEQUENCE '
'lims_result_report_template_id_seq '
'RENAME TO lims_report_template_id_seq')
super().__register__(module_name)
if old_table_exist:
cursor.execute(*sql_table.update(
[sql_table.report_name], ['lims.result_report'],
where=Literal(True)))
@staticmethod @staticmethod
def default_type(): def default_type():
return None return None
@staticmethod
def default_charts_x_row():
return '1'
@staticmethod @staticmethod
def default_page_orientation(): def default_page_orientation():
return 'portrait' return 'portrait'
@ -80,7 +125,7 @@ class ReportTemplate(ModelSQL, ModelView):
@classmethod @classmethod
def view_attributes(cls): def view_attributes(cls):
return super().view_attributes() + [ return super().view_attributes() + [
('//page[@id="content"]', 'states', { ('//page[@name="content"]', 'states', {
'invisible': ~Bool(Eval('type')), 'invisible': ~Bool(Eval('type')),
}), }),
('//page[@id="header_footer"]', 'states', { ('//page[@id="header_footer"]', 'states', {
@ -92,15 +137,12 @@ class ReportTemplate(ModelSQL, ModelView):
('//page[@name="sections"]', 'states', { ('//page[@name="sections"]', 'states', {
'invisible': Eval('type') != 'base', 'invisible': Eval('type') != 'base',
}), }),
('//page[@name="trend_charts"]', 'states', {
'invisible': Eval('type') != 'base',
}),
] ]
@classmethod @classmethod
def gettext(cls, *args, **variables): def gettext(cls, *args, **variables):
ReportTemplateTranslation = Pool().get( ReportTemplateTranslation = Pool().get(
'lims.result_report.template.translation') 'lims.report.template.translation')
template, src, lang = args template, src, lang = args
key = (template, src, lang) key = (template, src, lang)
text = cls._translation_cache.get(key) text = cls._translation_cache.get(key)
@ -143,17 +185,40 @@ class ReportTemplate(ModelSQL, ModelView):
class ReportTemplateTranslation(ModelSQL, ModelView): class ReportTemplateTranslation(ModelSQL, ModelView):
'Results Report Template Translation' 'Report Template Translation'
__name__ = 'lims.result_report.template.translation' __name__ = 'lims.report.template.translation'
_order_name = 'src' _order_name = 'src'
template = fields.Many2One('lims.result_report.template', 'Template', template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True) ondelete='CASCADE', select=True, required=True)
src = fields.Text('Source', required=True) src = fields.Text('Source', required=True)
value = fields.Text('Translation Value', required=True) value = fields.Text('Translation Value', required=True)
lang = fields.Selection('get_language', string='Language', required=True) lang = fields.Selection('get_language', string='Language', required=True)
_get_language_cache = Cache( _get_language_cache = Cache(
'lims.result_report.template.translation.get_language') 'lims.report.template.translation.get_language')
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
TableHandler = backend.TableHandler
old_table_exist = TableHandler.table_exist(
'lims_result_report_template_translation')
if old_table_exist:
cursor.execute('ALTER TABLE '
'lims_result_report_template_translation '
'RENAME TO lims_report_template_translation')
cursor.execute('ALTER INDEX '
'lims_result_report_template_translation_pkey '
'RENAME TO lims_report_template_translation_pkey')
cursor.execute('ALTER INDEX '
'lims_result_report_template_translation_template_index '
'RENAME TO lims_report_template_translation_template_index')
cursor.execute('ALTER SEQUENCE '
'lims_result_report_template_translation_id_seq '
'RENAME TO lims_report_template_translation_id_seq')
super().__register__(module_name)
@staticmethod @staticmethod
def default_lang(): def default_lang():
@ -171,29 +236,29 @@ class ReportTemplateTranslation(ModelSQL, ModelView):
@classmethod @classmethod
def create(cls, vlist): def create(cls, vlist):
Template = Pool().get('lims.result_report.template') Template = Pool().get('lims.report.template')
Template._translation_cache.clear() Template._translation_cache.clear()
return super().create(vlist) return super().create(vlist)
@classmethod @classmethod
def write(cls, *args): def write(cls, *args):
Template = Pool().get('lims.result_report.template') Template = Pool().get('lims.report.template')
Template._translation_cache.clear() Template._translation_cache.clear()
return super().write(*args) return super().write(*args)
@classmethod @classmethod
def delete(cls, translations): def delete(cls, translations):
Template = Pool().get('lims.result_report.template') Template = Pool().get('lims.report.template')
Template._translation_cache.clear() Template._translation_cache.clear()
return super().delete(translations) return super().delete(translations)
class ReportTemplateSection(ModelSQL, ModelView): class ReportTemplateSection(ModelSQL, ModelView):
'Results Report Template Section' 'Report Template Section'
__name__ = 'lims.result_report.template.section' __name__ = 'lims.report.template.section'
_order_name = 'order' _order_name = 'order'
template = fields.Many2One('lims.result_report.template', 'Template', template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True) ondelete='CASCADE', select=True, required=True)
name = fields.Char('Name', required=True) name = fields.Char('Name', required=True)
data = fields.Binary('File', filename='name', required=True, data = fields.Binary('File', filename='name', required=True,
@ -210,6 +275,29 @@ class ReportTemplateSection(ModelSQL, ModelView):
super().__setup__() super().__setup__()
cls._order.insert(0, ('order', 'ASC')) cls._order.insert(0, ('order', 'ASC'))
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
TableHandler = backend.TableHandler
old_table_exist = TableHandler.table_exist(
'lims_result_report_template_section')
if old_table_exist:
cursor.execute('ALTER TABLE '
'lims_result_report_template_section '
'RENAME TO lims_report_template_section ')
cursor.execute('ALTER INDEX '
'lims_result_report_template_section_pkey '
'RENAME TO lims_report_template_section_pkey')
cursor.execute('ALTER INDEX '
'lims_result_report_template_section_template_index '
'RENAME TO lims_report_template_section_template_index')
cursor.execute('ALTER SEQUENCE '
'lims_result_report_template_section_id_seq '
'RENAME TO lims_report_template_section_id_seq')
super().__register__(module_name)
@classmethod @classmethod
def validate(cls, sections): def validate(cls, sections):
super().validate(sections) super().validate(sections)
@ -222,13 +310,398 @@ class ReportTemplateSection(ModelSQL, ModelView):
raise UserError(gettext('lims_report_html.msg_section_pdf')) raise UserError(gettext('lims_report_html.msg_section_pdf'))
class ReportTemplateTrendChart(ModelSQL, ModelView): class LimsReport(Report):
@classmethod
def execute_custom_lims_report(cls, ids, data):
pool = Pool()
ActionReport = pool.get('ir.action.report')
cls.check_access()
action_id = data.get('action_id')
if action_id is None:
action_reports = ActionReport.search([
('report_name', '=', cls.__name__),
('template_extension', '!=', 'lims'),
])
assert action_reports, '%s not found' % cls
action = action_reports[0]
else:
action = ActionReport(action_id)
records = []
model = action.model or data.get('model')
if model:
records = cls._get_records(ids, model, data)
oext, content = cls._execute(records, data, action)
if not isinstance(content, str):
content = bytearray(content) if bytes == str else bytes(content)
return (oext, content, action.direct_print, action.name)
@classmethod
def execute_html_lims_report(cls, ids, data):
pool = Pool()
ActionReport = pool.get('ir.action.report')
cls.check_access()
action_reports = ActionReport.search([
('report_name', '=', cls.__name__),
('template_extension', '=', 'lims'),
])
assert action_reports, '%s not found' % cls
action = action_reports[0]
records = []
model = action.model or data.get('model')
if model:
records = cls._get_records(ids, model, data)
oext, content = cls._execute_html_lims_report(records, data, action)
if not isinstance(content, str):
content = bytearray(content) if bytes == str else bytes(content)
return (oext, content, action.direct_print, action.name)
@classmethod
def _execute_html_lims_report(cls, records, data, action):
record = records[0]
template_id, tcontent, theader, tfooter = (
cls.get_lims_template(action, record))
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_lims_template(action,
tcontent, record=record, records=[record],
data=data)
header = theader and cls.render_lims_template(action,
theader, record=record, records=[record],
data=data)
footer = tfooter and cls.render_lims_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)
page_orientation = (record.template and
record.template.page_orientation or 'portrait')
document = PdfGenerator(content,
header_html=header, footer_html=footer,
side_margin=1, extra_vertical_margin=30,
stylesheets=stylesheets,
page_orientation=page_orientation).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)
# Main 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_lims_template(cls, action, record):
template_id, content, header, footer = None, None, None, None
if record.template:
template_id = record.template
content = '<body>%s</body>' % record.template.content
header = (record.template.header and
'<header id="header">%s</header>' %
record.template.header.content)
footer = (record.template.footer and
'<footer id="footer">%s</footer>' %
record.template.footer.content)
if not content:
content = (action.report_content and
action.report_content.decode('utf-8'))
if not content:
raise UserError(gettext('lims_report_html.msg_no_template'))
return template_id, content, header, footer
@classmethod
def render_lims_template(cls, action, template_string,
record=None, records=None, data=None):
User = Pool().get('res.user')
user = User(Transaction().user)
if data and data.get('alt_lang'):
locale = data['alt_lang']
elif user.language:
locale = user.language.code
else:
locale = Transaction().language
with Transaction().set_context(locale=locale):
env = cls.get_lims_environment()
report_template = env.from_string(template_string)
context = cls.get_context(records, data)
context.update({
'report': action,
'get_image': cls.get_image,
'operation': cls.operation,
})
res = report_template.render(**context)
res = cls.parse_images(res)
# print('TEMPLATE:\n', res)
return res
@classmethod
def get_lims_environment(cls):
extensions = ['jinja2.ext.i18n', 'jinja2.ext.autoescape',
'jinja2.ext.with_', 'jinja2.ext.loopcontrols', 'jinja2.ext.do']
env = Environment(extensions=extensions,
loader=FunctionLoader(lambda name: ''))
env.filters.update(cls.get_lims_filters())
locale = Transaction().context.get('locale').split('_')[0]
translations = TemplateTranslations(locale)
env.install_gettext_translations(translations)
return env
@classmethod
def get_lims_filters(cls):
Lang = Pool().get('ir.lang')
def module_path(name):
module, path = name.split('/', 1)
with file_open(os.path.join(module, path)) as f:
return 'file://%s' % f.name
def render(value, digits=2, lang=None, filename=None):
if value is None or value == '':
return ''
if isinstance(value, (float, Decimal)):
return lang.format('%.*f', (digits, value), grouping=True)
if isinstance(value, int):
return lang.format('%d', value, grouping=True)
if isinstance(value, bool):
if value:
return gettext('lims_report_html.msg_yes')
return gettext('lims_report_html.msg_no')
if hasattr(value, 'rec_name'):
return value.rec_name
if isinstance(value, date):
return lang.strftime(value)
if isinstance(value, datetime):
return '%s %s' % (lang.strftime(value),
value.strftime('%H:%M:%S'))
if isinstance(value, str):
return value.replace('\n', '<br/>')
if isinstance(value, bytes):
b64_value = b2a_base64(value).decode('ascii')
mimetype = 'image/png'
if filename:
mimetype = mime_guess_type(filename)[0]
return ('data:%s;base64,%s' % (mimetype, b64_value)).strip()
return value
@contextfilter
def subrender(context, value, subobj=None):
if value is None or value == '':
return ''
_template = context.eval_ctx.environment.from_string(value)
if subobj:
new_context = {'subobj': subobj}
new_context.update(context)
else:
new_context = context
result = _template.render(**new_context)
if context.eval_ctx.autoescape:
result = Markup(result)
return result
locale = Transaction().context.get('locale').split('_')[0]
lang, = Lang.search([('code', '=', locale or 'en')])
return {
'modulepath': module_path,
'render': partial(render, lang=lang),
'subrender': subrender,
}
@classmethod
def parse_images(cls, template_string):
Attachment = Pool().get('ir.attachment')
root = lxml_html.fromstring(template_string)
for elem in root.iter('img'):
# get image from attachments
if 'id' in elem.attrib:
img = Attachment.search([('id', '=', int(elem.attrib['id']))])
if img:
elem.attrib['src'] = cls.get_image(img[0].data)
# get image from TinyMCE widget
elif 'data-mce-src' in elem.attrib:
elem.attrib['src'] = elem.attrib['data-mce-src']
del elem.attrib['data-mce-src']
# set width and height in style attribute
style = elem.attrib.get('style', '')
if 'width' in elem.attrib:
style += ' width: %spx;' % str(elem.attrib['width'])
if 'height' in elem.attrib:
style += ' height: %spx;' % str(elem.attrib['height'])
elem.attrib['style'] = style
return lxml_html.tostring(root).decode()
@classmethod
def get_image(cls, image):
if not image:
return ''
b64_image = b64encode(image).decode()
return 'data:image/png;base64,%s' % b64_image
@classmethod
def operation(cls, function, value1, value2):
return getattr(operator, function)(value1, value2)
@classmethod
def parse_stylesheets(cls, template_string):
Attachment = Pool().get('ir.attachment')
root = lxml_html.fromstring(template_string)
res = []
# get stylesheets from attachments
elems = root.xpath("//div[@id='tryton_styles_container']/div")
for elem in elems:
css = Attachment.search([('id', '=', int(elem.attrib['id']))])
if not css:
continue
res.append(css[0].data)
return res
class TemplateTranslations:
def __init__(self, lang='en'):
self.cache = {}
self.env = None
self.current = None
self.language = lang
self.template = None
self.set_language(lang)
def set_language(self, lang='en'):
self.language = lang
if lang in self.cache:
self.current = self.cache[lang]
return
context = Transaction().context
if context.get('default_translations'):
default_translations = context['default_translations']
if os.path.isdir(default_translations):
self.current = BabelTranslations.load(
dirname=default_translations, locales=[lang])
self.cache[lang] = self.current
else:
self.template = context.get('template', -1)
def ugettext(self, message):
ReportTemplate = Pool().get('lims.report.template')
if self.current:
return self.current.ugettext(message)
elif self.template:
return ReportTemplate.gettext(self.template, message,
self.language)
return message
def ngettext(self, singular, plural, n):
ReportTemplate = Pool().get('lims.report.template')
if self.current:
return self.current.ugettext(singular, plural, n)
elif self.template:
return ReportTemplate.gettext(self.template, singular,
self.language)
return singular
class ResultsReportTemplate(ReportTemplate):
__name__ = 'lims.report.template'
trend_charts = fields.One2Many('lims.report.template.trend.chart',
'template', 'Trend Charts')
charts_x_row = fields.Selection([
('1', '1'),
('2', '2'),
], 'Charts per Row')
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')],
states={
'invisible': Or(
Eval('type') != 'base',
Eval('report_name') != 'lims.result_report',
),
},
depends=['type'])
@staticmethod
def default_charts_x_row():
return '1'
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//page[@name="trend_charts"]', 'states', {
'invisible': Or(
Eval('type') != 'base',
Eval('report_name') != 'lims.result_report',
)
}),
]
class ResultsReportTemplateTrendChart(ModelSQL, ModelView):
'Results Report Template Trend Chart' 'Results Report Template Trend Chart'
__name__ = 'lims.result_report.template.trend.chart' __name__ = 'lims.report.template.trend.chart'
_order_name = 'order' _order_name = 'order'
template = fields.Many2One('lims.result_report.template', 'Template', template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True) ondelete='CASCADE', select=True, required=True)
chart = fields.Many2One('lims.trend.chart', 'Trend Chart', chart = fields.Many2One('lims.trend.chart', 'Trend Chart',
required=True, domain=[('active', '=', True)]) required=True, domain=[('active', '=', True)])
order = fields.Integer('Order') order = fields.Integer('Order')
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
TableHandler = backend.TableHandler
old_table_exist = TableHandler.table_exist(
'lims_result_report_template_trend_chart')
if old_table_exist:
cursor.execute('ALTER TABLE '
'lims_result_report_template_trend_chart '
'RENAME TO lims_report_template_trend_chart ')
cursor.execute('ALTER INDEX '
'lims_result_report_template_trend_chart_pkey '
'RENAME TO lims_report_template_trend_chart_pkey')
cursor.execute('ALTER INDEX '
'lims_result_report_template_trend_chart_template_index '
'RENAME TO lims_report_template_trend_chart_template_index')
cursor.execute('ALTER SEQUENCE '
'lims_result_report_template_trend_chart_id_seq '
'RENAME TO lims_report_template_trend_chart_id_seq')
super().__register__(module_name)

View File

@ -2,73 +2,76 @@
<tryton> <tryton>
<data> <data>
<!-- Results Report Template --> <!-- Report Template Translation -->
<record model="ir.ui.view" id="template_view_form">
<field name="model">lims.result_report.template</field>
<field name="type">form</field>
<field name="name">template_form</field>
</record>
<record model="ir.ui.view" id="template_view_list">
<field name="model">lims.result_report.template</field>
<field name="type">tree</field>
<field name="name">template_list</field>
</record>
<record model="ir.action.act_window" id="act_html_template_list">
<field name="name">Results Report Templates</field>
<field name="res_model">lims.result_report.template</field>
</record>
<record model="ir.action.act_window.view" id="act_html_template_view_list">
<field name="sequence" eval="10"/>
<field name="view" ref="template_view_list"/>
<field name="act_window" ref="act_html_template_list"/>
</record>
<record model="ir.action.act_window.view" id="act_html_template_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="template_view_form"/>
<field name="act_window" ref="act_html_template_list"/>
</record>
<menuitem action="act_html_template_list"
id="menu_html_template_list"
parent="lims.lims_config_report" sequence="10"/>
<!-- Results Report Template Translation -->
<record model="ir.ui.view" id="template_translation_view_form"> <record model="ir.ui.view" id="template_translation_view_form">
<field name="model">lims.result_report.template.translation</field> <field name="model">lims.report.template.translation</field>
<field name="type">form</field> <field name="type">form</field>
<field name="name">template_translation_form</field> <field name="name">template_translation_form</field>
</record> </record>
<record model="ir.ui.view" id="template_translation_view_list"> <record model="ir.ui.view" id="template_translation_view_list">
<field name="model">lims.result_report.template.translation</field> <field name="model">lims.report.template.translation</field>
<field name="type">tree</field> <field name="type">tree</field>
<field name="name">template_translation_list</field> <field name="name">template_translation_list</field>
</record> </record>
<!-- Results Report Template Section --> <!-- Report Template Section -->
<record model="ir.ui.view" id="template_section_view_form"> <record model="ir.ui.view" id="template_section_view_form">
<field name="model">lims.result_report.template.section</field> <field name="model">lims.report.template.section</field>
<field name="type">form</field> <field name="type">form</field>
<field name="name">template_section_form</field> <field name="name">template_section_form</field>
</record> </record>
<record model="ir.ui.view" id="template_section_view_list"> <record model="ir.ui.view" id="template_section_view_list">
<field name="model">lims.result_report.template.section</field> <field name="model">lims.report.template.section</field>
<field name="type">tree</field> <field name="type">tree</field>
<field name="name">template_section_list</field> <field name="name">template_section_list</field>
</record> </record>
<!-- Results Report Template -->
<record model="ir.ui.view" id="result_template_view_form">
<field name="model">lims.report.template</field>
<field name="type">form</field>
<field name="name">result_template_form</field>
</record>
<record model="ir.ui.view" id="result_template_view_list">
<field name="model">lims.report.template</field>
<field name="type">tree</field>
<field name="name">result_template_list</field>
</record>
<record model="ir.action.act_window" id="act_result_template_list">
<field name="name">Results Report Templates</field>
<field name="res_model">lims.report.template</field>
<field name="domain" pyson="1"
eval="[('report_name', '=', 'lims.result_report')]"/>
</record>
<record model="ir.action.act_window.view" id="act_result_template_view_list">
<field name="sequence" eval="10"/>
<field name="view" ref="result_template_view_list"/>
<field name="act_window" ref="act_result_template_list"/>
</record>
<record model="ir.action.act_window.view" id="act_result_template_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="result_template_view_form"/>
<field name="act_window" ref="act_result_template_list"/>
</record>
<menuitem action="act_result_template_list"
id="menu_result_template_list"
parent="lims.lims_config_report" sequence="10"
icon="tryton-list"/>
<!-- Results Report Template Trend Chart --> <!-- Results Report Template Trend Chart -->
<record model="ir.ui.view" id="template_trend_chart_view_form"> <record model="ir.ui.view" id="template_trend_chart_view_form">
<field name="model">lims.result_report.template.trend.chart</field> <field name="model">lims.report.template.trend.chart</field>
<field name="type">form</field> <field name="type">form</field>
<field name="name">template_trend_chart_form</field> <field name="name">template_trend_chart_form</field>
</record> </record>
<record model="ir.ui.view" id="template_trend_chart_view_list"> <record model="ir.ui.view" id="template_trend_chart_view_list">
<field name="model">lims.result_report.template.trend.chart</field> <field name="model">lims.report.template.trend.chart</field>
<field name="type">tree</field> <field name="type">tree</field>
<field name="name">template_trend_chart_list</field> <field name="name">template_trend_chart_list</field>
</record> </record>

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Laboratory(metaclass=PoolMeta): class Laboratory(metaclass=PoolMeta):
__name__ = 'lims.laboratory' __name__ = 'lims.laboratory'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])]) 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])

View File

@ -38,111 +38,115 @@ msgctxt "field:lims.notebook.generate_results_report.start,template:"
msgid "Report Template" msgid "Report Template"
msgstr "Plantilla de Informe" msgstr "Plantilla de Informe"
msgctxt "field:lims.result_report.template,charts_x_row:" msgctxt "field:lims.report.template,charts_x_row:"
msgid "Charts per Row" msgid "Charts per Row"
msgstr "Gráficos por fila" msgstr "Gráficos por fila"
msgctxt "field:lims.result_report.template,content:" msgctxt "field:lims.report.template,content:"
msgid "Content" msgid "Content"
msgstr "Contenido" msgstr "Contenido"
msgctxt "field:lims.result_report.template,following_sections:" msgctxt "field:lims.report.template,following_sections:"
msgid "Following Sections" msgid "Following Sections"
msgstr "Secciones siguientes" msgstr "Secciones siguientes"
msgctxt "field:lims.result_report.template,footer:" msgctxt "field:lims.report.template,footer:"
msgid "Footer" msgid "Footer"
msgstr "Pie de página" msgstr "Pie de página"
msgctxt "field:lims.result_report.template,header:" msgctxt "field:lims.report.template,header:"
msgid "Header" msgid "Header"
msgstr "Encabezado" msgstr "Encabezado"
msgctxt "field:lims.result_report.template,name:" msgctxt "field:lims.report.template,name:"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
msgctxt "field:lims.result_report.template,page_orientation:" msgctxt "field:lims.report.template,page_orientation:"
msgid "Page orientation" msgid "Page orientation"
msgstr "Orientación de la página" msgstr "Orientación de la página"
msgctxt "field:lims.result_report.template,previous_sections:" msgctxt "field:lims.report.template,previous_sections:"
msgid "Previous Sections" msgid "Previous Sections"
msgstr "Secciones anteriores" msgstr "Secciones anteriores"
msgctxt "field:lims.result_report.template,report:" msgctxt "field:lims.report.template,report:"
msgid "Report" msgid "Report"
msgstr "Informe" msgstr "Informe"
msgctxt "field:lims.result_report.template,resultrange_origin:" msgctxt "field:lims.report.template,report_name:"
msgid "Internal Name"
msgstr "Nombre interno"
msgctxt "field:lims.report.template,resultrange_origin:"
msgid "Comparison range" msgid "Comparison range"
msgstr "Rango de comparación" msgstr "Rango de comparación"
msgctxt "field:lims.result_report.template,sections:" msgctxt "field:lims.report.template,sections:"
msgid "Sections" msgid "Sections"
msgstr "Secciones" msgstr "Secciones"
msgctxt "field:lims.result_report.template,translations:" msgctxt "field:lims.report.template,translations:"
msgid "Translations" msgid "Translations"
msgstr "Traducciones" msgstr "Traducciones"
msgctxt "field:lims.result_report.template,trend_charts:" msgctxt "field:lims.report.template,trend_charts:"
msgid "Trend Charts" msgid "Trend Charts"
msgstr "Gráficos de tendencia" msgstr "Gráficos de tendencia"
msgctxt "field:lims.result_report.template,type:" msgctxt "field:lims.report.template,type:"
msgid "Type" msgid "Type"
msgstr "Tipo" msgstr "Tipo"
msgctxt "field:lims.result_report.template.section,data:" msgctxt "field:lims.report.template.section,data:"
msgid "File" msgid "File"
msgstr "Archivo" msgstr "Archivo"
msgctxt "field:lims.result_report.template.section,data_id:" msgctxt "field:lims.report.template.section,data_id:"
msgid "File ID" msgid "File ID"
msgstr "ID Archivo" msgstr "ID Archivo"
msgctxt "field:lims.result_report.template.section,name:" msgctxt "field:lims.report.template.section,name:"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
msgctxt "field:lims.result_report.template.section,order:" msgctxt "field:lims.report.template.section,order:"
msgid "Order" msgid "Order"
msgstr "Orden" msgstr "Orden"
msgctxt "field:lims.result_report.template.section,position:" msgctxt "field:lims.report.template.section,position:"
msgid "Position" msgid "Position"
msgstr "Posición" msgstr "Posición"
msgctxt "field:lims.result_report.template.section,template:" msgctxt "field:lims.report.template.section,template:"
msgid "Template" msgid "Template"
msgstr "Plantilla" msgstr "Plantilla"
msgctxt "field:lims.result_report.template.translation,lang:" msgctxt "field:lims.report.template.translation,lang:"
msgid "Language" msgid "Language"
msgstr "Idioma" msgstr "Idioma"
msgctxt "field:lims.result_report.template.translation,src:" msgctxt "field:lims.report.template.translation,src:"
msgid "Source" msgid "Source"
msgstr "Original" msgstr "Original"
msgctxt "field:lims.result_report.template.translation,template:" msgctxt "field:lims.report.template.translation,template:"
msgid "Template" msgid "Template"
msgstr "Plantilla" msgstr "Plantilla"
msgctxt "field:lims.result_report.template.translation,value:" msgctxt "field:lims.report.template.translation,value:"
msgid "Translation Value" msgid "Translation Value"
msgstr "Traducción" msgstr "Traducción"
msgctxt "field:lims.result_report.template.trend.chart,chart:" msgctxt "field:lims.report.template.trend.chart,chart:"
msgid "Trend Chart" msgid "Trend Chart"
msgstr "Gráfico de tendencia" msgstr "Gráfico de tendencia"
msgctxt "field:lims.result_report.template.trend.chart,order:" msgctxt "field:lims.report.template.trend.chart,order:"
msgid "Order" msgid "Order"
msgstr "Orden" msgstr "Orden"
msgctxt "field:lims.result_report.template.trend.chart,template:" msgctxt "field:lims.report.template.trend.chart,template:"
msgid "Template" msgid "Template"
msgstr "Plantilla" msgstr "Plantilla"
@ -150,7 +154,7 @@ msgctxt "field:lims.results_report.version.detail,charts_x_row:"
msgid "Charts per Row" msgid "Charts per Row"
msgstr "Gráficos por fila" msgstr "Gráficos por fila"
msgctxt "field:lims.results_report.version.detail,comments_html:" msgctxt "field:lims.results_report.version.detail,comments_plain:"
msgid "Comments" msgid "Comments"
msgstr "Observaciones" msgstr "Observaciones"
@ -234,11 +238,11 @@ msgctxt "field:party.party,result_template:"
msgid "Report Template" msgid "Report Template"
msgstr "Plantilla de Informe" msgstr "Plantilla de Informe"
msgctxt "model:ir.action,name:act_html_template_list" msgctxt "model:ir.action,name:act_result_template_list"
msgid "Results Report Templates" msgid "Results Report Templates"
msgstr "Plantillas de Informe de resultados" msgstr "Plantillas de Informe de resultados"
msgctxt "model:ir.action,name:report_result_report" msgctxt "model:ir.action,name:report_result_report_html"
msgid "Results Report" msgid "Results Report"
msgstr "Informe de resultados" msgstr "Informe de resultados"
@ -250,6 +254,10 @@ msgctxt "model:ir.message,text:msg_no_template"
msgid "The report has no template" msgid "The report has no template"
msgstr "El informe no tiene ninguna plantilla" msgstr "El informe no tiene ninguna plantilla"
msgctxt "model:ir.message,text:msg_print_multiple_record"
msgid "Please, select only one record to print"
msgstr "Por favor, seleccione un solo registro para imprimir"
msgctxt "model:ir.message,text:msg_section_pdf" msgctxt "model:ir.message,text:msg_section_pdf"
msgid "Section files must be in PDF format" msgid "Section files must be in PDF format"
msgstr "Los archivos de secciones deben ser PDF" msgstr "Los archivos de secciones deben ser PDF"
@ -258,23 +266,23 @@ msgctxt "model:ir.message,text:msg_yes"
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
msgctxt "model:ir.ui.menu,name:menu_html_template_list" msgctxt "model:ir.ui.menu,name:menu_result_template_list"
msgid "Results Report Templates" msgid "Results Report Templates"
msgstr "Plantillas de Informe" msgstr "Plantillas de Informe"
msgctxt "model:lims.result_report.template,name:" msgctxt "model:lims.report.template,name:"
msgid "Results Report Template" msgid "Report Template"
msgstr "Plantilla de Informe de resultados" msgstr "Plantilla de Informe"
msgctxt "model:lims.result_report.template.section,name:" msgctxt "model:lims.report.template.section,name:"
msgid "Results Report Template Section" msgid "Report Template Section"
msgstr "Sección de Plantilla de Informe de resultados" msgstr "Sección de Plantilla de Informe"
msgctxt "model:lims.result_report.template.translation,name:" msgctxt "model:lims.report.template.translation,name:"
msgid "Results Report Template Translation" msgid "Report Template Translation"
msgstr "Traducción de Plantilla de Informe de resultados" msgstr "Traducción de Plantilla de Informe"
msgctxt "model:lims.result_report.template.trend.chart,name:" msgctxt "model:lims.report.template.trend.chart,name:"
msgid "Results Report Template Trend Chart" msgid "Results Report Template Trend Chart"
msgstr "Gráfico de tendencia de Plantilla de Informe de resultados" msgstr "Gráfico de tendencia de Plantilla de Informe de resultados"
@ -286,39 +294,39 @@ msgctxt "model:lims.results_report.version.detail.trend.chart,name:"
msgid "Results Report Version Detail Trend Chart" 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:" msgctxt "selection:lims.report.template,charts_x_row:"
msgid "1" msgid "1"
msgstr "1" msgstr "1"
msgctxt "selection:lims.result_report.template,charts_x_row:" msgctxt "selection:lims.report.template,charts_x_row:"
msgid "2" msgid "2"
msgstr "2" msgstr "2"
msgctxt "selection:lims.result_report.template,page_orientation:" msgctxt "selection:lims.report.template,page_orientation:"
msgid "Landscape" msgid "Landscape"
msgstr "Horizontal" msgstr "Horizontal"
msgctxt "selection:lims.result_report.template,page_orientation:" msgctxt "selection:lims.report.template,page_orientation:"
msgid "Portrait" msgid "Portrait"
msgstr "Vertical" msgstr "Vertical"
msgctxt "selection:lims.result_report.template,type:" msgctxt "selection:lims.report.template,type:"
msgid "HTML" msgid "HTML"
msgstr "HTML" msgstr "HTML"
msgctxt "selection:lims.result_report.template,type:" msgctxt "selection:lims.report.template,type:"
msgid "HTML - Footer" msgid "HTML - Footer"
msgstr "HTML - Pie de página" msgstr "HTML - Pie de página"
msgctxt "selection:lims.result_report.template,type:" msgctxt "selection:lims.report.template,type:"
msgid "HTML - Header" msgid "HTML - Header"
msgstr "HTML - Encabezado" msgstr "HTML - Encabezado"
msgctxt "selection:lims.result_report.template.section,position:" msgctxt "selection:lims.report.template.section,position:"
msgid "Following" msgid "Following"
msgstr "Siguiente" msgstr "Siguiente"
msgctxt "selection:lims.result_report.template.section,position:" msgctxt "selection:lims.report.template.section,position:"
msgid "Previous" msgid "Previous"
msgstr "Anterior" msgstr "Anterior"
@ -354,7 +362,7 @@ msgctxt "view:lims.analysis:"
msgid "Report" msgid "Report"
msgstr "Informe" msgstr "Informe"
msgctxt "view:lims.result_report.template:" msgctxt "view:lims.report.template:"
msgid "Header and Footer" msgid "Header and Footer"
msgstr "Encabezado y Pie de página" msgstr "Encabezado y Pie de página"

View File

@ -1,6 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<tryton> <tryton>
<data grouped="1"> <data grouped="1">
<record model="ir.message" id="msg_print_multiple_record">
<field name="text">Please, select only one record to print</field>
</record>
<record model="ir.message" id="msg_no_template"> <record model="ir.message" id="msg_no_template">
<field name="text">The report has no template</field> <field name="text">The report has no template</field>
</record> </record>

View File

@ -10,7 +10,7 @@ class Notebook(metaclass=PoolMeta):
__name__ = 'lims.notebook' __name__ = 'lims.notebook'
result_template = fields.Function(fields.Many2One( result_template = fields.Function(fields.Many2One(
'lims.result_report.template', 'Report Template'), 'get_sample_field') 'lims.report.template', 'Report Template'), 'get_sample_field')
resultrange_origin = fields.Function(fields.Many2One('lims.range.type', resultrange_origin = fields.Function(fields.Many2One('lims.range.type',
'Comparison range'), 'get_sample_field') 'Comparison range'), 'get_sample_field')

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Party(metaclass=PoolMeta): class Party(metaclass=PoolMeta):
__name__ = 'party.party' __name__ = 'party.party'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])]) 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])

View File

@ -1,18 +1,6 @@
# This file is part of lims_report_html module for Tryton. # This file is part of lims_report_html module for Tryton.
# The COPYRIGHT file at the top level of this repository contains # The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms. # the full copyright notices and license terms.
import os
import operator
from mimetypes import guess_type as mime_guess_type
from binascii import b2a_base64
from functools import partial
from decimal import Decimal
from datetime import date, datetime
from lxml import html as lxml_html
from base64 import b64encode
from babel.support import Translations as BabelTranslations
from jinja2 import contextfilter, Markup
from jinja2 import Environment, FunctionLoader
from io import BytesIO from io import BytesIO
from PyPDF2 import PdfFileMerger from PyPDF2 import PdfFileMerger
from PyPDF2.utils import PdfReadError from PyPDF2.utils import PdfReadError
@ -23,16 +11,19 @@ from trytond.pyson import Eval, Not, Bool
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.exceptions import UserError from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.tools import file_open from .html_template import LimsReport
from .generator import PdfGenerator
class ResultsReportVersionDetail(metaclass=PoolMeta): class ResultsReportVersionDetail(metaclass=PoolMeta):
__name__ = 'lims.results_report.version.detail' __name__ = 'lims.results_report.version.detail'
template = fields.Many2One('lims.result_report.template', template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])], 'Report Template', domain=[
states={'readonly': Eval('state') != 'draft'}, depends=['state']) ('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
],
states={'readonly': Eval('state') != 'draft'},
depends=['state'])
template_type = fields.Function(fields.Selection([ template_type = fields.Function(fields.Selection([
(None, ''), (None, ''),
('base', 'HTML'), ('base', 'HTML'),
@ -56,9 +47,8 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
('1', '1'), ('1', '1'),
('2', '2'), ('2', '2'),
], 'Charts per Row') ], 'Charts per Row')
comments_html = fields.Function(fields.Text('Comments', comments_plain = fields.Function(fields.Text('Comments', translate=True),
states={'readonly': ~Eval('state').in_(['draft', 'revised'])}, 'get_comments_plain', setter='set_comments_plain')
depends=['state']), 'get_comments', setter='set_comments')
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
@ -71,10 +61,10 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
@classmethod @classmethod
def view_attributes(cls): def view_attributes(cls):
return super().view_attributes() + [ return super().view_attributes() + [
('//page[@id="comments_html"]', 'states', { ('//page[@id="comments"]', 'states', {
'invisible': Not(Bool(Eval('template_type'))), 'invisible': Not(Bool(Eval('template_type'))),
}), }),
('//page[@id="comments"]', 'states', { ('//page[@id="comments_plain"]', 'states', {
'invisible': Eval('template_type') == 'base', 'invisible': Eval('template_type') == 'base',
}), }),
] ]
@ -244,6 +234,13 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
} for s in detail.sections])] } for s in detail.sections])]
return detail_default return detail_default
def get_comments_plain(self, name):
return self.comments
@classmethod
def set_comments_plain(cls, records, name, value):
cls.write(records, {'comments': value})
class ResultsReportVersionDetailSection(ModelSQL, ModelView): class ResultsReportVersionDetailSection(ModelSQL, ModelView):
'Results Report Version Detail Section' 'Results Report Version Detail Section'
@ -426,18 +423,19 @@ class ResultsReportVersionDetailSample(metaclass=PoolMeta):
return content return content
class ResultReport(metaclass=PoolMeta): class ResultReport(LimsReport, metaclass=PoolMeta):
__name__ = 'lims.result_report' __name__ = 'lims.result_report'
@classmethod @classmethod
def execute(cls, ids, data): def execute(cls, ids, data):
if len(ids) > 1:
raise UserError(gettext('lims.msg_multiple_reports'))
pool = Pool() pool = Pool()
ResultsDetail = pool.get('lims.results_report.version.detail') ResultsDetail = pool.get('lims.results_report.version.detail')
CachedReport = pool.get('lims.results_report.cached_report') CachedReport = pool.get('lims.results_report.cached_report')
if len(ids) > 1:
raise UserError(gettext(
'lims_report_html.msg_print_multiple_record'))
results_report = ResultsDetail(ids[0]) results_report = ResultsDetail(ids[0])
if results_report.state == 'annulled': if results_report.state == 'annulled':
raise UserError(gettext('lims.msg_annulled_report')) raise UserError(gettext('lims.msg_annulled_report'))
@ -449,12 +447,12 @@ class ResultReport(metaclass=PoolMeta):
template = results_report.template template = results_report.template
if template and template.type == 'base': # HTML if template and template.type == 'base': # HTML
result = cls.execute_html_results_report(ids, current_data) result = cls.execute_html_lims_report(ids, current_data)
else: else:
current_data['action_id'] = None current_data['action_id'] = None
if template and template.report: if template and template.report:
current_data['action_id'] = template.report.id current_data['action_id'] = template.report.id
result = cls.execute_custom_results_report(ids, current_data) result = cls.execute_custom_lims_report(ids, current_data)
cached_reports = CachedReport.search([ cached_reports = CachedReport.search([
('version_detail', '=', results_report.id), ('version_detail', '=', results_report.id),
@ -489,338 +487,15 @@ class ResultReport(metaclass=PoolMeta):
return result return result
@classmethod
def execute_custom_results_report(cls, ids, data):
pool = Pool()
ActionReport = pool.get('ir.action.report')
cls.check_access()
action_id = data.get('action_id')
if action_id is None:
action_reports = ActionReport.search([
('report_name', '=', cls.__name__),
('template_extension', '!=', 'results'),
])
assert action_reports, '%s not found' % cls
action = action_reports[0]
else:
action = ActionReport(action_id)
records = []
model = action.model or data.get('model')
if model:
records = cls._get_records(ids, model, data)
oext, content = cls._execute(records, [], data, action)
if not isinstance(content, str):
content = bytearray(content) if bytes == str else bytes(content)
return (oext, content, action.direct_print, action.name)
@classmethod
def execute_html_results_report(cls, ids, data):
pool = Pool()
ActionReport = pool.get('ir.action.report')
cls.check_access()
action_reports = ActionReport.search([
('report_name', '=', cls.__name__),
('template_extension', '=', 'results'),
])
assert action_reports, '%s not found' % cls
action = action_reports[0]
records = []
model = action.model or data.get('model')
if model:
records = cls._get_records(ids, model, data)
oext, content = cls._execute_html_results_report(records, data, action)
if not isinstance(content, str):
content = bytearray(content) if bytes == str else bytes(content)
return (oext, content, action.direct_print, action.name)
@classmethod
def _execute_html_results_report(cls, records, data, action):
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)
page_orientation = (record.template and
record.template.page_orientation or 'portrait')
document = PdfGenerator(content,
header_html=header, footer_html=footer,
side_margin=1, extra_vertical_margin=30,
stylesheets=stylesheets,
page_orientation=page_orientation).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):
ResultsDetail = Pool().get('lims.results_report.version.detail')
template_id, content, header, footer = None, None, None, None
detail = ResultsDetail(detail_id)
if detail.template:
template_id = detail.template
content = '<body>%s</body>' % detail.template.content
header = (detail.template.header and
'<header id="header">%s</header>' %
detail.template.header.content)
footer = (detail.template.footer and
'<footer id="footer">%s</footer>' %
detail.template.footer.content)
if not content:
content = (action.report_content and
action.report_content.decode('utf-8'))
if not content:
raise UserError(gettext('lims_report_html.msg_no_template'))
return template_id, content, header, footer
@classmethod
def render_results_report_template(cls, action, template_string,
record=None, records=None, data=None):
User = Pool().get('res.user')
user = User(Transaction().user)
if data and data.get('alt_lang'):
locale = data['alt_lang']
elif user.language:
locale = user.language.code
else:
locale = Transaction().language
with Transaction().set_context(locale=locale):
env = cls.get_results_report_environment()
report_template = env.from_string(template_string)
context = cls.get_context(records, [], data)
context.update({
'report': action,
'get_image': cls.get_image,
'operation': cls.operation,
})
res = report_template.render(**context)
res = cls.parse_images(res)
# print('TEMPLATE:\n', res)
return res
@classmethod
def get_results_report_environment(cls):
extensions = ['jinja2.ext.i18n', 'jinja2.ext.autoescape',
'jinja2.ext.with_', 'jinja2.ext.loopcontrols', 'jinja2.ext.do']
env = Environment(extensions=extensions,
loader=FunctionLoader(lambda name: ''))
env.filters.update(cls.get_results_report_filters())
locale = Transaction().context.get('locale').split('_')[0]
translations = TemplateTranslations(locale)
env.install_gettext_translations(translations)
return env
@classmethod
def get_results_report_filters(cls):
Lang = Pool().get('ir.lang')
def module_path(name):
module, path = name.split('/', 1)
with file_open(os.path.join(module, path)) as f:
return 'file://%s' % f.name
def render(value, digits=2, lang=None, filename=None):
if value is None or value == '':
return ''
if isinstance(value, (float, Decimal)):
return lang.format('%.*f', (digits, value), grouping=True)
if isinstance(value, int):
return lang.format('%d', value, grouping=True)
if isinstance(value, bool):
if value:
return gettext('lims_report_html.msg_yes')
return gettext('lims_report_html.msg_no')
if hasattr(value, 'rec_name'):
return value.rec_name
if isinstance(value, date):
return lang.strftime(value)
if isinstance(value, datetime):
return '%s %s' % (lang.strftime(value),
value.strftime('%H:%M:%S'))
if isinstance(value, str):
return value.replace('\n', '<br/>')
if isinstance(value, bytes):
b64_value = b2a_base64(value).decode('ascii')
mimetype = 'image/png'
if filename:
mimetype = mime_guess_type(filename)[0]
return ('data:%s;base64,%s' % (mimetype, b64_value)).strip()
return value
@contextfilter
def subrender(context, value, subobj=None):
if value is None or value == '':
return ''
_template = context.eval_ctx.environment.from_string(value)
if subobj:
new_context = {'subobj': subobj}
new_context.update(context)
else:
new_context = context
result = _template.render(**new_context)
if context.eval_ctx.autoescape:
result = Markup(result)
return result
locale = Transaction().context.get('locale').split('_')[0]
lang, = Lang.search([('code', '=', locale or 'en')])
return {
'modulepath': module_path,
'render': partial(render, lang=lang),
'subrender': subrender,
}
@classmethod
def parse_images(cls, template_string):
Attachment = Pool().get('ir.attachment')
root = lxml_html.fromstring(template_string)
for elem in root.iter('img'):
# get image from attachments
if 'id' in elem.attrib:
img = Attachment.search([('id', '=', int(elem.attrib['id']))])
if img:
elem.attrib['src'] = cls.get_image(img[0].data)
# get image from TinyMCE widget
elif 'data-mce-src' in elem.attrib:
elem.attrib['src'] = elem.attrib['data-mce-src']
del elem.attrib['data-mce-src']
# set width and height in style attribute
style = elem.attrib.get('style', '')
if 'width' in elem.attrib:
style += ' width: %spx;' % str(elem.attrib['width'])
if 'height' in elem.attrib:
style += ' height: %spx;' % str(elem.attrib['height'])
elem.attrib['style'] = style
return lxml_html.tostring(root).decode()
@classmethod
def get_image(cls, image):
if not image:
return ''
b64_image = b64encode(image).decode()
return 'data:image/png;base64,%s' % b64_image
@classmethod
def operation(cls, function, value1, value2):
return getattr(operator, function)(value1, value2)
@classmethod
def parse_stylesheets(cls, template_string):
Attachment = Pool().get('ir.attachment')
root = lxml_html.fromstring(template_string)
res = []
# get stylesheets from attachments
elems = root.xpath("//div[@id='tryton_styles_container']/div")
for elem in elems:
css = Attachment.search([('id', '=', int(elem.attrib['id']))])
if not css:
continue
res.append(css[0].data)
return res
class TemplateTranslations:
def __init__(self, lang='en'):
self.cache = {}
self.env = None
self.current = None
self.language = lang
self.template = None
self.set_language(lang)
def set_language(self, lang='en'):
self.language = lang
if lang in self.cache:
self.current = self.cache[lang]
return
context = Transaction().context
if context.get('default_translations'):
default_translations = context['default_translations']
if os.path.isdir(default_translations):
self.current = BabelTranslations.load(
dirname=default_translations, locales=[lang])
self.cache[lang] = self.current
else:
self.template = context.get('template', -1)
def ugettext(self, message):
ReportTemplate = Pool().get('lims.result_report.template')
if self.current:
return self.current.ugettext(message)
elif self.template:
return ReportTemplate.gettext(self.template, message,
self.language)
return message
def ngettext(self, singular, plural, n):
ReportTemplate = Pool().get('lims.result_report.template')
if self.current:
return self.current.ugettext(singular, plural, n)
elif self.template:
return ReportTemplate.gettext(self.template, singular,
self.language)
return singular
class GenerateReportStart(metaclass=PoolMeta): class GenerateReportStart(metaclass=PoolMeta):
__name__ = 'lims.notebook.generate_results_report.start' __name__ = 'lims.notebook.generate_results_report.start'
template = fields.Many2One('lims.result_report.template', template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])], 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
],
states={'readonly': Bool(Eval('report'))}, states={'readonly': Bool(Eval('report'))},
depends=['report']) depends=['report'])

View File

@ -43,13 +43,13 @@
<!-- Results Report --> <!-- Results Report -->
<record model="ir.action.report" id="report_result_report"> <record model="ir.action.report" id="report_result_report_html">
<field name="name">Results Report</field> <field name="name">Results Report</field>
<field name="model">lims.results_report.version.detail</field> <field name="model">lims.results_report.version.detail</field>
<field name="report_name">lims.result_report</field> <field name="report_name">lims.result_report</field>
<field name="report">lims_report_html/report/results_report.html</field> <field name="report">lims_report_html/report/results_report.html</field>
<field name="extension">pdf</field> <field name="extension">pdf</field>
<field name="template_extension">results</field> <field name="template_extension">lims</field>
</record> </record>
<!-- Wizard Generate Results Report --> <!-- Wizard Generate Results Report -->

View File

@ -10,7 +10,7 @@ class Fraction(metaclass=PoolMeta):
__name__ = 'lims.fraction' __name__ = 'lims.fraction'
result_template = fields.Function(fields.Many2One( result_template = fields.Function(fields.Many2One(
'lims.result_report.template', 'Report Template'), 'get_sample_field') 'lims.report.template', 'Report Template'), 'get_sample_field')
def _order_sample_field(name): def _order_sample_field(name):
def order_field(tables): def order_field(tables):
@ -32,8 +32,11 @@ class Fraction(metaclass=PoolMeta):
class Sample(metaclass=PoolMeta): class Sample(metaclass=PoolMeta):
__name__ = 'lims.sample' __name__ = 'lims.sample'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])]) 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range', resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')]) domain=[('use', '=', 'result_range')])
@ -47,8 +50,11 @@ class Sample(metaclass=PoolMeta):
class CreateSampleStart(metaclass=PoolMeta): class CreateSampleStart(metaclass=PoolMeta):
__name__ = 'lims.create_sample.start' __name__ = 'lims.create_sample.start'
result_template = fields.Many2One('lims.result_report.template', result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])]) 'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range', resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')]) domain=[('use', '=', 'result_range')])

View File

@ -3,7 +3,8 @@
<xpath expr="/form/notebook/page[@id='times']" position="after"> <xpath expr="/form/notebook/page[@id='times']" position="after">
<page id="report" string="Report"> <page id="report" string="Report">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</page> </page>
</xpath> </xpath>
</data> </data>

View File

@ -4,6 +4,7 @@
expr="/form/notebook/page[@id='general']/field[@name='results_report_language']" expr="/form/notebook/page[@id='general']/field[@name='results_report_language']"
position="after"> position="after">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</xpath> </xpath>
</data> </data>

View File

@ -4,7 +4,8 @@
expr="/form/notebook/page[@id='report']/separator[@id='report_comments']" expr="/form/notebook/page[@id='report']/separator[@id='report_comments']"
position="before"> position="before">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
<label name="resultrange_origin"/> <label name="resultrange_origin"/>
<field name="resultrange_origin"/> <field name="resultrange_origin"/>
</xpath> </xpath>

View File

@ -2,6 +2,7 @@
<data> <data>
<xpath expr="/form/field[@name='headquarters']" position="after"> <xpath expr="/form/field[@name='headquarters']" position="after">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</xpath> </xpath>
</data> </data>

View File

@ -2,6 +2,7 @@
<data> <data>
<xpath expr="/form/group[@id='append_samples']" position="after"> <xpath expr="/form/group[@id='append_samples']" position="after">
<label name="template"/> <label name="template"/>
<field name="template"/> <field name="template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</xpath> </xpath>
</data> </data>

View File

@ -4,6 +4,7 @@
expr="/form/notebook/page[@id='lims']/field[@name='report_language']" expr="/form/notebook/page[@id='lims']/field[@name='report_language']"
position="after"> position="after">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</xpath> </xpath>
</data> </data>

View File

@ -6,19 +6,21 @@
<field name="type"/> <field name="type"/>
<label name="report"/> <label name="report"/>
<field name="report"/> <field name="report"/>
<label name="resultrange_origin"/>
<field name="resultrange_origin"/>
<label name="page_orientation"/> <label name="page_orientation"/>
<field name="page_orientation"/> <field name="page_orientation"/>
<label name="resultrange_origin"/>
<field name="resultrange_origin"/>
<notebook> <notebook>
<page name="content"> <page name="content">
<field name="content" colspan="4" widget="html"/> <field name="content" colspan="4" widget="html"/>
</page> </page>
<page id="header_footer" string="Header and Footer"> <page id="header_footer" string="Header and Footer">
<label name="header"/> <label name="header"/>
<field name="header"/> <field name="header"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
<label name="footer"/> <label name="footer"/>
<field name="footer"/> <field name="footer"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</page> </page>
<page name="translations"> <page name="translations">
<field name="translations" colspan="4"/> <field name="translations" colspan="4"/>

View File

@ -12,15 +12,16 @@
<xpath expr="/form/field[@name='report_result_type']" position="replace"/> <xpath expr="/form/field[@name='report_result_type']" position="replace"/>
<xpath expr="/form/label[@name='resultrange_origin']" position="before"> <xpath expr="/form/label[@name='resultrange_origin']" position="before">
<label name="template"/> <label name="template"/>
<field name="template"/> <field name="template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
</xpath> </xpath>
<xpath expr="/form/notebook/page[@name='comments']" position="replace"> <xpath expr="/form/notebook/page[@name='comments']" position="replace">
<page id="comments" string="Comments"> <page id="comments_plain" string="Comments">
<field name="comments" colspan="4" widget="text"/> <field name="comments_plain" colspan="4" widget="text"/>
<field name="template_type" colspan="4" invisible="1"/> <field name="template_type" colspan="4" invisible="1"/>
</page> </page>
<page id="comments_html" string="Comments"> <page id="comments" string="Comments">
<field name="comments_html" colspan="4" widget="html"/> <field name="comments" colspan="4" widget="html"/>
<field name="template_type" colspan="4" invisible="1"/> <field name="template_type" colspan="4" invisible="1"/>
</page> </page>
<page name="sections"> <page name="sections">

View File

@ -4,7 +4,8 @@
expr="/form/notebook/page[@id='report']/separator[@id='report_comments']" expr="/form/notebook/page[@id='report']/separator[@id='report_comments']"
position="before"> position="before">
<label name="result_template"/> <label name="result_template"/>
<field name="result_template"/> <field name="result_template"
view_ids="lims_report_html.result_template_view_list,lims_report_html.result_template_view_form"/>
<label name="resultrange_origin"/> <label name="resultrange_origin"/>
<field name="resultrange_origin"/> <field name="resultrange_origin"/>
</xpath> </xpath>