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.DiagnosisTemplate,
html_template.DiagnosisTemplateState,
html_template.ReportTemplate,
html_template.ResultsReportTemplate,
sample.Fraction,
sample.Sample,
sample.CreateSampleStart,

View File

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

View File

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

View File

@ -110,11 +110,11 @@ msgctxt "field:lims.product.type,diagnostician:"
msgid "Diagnostician"
msgstr "Diagnosticador"
msgctxt "field:lims.result_report.template,diagnosis_length:"
msgctxt "field:lims.report.template,diagnosis_length:"
msgid "Diagnosis Length"
msgstr "Longitud del diagnóstico"
msgctxt "field:lims.result_report.template,diagnosis_template:"
msgctxt "field:lims.report.template,diagnosis_template:"
msgid "Diagnosis Template"
msgstr "Plantilla de Diagnóstico"
@ -267,7 +267,7 @@ msgctxt "help:lims.notebook.repeat_analysis.start,notify_acceptance:"
msgid "Notify when analysis is ready"
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"
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):
pool = Pool()
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,
generate_report_form)
@ -476,7 +476,8 @@ class SamplesComparatorLine(ModelSQL, ModelView):
result[name] = {}
if name == 'result':
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':
for l in lines:
result[name][l.id] = (
@ -514,7 +515,7 @@ class SamplesComparatorLine(ModelSQL, ModelView):
])
if not notebook_line:
return None
return notebook_line[0].formated_result
return notebook_line[0].get_formated_result()
class Cron(metaclass=PoolMeta):
@ -533,8 +534,8 @@ class ResultReport(metaclass=PoolMeta):
__name__ = 'lims.result_report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
def get_context(cls, records, data):
report_context = super().get_context(records, data)
report_context['state_image'] = cls.get_state_image
return report_context

View File

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

View File

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

View File

@ -3,6 +3,7 @@
# the full copyright notices and license terms.
from trytond.pool import PoolMeta
from trytond.transaction import Transaction
class ActionReport(metaclass=PoolMeta):
@ -11,13 +12,21 @@ class ActionReport(metaclass=PoolMeta):
@classmethod
def __setup__(cls):
super().__setup__()
results_option = ('results', 'Results Report')
if results_option not in cls.template_extension.selection:
cls.template_extension.selection.append(results_option)
lims_option = ('lims', 'Lims Report')
if lims_option not in cls.template_extension.selection:
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):
__name__ = 'ir.translation.set'
def extract_report_results(self, content):
def extract_report_lims(self, content):
return []

