2020-05-04 18:18:27 +02:00
|
|
|
# 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.
|
2022-05-07 22:45:37 +02:00
|
|
|
import os
|
|
|
|
import operator
|
2020-07-16 01:40:19 +02:00
|
|
|
from io import BytesIO
|
2022-05-07 22:45:37 +02:00
|
|
|
from decimal import Decimal
|
|
|
|
from datetime import date, datetime
|
|
|
|
from binascii import b2a_base64
|
|
|
|
from functools import partial
|
2020-07-16 01:40:19 +02:00
|
|
|
from PyPDF2 import PdfFileMerger
|
|
|
|
from PyPDF2.utils import PdfReadError
|
2022-05-07 22:45:37 +02:00
|
|
|
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
|
2020-05-04 18:18:27 +02:00
|
|
|
|
2022-09-14 01:35:36 +02:00
|
|
|
from trytond.model import ModelSQL, ModelView, DeactivableMixin, fields
|
2020-05-23 16:19:07 +02:00
|
|
|
from trytond.pool import Pool
|
2022-05-07 22:45:37 +02:00
|
|
|
from trytond.pyson import Eval, Bool, Or
|
2020-05-23 16:19:07 +02:00
|
|
|
from trytond.transaction import Transaction
|
|
|
|
from trytond.cache import Cache
|
2020-07-16 01:40:19 +02:00
|
|
|
from trytond.exceptions import UserError
|
|
|
|
from trytond.i18n import gettext
|
2022-05-07 22:45:37 +02:00
|
|
|
from trytond.tools import file_open
|
|
|
|
from trytond import backend
|
|
|
|
from .generator import PdfGenerator
|
2020-05-04 18:18:27 +02:00
|
|
|
|
|
|
|
|
2022-09-14 01:35:36 +02:00
|
|
|
class ReportTemplate(DeactivableMixin, ModelSQL, ModelView):
|
2022-05-07 22:45:37 +02:00
|
|
|
'Report Template'
|
|
|
|
__name__ = 'lims.report.template'
|
2020-05-04 18:18:27 +02:00
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
report_name = fields.Char('Internal Name', required=True)
|
2020-05-04 18:18:27 +02:00
|
|
|
name = fields.Char('Name', required=True)
|
2020-06-19 16:11:14 +02:00
|
|
|
type = fields.Selection([
|
2020-09-02 01:13:00 +02:00
|
|
|
(None, ''),
|
|
|
|
('base', 'HTML'),
|
|
|
|
('header', 'HTML - Header'),
|
|
|
|
('footer', 'HTML - Footer'),
|
|
|
|
], 'Type')
|
2022-05-07 22:45:37 +02:00
|
|
|
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'])
|
2020-09-02 01:13:00 +02:00
|
|
|
content = fields.Text('Content',
|
|
|
|
states={'required': Bool(Eval('type'))}, depends=['type'])
|
2022-05-07 22:45:37 +02:00
|
|
|
header = fields.Many2One('lims.report.template', 'Header',
|
|
|
|
domain=[
|
|
|
|
('report_name', '=', Eval('report_name')),
|
|
|
|
('type', '=', 'header'),
|
2022-09-14 01:35:36 +02:00
|
|
|
['OR', ('active', '=', True),
|
|
|
|
('id', '=', Eval('header'))],
|
2022-05-07 22:45:37 +02:00
|
|
|
],
|
|
|
|
depends=['report_name'])
|
|
|
|
footer = fields.Many2One('lims.report.template', 'Footer',
|
|
|
|
domain=[
|
|
|
|
('report_name', '=', Eval('report_name')),
|
|
|
|
('type', '=', 'footer'),
|
2022-09-14 01:35:36 +02:00
|
|
|
['OR', ('active', '=', True),
|
|
|
|
('id', '=', Eval('footer'))],
|
2022-05-07 22:45:37 +02:00
|
|
|
],
|
|
|
|
depends=['report_name'])
|
|
|
|
translations = fields.One2Many('lims.report.template.translation',
|
2020-05-23 16:19:07 +02:00
|
|
|
'template', 'Translations')
|
2022-05-07 22:45:37 +02:00
|
|
|
_translation_cache = Cache('lims.report.template.translation',
|
2020-05-23 16:19:07 +02:00
|
|
|
size_limit=10240, context=False)
|
2022-05-07 22:45:37 +02:00
|
|
|
sections = fields.One2Many('lims.report.template.section',
|
2020-07-16 01:40:19 +02:00
|
|
|
'template', 'Sections')
|
|
|
|
previous_sections = fields.Function(fields.One2Many(
|
2022-05-07 22:45:37 +02:00
|
|
|
'lims.report.template.section', 'template',
|
2020-07-16 01:40:19 +02:00
|
|
|
'Previous Sections', domain=[('position', '=', 'previous')]),
|
|
|
|
'get_previous_sections', setter='set_previous_sections')
|
|
|
|
following_sections = fields.Function(fields.One2Many(
|
2022-05-07 22:45:37 +02:00
|
|
|
'lims.report.template.section', 'template',
|
2020-07-16 01:40:19 +02:00
|
|
|
'Following Sections', domain=[('position', '=', 'following')]),
|
|
|
|
'get_following_sections', setter='set_following_sections')
|
2021-06-02 14:57:01 +02:00
|
|
|
page_orientation = fields.Selection([
|
|
|
|
('portrait', 'Portrait'),
|
2021-09-24 02:45:53 +02:00
|
|
|
('landscape', 'Landscape'),
|
2022-05-07 22:45:37 +02:00
|
|
|
], '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)))
|
2020-05-23 16:19:07 +02:00
|
|
|
|
2020-06-19 16:11:14 +02:00
|
|
|
@staticmethod
|
|
|
|
def default_type():
|
2020-09-02 01:13:00 +02:00
|
|
|
return None
|
2020-06-19 16:11:14 +02:00
|
|
|
|
2021-06-02 14:57:01 +02:00
|
|
|
@staticmethod
|
|
|
|
def default_page_orientation():
|
|
|
|
return 'portrait'
|
|
|
|
|
2020-06-19 16:11:14 +02:00
|
|
|
@classmethod
|
|
|
|
def view_attributes(cls):
|
2020-08-06 19:52:36 +02:00
|
|
|
return super().view_attributes() + [
|
2022-05-07 22:45:37 +02:00
|
|
|
('//page[@name="content"]', 'states', {
|
2020-09-02 01:13:00 +02:00
|
|
|
'invisible': ~Bool(Eval('type')),
|
|
|
|
}),
|
2020-06-19 16:11:14 +02:00
|
|
|
('//page[@id="header_footer"]', 'states', {
|
2020-08-06 23:36:35 +02:00
|
|
|
'invisible': Eval('type') != 'base',
|
|
|
|
}),
|
2020-09-02 01:13:00 +02:00
|
|
|
('//page[@name="translations"]', 'states', {
|
|
|
|
'invisible': ~Bool(Eval('type')),
|
|
|
|
}),
|
2020-07-16 01:40:19 +02:00
|
|
|
]
|
2020-06-19 16:11:14 +02:00
|
|
|
|
2020-05-23 16:19:07 +02:00
|
|
|
@classmethod
|
|
|
|
def gettext(cls, *args, **variables):
|
|
|
|
ReportTemplateTranslation = Pool().get(
|
2022-05-07 22:45:37 +02:00
|
|
|
'lims.report.template.translation')
|
2020-05-23 16:19:07 +02:00
|
|
|
template, src, lang = args
|
|
|
|
key = (template, src, lang)
|
|
|
|
text = cls._translation_cache.get(key)
|
|
|
|
if text is None:
|
2020-06-19 16:11:14 +02:00
|
|
|
template_ids = [template]
|
|
|
|
base = cls(template)
|
|
|
|
if base.header:
|
|
|
|
template_ids.append(base.header.id)
|
|
|
|
if base.footer:
|
|
|
|
template_ids.append(base.footer.id)
|
2020-05-23 16:19:07 +02:00
|
|
|
translations = ReportTemplateTranslation.search([
|
2020-06-19 16:11:14 +02:00
|
|
|
('template', 'in', template_ids),
|
2020-05-23 16:19:07 +02:00
|
|
|
('src', '=', src),
|
|
|
|
('lang', '=', lang),
|
|
|
|
], limit=1)
|
|
|
|
if translations:
|
|
|
|
text = translations[0].value
|
|
|
|
else:
|
|
|
|
text = src
|
|
|
|
cls._translation_cache.set(key, text)
|
|
|
|
return text if not variables else text % variables
|
|
|
|
|
2020-07-16 01:40:19 +02:00
|
|
|
def get_previous_sections(self, name):
|
|
|
|
return [s.id for s in self.sections if s.position == 'previous']
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_previous_sections(cls, sections, name, value):
|
|
|
|
if not value:
|
|
|
|
return
|
|
|
|
cls.write(sections, {'sections': value})
|
|
|
|
|
|
|
|
def get_following_sections(self, name):
|
|
|
|
return [s.id for s in self.sections if s.position == 'following']
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_following_sections(cls, sections, name, value):
|
|
|
|
if not value:
|
|
|
|
return
|
|
|
|
cls.write(sections, {'sections': value})
|
|
|
|
|
2020-05-23 16:19:07 +02:00
|
|
|
|
|
|
|
class ReportTemplateTranslation(ModelSQL, ModelView):
|
2022-05-07 22:45:37 +02:00
|
|
|
'Report Template Translation'
|
|
|
|
__name__ = 'lims.report.template.translation'
|
2020-05-23 16:19:07 +02:00
|
|
|
_order_name = 'src'
|
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
template = fields.Many2One('lims.report.template', 'Template',
|
2020-05-23 16:19:07 +02:00
|
|
|
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(
|
2022-05-07 22:45:37 +02:00
|
|
|
'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)
|
2020-05-23 16:19:07 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def default_lang():
|
|
|
|
return Transaction().language
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_language(cls):
|
|
|
|
result = cls._get_language_cache.get(None)
|
|
|
|
if result is not None:
|
|
|
|
return result
|
|
|
|
langs = Pool().get('ir.lang').search([('translatable', '=', True)])
|
|
|
|
result = [(lang.code, lang.name) for lang in langs]
|
|
|
|
cls._get_language_cache.set(None, result)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def create(cls, vlist):
|
2022-05-07 22:45:37 +02:00
|
|
|
Template = Pool().get('lims.report.template')
|
2020-05-23 16:19:07 +02:00
|
|
|
Template._translation_cache.clear()
|
2020-08-06 19:52:36 +02:00
|
|
|
return super().create(vlist)
|
2020-05-23 16:19:07 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def write(cls, *args):
|
2022-05-07 22:45:37 +02:00
|
|
|
Template = Pool().get('lims.report.template')
|
2020-05-23 16:19:07 +02:00
|
|
|
Template._translation_cache.clear()
|
2020-08-06 19:52:36 +02:00
|
|
|
return super().write(*args)
|
2020-05-23 16:19:07 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def delete(cls, translations):
|
2022-05-07 22:45:37 +02:00
|
|
|
Template = Pool().get('lims.report.template')
|
2020-05-23 16:19:07 +02:00
|
|
|
Template._translation_cache.clear()
|
2020-08-06 19:52:36 +02:00
|
|
|
return super().delete(translations)
|
2020-07-09 17:50:47 +02:00
|
|
|
|
|
|
|
|
2020-07-16 01:40:19 +02:00
|
|
|
class ReportTemplateSection(ModelSQL, ModelView):
|
2022-05-07 22:45:37 +02:00
|
|
|
'Report Template Section'
|
|
|
|
__name__ = 'lims.report.template.section'
|
2020-07-16 01:40:19 +02:00
|
|
|
_order_name = 'order'
|
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
template = fields.Many2One('lims.report.template', 'Template',
|
2020-07-16 01:40:19 +02:00
|
|
|
ondelete='CASCADE', select=True, required=True)
|
|
|
|
name = fields.Char('Name', required=True)
|
|
|
|
data = fields.Binary('File', filename='name', required=True,
|
|
|
|
file_id='data_id', store_prefix='results_report_template_section')
|
|
|
|
data_id = fields.Char('File ID', readonly=True)
|
|
|
|
position = fields.Selection([
|
|
|
|
('previous', 'Previous'),
|
|
|
|
('following', 'Following'),
|
|
|
|
], 'Position', required=True)
|
|
|
|
order = fields.Integer('Order')
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def __setup__(cls):
|
2020-08-06 19:52:36 +02:00
|
|
|
super().__setup__()
|
2020-07-16 01:40:19 +02:00
|
|
|
cls._order.insert(0, ('order', 'ASC'))
|
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
@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)
|
|
|
|
|
2020-07-16 01:40:19 +02:00
|
|
|
@classmethod
|
|
|
|
def validate(cls, sections):
|
2020-08-06 19:52:36 +02:00
|
|
|
super().validate(sections)
|
2020-07-16 01:40:19 +02:00
|
|
|
merger = PdfFileMerger(strict=False)
|
|
|
|
for section in sections:
|
|
|
|
filedata = BytesIO(section.data)
|
|
|
|
try:
|
|
|
|
merger.append(filedata)
|
|
|
|
except PdfReadError:
|
|
|
|
raise UserError(gettext('lims_report_html.msg_section_pdf'))
|
|
|
|
|
|
|
|
|
2023-08-09 18:59:39 +02:00
|
|
|
class LimsReport:
|
|
|
|
__slots__ = ()
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
@classmethod
|
2023-08-09 18:59:39 +02:00
|
|
|
def get_action(cls, data):
|
2022-05-07 22:45:37 +02:00
|
|
|
pool = Pool()
|
|
|
|
ActionReport = pool.get('ir.action.report')
|
2023-08-09 18:59:39 +02:00
|
|
|
|
|
|
|
action_id = data.get('action_id')
|
|
|
|
if action_id is None:
|
|
|
|
action_reports = ActionReport.search([
|
|
|
|
('report_name', '=', cls.__name__),
|
|
|
|
])
|
|
|
|
assert action_reports, '%s not found' % cls
|
|
|
|
action = action_reports[0]
|
|
|
|
else:
|
|
|
|
action = ActionReport(action_id)
|
|
|
|
|
|
|
|
return action, action.model or data.get('model')
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def execute(cls, ids, data):
|
2022-05-07 22:45:37 +02:00
|
|
|
cls.check_access()
|
2023-08-09 18:59:39 +02:00
|
|
|
action, model = cls.get_action(data)
|
|
|
|
if action.template_extension != 'lims' or action.lims_template is None:
|
|
|
|
return super().execute(ids, data)
|
|
|
|
|
|
|
|
if data is None:
|
|
|
|
data = {}
|
|
|
|
current_data = data.copy()
|
|
|
|
|
|
|
|
template = action.lims_template
|
|
|
|
if template.type == 'base': # HTML
|
|
|
|
result = cls.execute_html_lims_report(ids, current_data)
|
|
|
|
else:
|
|
|
|
current_data['action_id'] = None
|
|
|
|
if template.report:
|
|
|
|
current_data['action_id'] = template.report.id
|
|
|
|
result = cls.execute_custom_lims_report(ids, current_data)
|
|
|
|
return result
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def execute_custom_lims_report(cls, ids, data):
|
|
|
|
pool = Pool()
|
|
|
|
ActionReport = pool.get('ir.action.report')
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
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 = []
|
2023-02-24 16:24:07 +01:00
|
|
|
header = {}
|
2022-05-07 22:45:37 +02:00
|
|
|
model = action.model or data.get('model')
|
|
|
|
if model:
|
|
|
|
records = cls._get_records(ids, model, data)
|
2023-02-24 16:24:07 +01:00
|
|
|
oext, content = cls._execute(records, header, data, action)
|
2022-05-07 22:45:37 +02:00
|
|
|
if not isinstance(content, str):
|
|
|
|
content = bytearray(content) if bytes == str else bytes(content)
|
2022-12-02 15:05:15 +01:00
|
|
|
|
|
|
|
record = records[0]
|
2023-08-10 14:22:22 +02:00
|
|
|
if oext == 'pdf':
|
|
|
|
previous_sections = (hasattr(record, 'previous_sections') and
|
|
|
|
record.previous_sections or [])
|
|
|
|
following_sections = (hasattr(record, 'following_sections') and
|
|
|
|
record.following_sections or [])
|
|
|
|
if previous_sections or following_sections:
|
|
|
|
merger = PdfFileMerger(strict=False)
|
|
|
|
# Previous Sections
|
|
|
|
for section in previous_sections:
|
|
|
|
filedata = BytesIO(section.data)
|
|
|
|
merger.append(filedata)
|
|
|
|
# Main Report
|
|
|
|
filedata = BytesIO(content)
|
2022-12-02 15:05:15 +01:00
|
|
|
merger.append(filedata)
|
2023-08-10 14:22:22 +02:00
|
|
|
# Following Sections
|
|
|
|
for section in following_sections:
|
|
|
|
filedata = BytesIO(section.data)
|
|
|
|
merger.append(filedata)
|
|
|
|
output = BytesIO()
|
|
|
|
merger.write(output)
|
|
|
|
content = output.getvalue()
|
2022-12-02 15:05:15 +01:00
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
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')
|
|
|
|
|
|
|
|
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]
|
2023-08-09 18:59:39 +02:00
|
|
|
template, tcontent, theader, tfooter = (
|
2022-05-07 22:45:37 +02:00
|
|
|
cls.get_lims_template(action, record))
|
|
|
|
context = Transaction().context
|
2023-08-09 18:59:39 +02:00
|
|
|
context['template'] = template.id
|
|
|
|
if not template:
|
2022-05-07 22:45:37 +02:00
|
|
|
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)
|
|
|
|
|
2023-08-09 18:59:39 +02:00
|
|
|
page_orientation = (template and
|
|
|
|
template.page_orientation or 'portrait')
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
2023-08-10 14:22:22 +02:00
|
|
|
previous_sections = []
|
|
|
|
if hasattr(record, 'previous_sections'):
|
|
|
|
previous_sections = record.previous_sections
|
|
|
|
elif template:
|
|
|
|
previous_sections = template.previous_sections
|
|
|
|
following_sections = []
|
|
|
|
if hasattr(record, 'following_sections'):
|
|
|
|
following_sections = record.following_sections
|
|
|
|
elif template:
|
|
|
|
following_sections = template.following_sections
|
2023-08-09 18:59:39 +02:00
|
|
|
if previous_sections or following_sections:
|
2022-05-07 22:45:37 +02:00
|
|
|
merger = PdfFileMerger(strict=False)
|
|
|
|
# Previous Sections
|
2023-08-09 18:59:39 +02:00
|
|
|
for section in previous_sections:
|
2022-05-07 22:45:37 +02:00
|
|
|
filedata = BytesIO(section.data)
|
|
|
|
merger.append(filedata)
|
|
|
|
# Main Report
|
|
|
|
filedata = BytesIO(document)
|
|
|
|
merger.append(filedata)
|
|
|
|
# Following Sections
|
2023-08-09 18:59:39 +02:00
|
|
|
for section in following_sections:
|
2022-05-07 22:45:37 +02:00
|
|
|
filedata = BytesIO(section.data)
|
|
|
|
merger.append(filedata)
|
|
|
|
output = BytesIO()
|
|
|
|
merger.write(output)
|
2023-03-14 15:42:26 +01:00
|
|
|
merger.close()
|
2022-05-07 22:45:37 +02:00
|
|
|
document = output.getvalue()
|
2023-03-14 15:42:26 +01:00
|
|
|
output.close()
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
return 'pdf', document
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_lims_template(cls, action, record):
|
2023-08-09 18:59:39 +02:00
|
|
|
template, content, header, footer = None, None, None, None
|
|
|
|
template = action.lims_template or record.template
|
|
|
|
if template:
|
|
|
|
content = '<body>%s</body>' % template.content
|
|
|
|
header = (template.header and
|
2022-05-07 22:45:37 +02:00
|
|
|
'<header id="header">%s</header>' %
|
2023-08-09 18:59:39 +02:00
|
|
|
template.header.content)
|
|
|
|
footer = (template.footer and
|
2022-05-07 22:45:37 +02:00
|
|
|
'<footer id="footer">%s</footer>' %
|
2023-08-09 18:59:39 +02:00
|
|
|
template.footer.content)
|
2022-05-07 22:45:37 +02:00
|
|
|
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'))
|
2023-08-09 18:59:39 +02:00
|
|
|
return template, content, header, footer
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
@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()
|
|
|
|
|
2023-03-10 18:46:55 +01:00
|
|
|
header = {}
|
2022-05-07 22:45:37 +02:00
|
|
|
report_template = env.from_string(template_string)
|
2023-03-10 18:46:55 +01:00
|
|
|
context = cls.get_context(records, header, data=data)
|
2022-05-07 22:45:37 +02:00
|
|
|
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')
|
2023-08-09 00:10:40 +02:00
|
|
|
Company = Pool().get('company.company')
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
def module_path(name):
|
|
|
|
module, path = name.split('/', 1)
|
|
|
|
with file_open(os.path.join(module, path)) as f:
|
|
|
|
return 'file://%s' % f.name
|
|
|
|
|
2023-08-09 00:10:40 +02:00
|
|
|
def render(value, digits=2, lang=None, company=None, filename=None, date_format=None):
|
2022-05-07 22:45:37 +02:00
|
|
|
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
|
2023-08-09 00:10:40 +02:00
|
|
|
|
|
|
|
if isinstance(value, datetime):
|
|
|
|
if company:
|
|
|
|
value = company.convert_timezone_datetime(value)
|
|
|
|
return lang.strftime(value, format=date_format)
|
2022-05-07 22:45:37 +02:00
|
|
|
if isinstance(value, date):
|
2023-07-18 22:49:20 +02:00
|
|
|
return lang.strftime(value, format=date_format)
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
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')])
|
2023-08-09 00:10:40 +02:00
|
|
|
company_id = Transaction().context.get('company')
|
|
|
|
company = None
|
|
|
|
if company_id:
|
|
|
|
company = Company(company_id)
|
|
|
|
# now = company.convert_timezone_datetime(now)
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
'modulepath': module_path,
|
2023-08-09 00:10:40 +02:00
|
|
|
'render': partial(render, lang=lang, company=company),
|
2022-05-07 22:45:37 +02:00
|
|
|
'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):
|
2020-07-09 17:50:47 +02:00
|
|
|
'Results Report Template Trend Chart'
|
2022-05-07 22:45:37 +02:00
|
|
|
__name__ = 'lims.report.template.trend.chart'
|
2020-07-09 17:50:47 +02:00
|
|
|
_order_name = 'order'
|
|
|
|
|
2022-05-07 22:45:37 +02:00
|
|
|
template = fields.Many2One('lims.report.template', 'Template',
|
2020-07-09 17:50:47 +02:00
|
|
|
ondelete='CASCADE', select=True, required=True)
|
|
|
|
chart = fields.Many2One('lims.trend.chart', 'Trend Chart',
|
|
|
|
required=True, domain=[('active', '=', True)])
|
|
|
|
order = fields.Integer('Order')
|
2022-05-07 22:45:37 +02:00
|
|
|
|
|
|
|
@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)
|