View File

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

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Configuration(metaclass=PoolMeta):
__name__ = 'lims.configuration'
result_template = fields.Many2One('lims.result_report.template',
'Default Report Template', domain=[('type', 'in', [None, 'base'])])
result_template = fields.Many2One('lims.report.template',
'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.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
import os
import operator
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.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.report import Report
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.cache import Cache
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.tools import file_open
from trytond import backend
from .generator import PdfGenerator
class ReportTemplate(ModelSQL, ModelView):
'Results Report Template'
__name__ = 'lims.result_report.template'
_history = True
'Report Template'
__name__ = 'lims.report.template'
report_name = fields.Char('Internal 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([
(None, ''),
('base', 'HTML'),
('header', 'HTML - Header'),
('footer', 'HTML - Footer'),
], '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',
states={'required': Bool(Eval('type'))}, depends=['type'])
header = fields.Many2One('lims.result_report.template', 'Header',
domain=[('type', '=', 'header')])
footer = fields.Many2One('lims.result_report.template', 'Footer',
domain=[('type', '=', 'footer')])
translations = fields.One2Many('lims.result_report.template.translation',
header = fields.Many2One('lims.report.template', 'Header',
domain=[
('report_name', '=', Eval('report_name')),
('type', '=', 'header'),
],
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')
_translation_cache = Cache('lims.result_report.template.translation',
_translation_cache = Cache('lims.report.template.translation',
size_limit=10240, context=False)
sections = fields.One2Many('lims.result_report.template.section',
sections = fields.One2Many('lims.report.template.section',
'template', 'Sections')
previous_sections = fields.Function(fields.One2Many(
'lims.result_report.template.section', 'template',
'lims.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',
'lims.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([
('1', '1'),
('2', '2'),
], 'Charts per Row')
page_orientation = fields.Selection([
('portrait', 'Portrait'),
('landscape', 'Landscape'),
], 'Page orientation', sort=False)
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')])
], 'Page orientation', sort=False,
states={'invisible': Eval('type') != 'base'},
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
def default_type():
return None
@staticmethod
def default_charts_x_row():
return '1'
@staticmethod
def default_page_orientation():
return 'portrait'
@ -80,7 +125,7 @@ class ReportTemplate(ModelSQL, ModelView):
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//page[@id="content"]', 'states', {
('//page[@name="content"]', 'states', {
'invisible': ~Bool(Eval('type')),
}),
('//page[@id="header_footer"]', 'states', {
@ -92,15 +137,12 @@ class ReportTemplate(ModelSQL, ModelView):
('//page[@name="sections"]', 'states', {
'invisible': Eval('type') != 'base',
}),
('//page[@name="trend_charts"]', 'states', {
'invisible': Eval('type') != 'base',
}),
]
@classmethod
def gettext(cls, *args, **variables):
ReportTemplateTranslation = Pool().get(
'lims.result_report.template.translation')
'lims.report.template.translation')
template, src, lang = args
key = (template, src, lang)
text = cls._translation_cache.get(key)
@ -143,17 +185,40 @@ class ReportTemplate(ModelSQL, ModelView):
class ReportTemplateTranslation(ModelSQL, ModelView):
'Results Report Template Translation'
__name__ = 'lims.result_report.template.translation'
'Report Template Translation'
__name__ = 'lims.report.template.translation'
_order_name = 'src'
template = fields.Many2One('lims.result_report.template', 'Template',
template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True)
src = fields.Text('Source', required=True)
value = fields.Text('Translation Value', required=True)
lang = fields.Selection('get_language', string='Language', required=True)
_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
def default_lang():
@ -171,29 +236,29 @@ class ReportTemplateTranslation(ModelSQL, ModelView):
@classmethod
def create(cls, vlist):
Template = Pool().get('lims.result_report.template')
Template = Pool().get('lims.report.template')
Template._translation_cache.clear()
return super().create(vlist)
@classmethod
def write(cls, *args):
Template = Pool().get('lims.result_report.template')
Template = Pool().get('lims.report.template')
Template._translation_cache.clear()
return super().write(*args)
@classmethod
def delete(cls, translations):
Template = Pool().get('lims.result_report.template')
Template = Pool().get('lims.report.template')
Template._translation_cache.clear()
return super().delete(translations)
class ReportTemplateSection(ModelSQL, ModelView):
'Results Report Template Section'
__name__ = 'lims.result_report.template.section'
'Report Template Section'
__name__ = 'lims.report.template.section'
_order_name = 'order'
template = fields.Many2One('lims.result_report.template', 'Template',
template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True)
name = fields.Char('Name', required=True)
data = fields.Binary('File', filename='name', required=True,
@ -210,6 +275,29 @@ class ReportTemplateSection(ModelSQL, ModelView):
super().__setup__()
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
def validate(cls, sections):
super().validate(sections)
@ -222,13 +310,398 @@ class ReportTemplateSection(ModelSQL, ModelView):
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'
__name__ = 'lims.result_report.template.trend.chart'
__name__ = 'lims.report.template.trend.chart'
_order_name = 'order'
template = fields.Many2One('lims.result_report.template', 'Template',
template = fields.Many2One('lims.report.template', 'Template',
ondelete='CASCADE', select=True, required=True)
chart = fields.Many2One('lims.trend.chart', 'Trend Chart',
required=True, domain=[('active', '=', True)])
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>
<data>
<!-- Results Report Template -->
<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 -->
<!-- Report Template Translation -->
<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="name">template_translation_form</field>
</record>
<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="name">template_translation_list</field>
</record>
<!-- Results Report Template Section -->
<!-- Report Template Section -->
<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="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="model">lims.report.template.section</field>
<field name="type">tree</field>
<field name="name">template_section_list</field>
</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 -->
<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="name">template_trend_chart_form</field>
</record>
<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="name">template_trend_chart_list</field>
</record>

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Laboratory(metaclass=PoolMeta):
__name__ = 'lims.laboratory'
result_template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])])
result_template = fields.Many2One('lims.report.template',
'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"
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"
msgstr "Gráficos por fila"
msgctxt "field:lims.result_report.template,content:"
msgctxt "field:lims.report.template,content:"
msgid "Content"
msgstr "Contenido"
msgctxt "field:lims.result_report.template,following_sections:"
msgctxt "field:lims.report.template,following_sections:"
msgid "Following Sections"
msgstr "Secciones siguientes"
msgctxt "field:lims.result_report.template,footer:"
msgctxt "field:lims.report.template,footer:"
msgid "Footer"
msgstr "Pie de página"
msgctxt "field:lims.result_report.template,header:"
msgctxt "field:lims.report.template,header:"
msgid "Header"
msgstr "Encabezado"
msgctxt "field:lims.result_report.template,name:"
msgctxt "field:lims.report.template,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:lims.result_report.template,page_orientation:"
msgctxt "field:lims.report.template,page_orientation:"
msgid "Page orientation"
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"
msgstr "Secciones anteriores"
msgctxt "field:lims.result_report.template,report:"
msgctxt "field:lims.report.template,report:"
msgid "Report"
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"
msgstr "Rango de comparación"
msgctxt "field:lims.result_report.template,sections:"
msgctxt "field:lims.report.template,sections:"
msgid "Sections"
msgstr "Secciones"
msgctxt "field:lims.result_report.template,translations:"
msgctxt "field:lims.report.template,translations:"
msgid "Translations"
msgstr "Traducciones"
msgctxt "field:lims.result_report.template,trend_charts:"
msgctxt "field:lims.report.template,trend_charts:"
msgid "Trend Charts"
msgstr "Gráficos de tendencia"
msgctxt "field:lims.result_report.template,type:"
msgctxt "field:lims.report.template,type:"
msgid "Type"
msgstr "Tipo"
msgctxt "field:lims.result_report.template.section,data:"
msgctxt "field:lims.report.template.section,data:"
msgid "File"
msgstr "Archivo"
msgctxt "field:lims.result_report.template.section,data_id:"
msgctxt "field:lims.report.template.section,data_id:"
msgid "File ID"
msgstr "ID Archivo"
msgctxt "field:lims.result_report.template.section,name:"
msgctxt "field:lims.report.template.section,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:lims.result_report.template.section,order:"
msgctxt "field:lims.report.template.section,order:"
msgid "Order"
msgstr "Orden"
msgctxt "field:lims.result_report.template.section,position:"
msgctxt "field:lims.report.template.section,position:"
msgid "Position"
msgstr "Posición"
msgctxt "field:lims.result_report.template.section,template:"
msgctxt "field:lims.report.template.section,template:"
msgid "Template"
msgstr "Plantilla"
msgctxt "field:lims.result_report.template.translation,lang:"
msgctxt "field:lims.report.template.translation,lang:"
msgid "Language"
msgstr "Idioma"
msgctxt "field:lims.result_report.template.translation,src:"
msgctxt "field:lims.report.template.translation,src:"
msgid "Source"
msgstr "Original"
msgctxt "field:lims.result_report.template.translation,template:"
msgctxt "field:lims.report.template.translation,template:"
msgid "Template"
msgstr "Plantilla"
msgctxt "field:lims.result_report.template.translation,value:"
msgctxt "field:lims.report.template.translation,value:"
msgid "Translation Value"
msgstr "Traducción"
msgctxt "field:lims.result_report.template.trend.chart,chart:"
msgctxt "field:lims.report.template.trend.chart,chart:"
msgid "Trend Chart"
msgstr "Gráfico de tendencia"
msgctxt "field:lims.result_report.template.trend.chart,order:"
msgctxt "field:lims.report.template.trend.chart,order:"
msgid "Order"
msgstr "Orden"
msgctxt "field:lims.result_report.template.trend.chart,template:"
msgctxt "field:lims.report.template.trend.chart,template:"
msgid "Template"
msgstr "Plantilla"
@ -150,7 +154,7 @@ 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,comments_html:"
msgctxt "field:lims.results_report.version.detail,comments_plain:"
msgid "Comments"
msgstr "Observaciones"
@ -234,11 +238,11 @@ msgctxt "field:party.party,result_template:"
msgid "Report Template"
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"
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"
msgstr "Informe de resultados"
@ -250,6 +254,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_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"
msgid "Section files must be in PDF format"
msgstr "Los archivos de secciones deben ser PDF"
@ -258,23 +266,23 @@ msgctxt "model:ir.message,text:msg_yes"
msgid "Yes"
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"
msgstr "Plantillas de Informe"
msgctxt "model:lims.result_report.template,name:"
msgid "Results Report Template"
msgstr "Plantilla de Informe de resultados"
msgctxt "model:lims.report.template,name:"
msgid "Report Template"
msgstr "Plantilla de Informe"
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.report.template.section,name:"
msgid "Report Template Section"
msgstr "Sección de Plantilla de Informe"
msgctxt "model:lims.result_report.template.translation,name:"
msgid "Results Report Template Translation"
msgstr "Traducción de Plantilla de Informe de resultados"
msgctxt "model:lims.report.template.translation,name:"
msgid "Report Template Translation"
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"
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"
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"
msgstr "1"
msgctxt "selection:lims.result_report.template,charts_x_row:"
msgctxt "selection:lims.report.template,charts_x_row:"
msgid "2"
msgstr "2"
msgctxt "selection:lims.result_report.template,page_orientation:"
msgctxt "selection:lims.report.template,page_orientation:"
msgid "Landscape"
msgstr "Horizontal"
msgctxt "selection:lims.result_report.template,page_orientation:"
msgctxt "selection:lims.report.template,page_orientation:"
msgid "Portrait"
msgstr "Vertical"
msgctxt "selection:lims.result_report.template,type:"
msgctxt "selection:lims.report.template,type:"
msgid "HTML"
msgstr "HTML"
msgctxt "selection:lims.result_report.template,type:"
msgctxt "selection:lims.report.template,type:"
msgid "HTML - Footer"
msgstr "HTML - Pie de página"
msgctxt "selection:lims.result_report.template,type:"
msgctxt "selection:lims.report.template,type:"
msgid "HTML - Header"
msgstr "HTML - Encabezado"
msgctxt "selection:lims.result_report.template.section,position:"
msgctxt "selection:lims.report.template.section,position:"
msgid "Following"
msgstr "Siguiente"
msgctxt "selection:lims.result_report.template.section,position:"
msgctxt "selection:lims.report.template.section,position:"
msgid "Previous"
msgstr "Anterior"
@ -354,7 +362,7 @@ msgctxt "view:lims.analysis:"
msgid "Report"
msgstr "Informe"
msgctxt "view:lims.result_report.template:"
msgctxt "view:lims.report.template:"
msgid "Header and Footer"
msgstr "Encabezado y Pie de página"

View File

@ -1,6 +1,9 @@
<?xml version="1.0"?>
<tryton>
<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">
<field name="text">The report has no template</field>
</record>

View File

@ -10,7 +10,7 @@ class Notebook(metaclass=PoolMeta):
__name__ = 'lims.notebook'
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',
'Comparison range'), 'get_sample_field')

View File

@ -9,5 +9,8 @@ from trytond.pool import PoolMeta
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
result_template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])])
result_template = fields.Many2One('lims.report.template',
'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.
# The COPYRIGHT file at the top level of this repository contains
# 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 PyPDF2 import PdfFileMerger
from PyPDF2.utils import PdfReadError
@ -23,16 +11,19 @@ from trytond.pyson import Eval, Not, Bool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.tools import file_open
from .generator import PdfGenerator
from .html_template import LimsReport
class ResultsReportVersionDetail(metaclass=PoolMeta):
__name__ = 'lims.results_report.version.detail'
template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])],
states={'readonly': Eval('state') != 'draft'}, depends=['state'])
template = fields.Many2One('lims.report.template',
'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
],
states={'readonly': Eval('state') != 'draft'},
depends=['state'])
template_type = fields.Function(fields.Selection([
(None, ''),
('base', 'HTML'),
@ -56,9 +47,8 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
('1', '1'),
('2', '2'),
], 'Charts per Row')
comments_html = fields.Function(fields.Text('Comments',
states={'readonly': ~Eval('state').in_(['draft', 'revised'])},
depends=['state']), 'get_comments', setter='set_comments')
comments_plain = fields.Function(fields.Text('Comments', translate=True),
'get_comments_plain', setter='set_comments_plain')
@classmethod
def __setup__(cls):
@ -71,10 +61,10 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//page[@id="comments_html"]', 'states', {
('//page[@id="comments"]', 'states', {
'invisible': Not(Bool(Eval('template_type'))),
}),
('//page[@id="comments"]', 'states', {
('//page[@id="comments_plain"]', 'states', {
'invisible': Eval('template_type') == 'base',
}),
]
@ -244,6 +234,13 @@ class ResultsReportVersionDetail(metaclass=PoolMeta):
} for s in detail.sections])]
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):
'Results Report Version Detail Section'
@ -426,18 +423,19 @@ class ResultsReportVersionDetailSample(metaclass=PoolMeta):
return content
class ResultReport(metaclass=PoolMeta):
class ResultReport(LimsReport, metaclass=PoolMeta):
__name__ = 'lims.result_report'
@classmethod
def execute(cls, ids, data):
if len(ids) > 1:
raise UserError(gettext('lims.msg_multiple_reports'))
pool = Pool()
ResultsDetail = pool.get('lims.results_report.version.detail')
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])
if results_report.state == 'annulled':
raise UserError(gettext('lims.msg_annulled_report'))
@ -449,12 +447,12 @@ class ResultReport(metaclass=PoolMeta):
template = results_report.template
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:
current_data['action_id'] = None
if template and template.report:
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([
('version_detail', '=', results_report.id),
@ -489,338 +487,15 @@ class ResultReport(metaclass=PoolMeta):
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):
__name__ = 'lims.notebook.generate_results_report.start'
template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])],
template = fields.Many2One('lims.report.template',
'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
],
states={'readonly': Bool(Eval('report'))},
depends=['report'])

View File

@ -43,13 +43,13 @@
<!-- 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="model">lims.results_report.version.detail</field>
<field name="report_name">lims.result_report</field>
<field name="report">lims_report_html/report/results_report.html</field>
<field name="extension">pdf</field>
<field name="template_extension">results</field>
<field name="template_extension">lims</field>
</record>
<!-- Wizard Generate Results Report -->

View File

@ -10,7 +10,7 @@ class Fraction(metaclass=PoolMeta):
__name__ = 'lims.fraction'
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_field(tables):
@ -32,8 +32,11 @@ class Fraction(metaclass=PoolMeta):
class Sample(metaclass=PoolMeta):
__name__ = 'lims.sample'
result_template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])])
result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')])
@ -47,8 +50,11 @@ class Sample(metaclass=PoolMeta):
class CreateSampleStart(metaclass=PoolMeta):
__name__ = 'lims.create_sample.start'
result_template = fields.Many2One('lims.result_report.template',
'Report Template', domain=[('type', 'in', [None, 'base'])])
result_template = fields.Many2One('lims.report.template',
'Report Template', domain=[
('report_name', '=', 'lims.result_report'),
('type', 'in', [None, 'base']),
])
resultrange_origin = fields.Many2One('lims.range.type', 'Comparison range',
domain=[('use', '=', 'result_range')])

View File

@ -3,7 +3,8 @@
<xpath expr="/form/notebook/page[@id='times']" position="after">
<page id="report" string="Report">
<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>
</xpath>
</data>

View File

@ -4,6 +4,7 @@
expr="/form/notebook/page[@id='general']/field[@name='results_report_language']"
position="after">
<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>
</data>

View File

@ -4,7 +4,8 @@
expr="/form/notebook/page[@id='report']/separator[@id='report_comments']"
position="before">
<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"/>
<field name="resultrange_origin"/>
</xpath>

View File

@ -2,6 +2,7 @@
<data>
<xpath expr="/form/field[@name='headquarters']" position="after">
<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>
</data>

View File

@ -2,6 +2,7 @@
<data>
<xpath expr="/form/group[@id='append_samples']" position="after">
<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>
</data>

View File

@ -4,6 +4,7 @@
expr="/form/notebook/page[@id='lims']/field[@name='report_language']"
position="after">
<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>
</data>

View File

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

View File

@ -12,15 +12,16 @@
<xpath expr="/form/field[@name='report_result_type']" position="replace"/>
<xpath expr="/form/label[@name='resultrange_origin']" position="before">
<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 expr="/form/notebook/page[@name='comments']" position="replace">
<page id="comments" string="Comments">
<field name="comments" colspan="4" widget="text"/>
<page id="comments_plain" string="Comments">
<field name="comments_plain" colspan="4" widget="text"/>
<field name="template_type" colspan="4" invisible="1"/>
</page>
<page id="comments_html" string="Comments">
<field name="comments_html" colspan="4" widget="html"/>
<page id="comments" string="Comments">
<field name="comments" colspan="4" widget="html"/>
<field name="template_type" colspan="4" invisible="1"/>
</page>
<page name="sections">

View File

@ -4,7 +4,8 @@
expr="/form/notebook/page[@id='report']/separator[@id='report_comments']"
position="before">
<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"/>
<field name="resultrange_origin"/>
</xpath>