# This file is part of lims_report_html module for Tryton. # The COPYRIGHT file at the top level of this repository contains # the full copyright notices and license terms. from io import BytesIO from PyPDF2 import PdfFileMerger from PyPDF2.utils import PdfReadError from trytond.model import ModelView, ModelSQL, fields from trytond.wizard import Wizard, StateTransition, StateView, Button from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, Not, Bool from trytond.transaction import Transaction from trytond.exceptions import UserError from trytond.i18n import gettext from .html_template import LimsReport class ResultsReportVersionDetail(metaclass=PoolMeta): __name__ = 'lims.results_report.version.detail' template = fields.Many2One('lims.report.template', 'Report Template', domain=[ ('report_name', '=', 'lims.result_report'), ('type', 'in', [None, 'base']), ['OR', ('active', '=', True), ('id', '=', Eval('template'))], ], states={'readonly': Eval('state') != 'draft'}, depends=['state']) template_type = fields.Function(fields.Selection([ (None, ''), ('base', 'HTML'), ('header', 'HTML - Header'), ('footer', 'HTML - Footer'), ], 'Report Template Type'), 'get_template_type') sections = fields.One2Many('lims.results_report.version.detail.section', 'version_detail', 'Sections') previous_sections = fields.Function(fields.One2Many( 'lims.results_report.version.detail.section', 'version_detail', 'Previous Sections', domain=[('position', '=', 'previous')]), 'get_previous_sections', setter='set_previous_sections') following_sections = fields.Function(fields.One2Many( 'lims.results_report.version.detail.section', 'version_detail', 'Following Sections', domain=[('position', '=', 'following')]), 'get_following_sections', setter='set_following_sections') trend_charts = fields.One2Many( 'lims.results_report.version.detail.trend.chart', 'version_detail', 'Trend Charts') charts_x_row = fields.Selection([ ('1', '1'), ('2', '2'), ], 'Charts per Row') comments_plain = fields.Function(fields.Text('Comments', translate=True), 'get_comments_plain', setter='set_comments_plain') @classmethod def __setup__(cls): super().__setup__() if 'invisible' in cls.resultrange_origin.states: del cls.resultrange_origin.states['invisible'] if 'required' in cls.resultrange_origin.states: del cls.resultrange_origin.states['required'] cls._buttons.update({ 'relate_attachment': { 'invisible': Eval('state').in_(['released', 'annulled']), 'depends': ['state'], }, }) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('//page[@id="comments"]', 'states', { 'invisible': Not(Bool(Eval('template_type'))), }), ('//page[@id="comments_plain"]', 'states', { 'invisible': Eval('template_type') == 'base', }), ] @staticmethod def default_charts_x_row(): return '1' def get_template_type(self, name): return self.template and self.template.type or None @fields.depends('template', '_parent_template.trend_charts', '_parent_template.sections', 'sections', 'resultrange_origin') def on_change_template(self): if (self.template and self.template.resultrange_origin and not self.resultrange_origin): self.resultrange_origin = self.template.resultrange_origin.id if self.template and self.template.trend_charts: self.trend_charts = [{ 'chart': c.chart.id, 'order': c.order, } for c in self.template.trend_charts] self.charts_x_row = self.template.charts_x_row if self.template and self.template.sections: sections = {} for s in self.sections + self.template.sections: sections[s.name] = { 'name': s.name, 'data': s.data, 'data_id': s.data_id, 'position': s.position, 'order': s.order, } self.sections = sections.values() def get_previous_sections(self, name): return [s.id for s in self.sections if s.position == 'previous'] @classmethod def set_previous_sections(cls, sections, name, value): if not value: return cls.write(sections, {'sections': value}) def get_following_sections(self, name): return [s.id for s in self.sections if s.position == 'following'] @classmethod def set_following_sections(cls, sections, name, value): if not value: return cls.write(sections, {'sections': value}) @classmethod def _get_fields_from_samples(cls, samples, generate_report_form=None): pool = Pool() Notebook = pool.get('lims.notebook') detail_default = super()._get_fields_from_samples(samples, generate_report_form) result_template = None if generate_report_form and generate_report_form.template: result_template = generate_report_form.template resultrange_origin = None for sample in samples: nb = Notebook(sample['notebook']) if not result_template: result_template = cls._get_result_template_from_sample(nb) if not resultrange_origin: resultrange_origin = cls._get_resultrange_from_sample(nb) if result_template: detail_default['template'] = result_template.id if not resultrange_origin: resultrange_origin = result_template.resultrange_origin if result_template.trend_charts: detail_default['trend_charts'] = [('create', [{ 'chart': c.chart.id, 'order': c.order, } for c in result_template.trend_charts])] detail_default['charts_x_row'] = ( result_template.charts_x_row) if result_template.sections: detail_default['sections'] = [('create', [{ 'name': s.name, 'data': s.data, 'data_id': s.data_id, 'position': s.position, 'order': s.order, } for s in result_template.sections])] if resultrange_origin: detail_default['resultrange_origin'] = resultrange_origin.id return detail_default @classmethod def _get_result_template_from_sample(cls, notebook): pool = Pool() Service = pool.get('lims.service') Laboratory = pool.get('lims.laboratory') Configuration = pool.get('lims.configuration') result_template = notebook.fraction.sample.result_template if not result_template: ok = True services = Service.search([ ('fraction', '=', notebook.fraction), ('analysis.type', '=', 'group'), ('annulled', '=', False), ]) for service in services: if service.analysis.result_template: if not result_template: result_template = service.analysis.result_template elif result_template != service.analysis.result_template: ok = False elif result_template: ok = False if not ok: result_template = None if not result_template: laboratory_id = Transaction().context.get( 'samples_pending_reporting_laboratory', None) if laboratory_id: laboratory = Laboratory(laboratory_id) result_template = laboratory.result_template if not result_template: config_ = Configuration(1) result_template = config_.result_template return result_template @classmethod def _get_resultrange_from_sample(cls, notebook): return notebook.fraction.sample.resultrange_origin @classmethod def _get_fields_not_overwrite(cls): fields = super()._get_fields_not_overwrite() fields.extend(['template', 'trend_charts', 'charts_x_row', 'sections', 'resultrange_origin']) return fields @classmethod def _get_fields_from_detail(cls, detail): detail_default = super()._get_fields_from_detail(detail) if detail.template: detail_default['template'] = detail.template.id if detail.trend_charts: detail_default['trend_charts'] = [('create', [{ 'chart': c.chart.id, 'order': c.order, } for c in detail.trend_charts])] detail_default['charts_x_row'] = detail.charts_x_row if detail.sections: detail_default['sections'] = [('create', [{ 'name': s.name, 'data': s.data, 'data_id': s.data_id, 'position': s.position, 'order': s.order, } for s in detail.sections])] return detail_default def get_comments_plain(self, name): return self.comments @classmethod def set_comments_plain(cls, records, name, value): cls.write(records, {'comments': value}) @classmethod @ModelView.button_action( 'lims_report_html.wiz_results_report_version_detail_relate_attachment') def relate_attachment(cls, details): pass class ResultsReportVersionDetailSection(ModelSQL, ModelView): 'Results Report Version Detail Section' __name__ = 'lims.results_report.version.detail.section' _order_name = 'order' version_detail = fields.Many2One('lims.results_report.version.detail', 'Report Detail', ondelete='CASCADE', select=True, required=True) name = fields.Char('Name', required=True) data = fields.Binary('File', filename='name', required=True, file_id='data_id', store_prefix='results_report_section') data_id = fields.Char('File ID', readonly=True) position = fields.Selection([ ('previous', 'Previous'), ('following', 'Following'), ], 'Position', required=True) order = fields.Integer('Order') @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('order', 'ASC')) @classmethod def validate(cls, sections): super().validate(sections) merger = PdfFileMerger(strict=False) for section in sections: filedata = BytesIO(section.data) try: merger.append(filedata) except PdfReadError: raise UserError(gettext('lims_report_html.msg_section_pdf')) class ResultsReportVersionDetailTrendChart(ModelSQL, ModelView): 'Results Report Version Detail Trend Chart' __name__ = 'lims.results_report.version.detail.trend.chart' _order_name = 'order' version_detail = fields.Many2One('lims.results_report.version.detail', 'Report Detail', ondelete='CASCADE', select=True, required=True) chart = fields.Many2One('lims.trend.chart', 'Trend Chart', required=True, domain=[('active', '=', True)]) order = fields.Integer('Order') class RelateAttachmentResultsReportStart(ModelView): 'Relate Attachment to Results Report' __name__ = 'lims.results_report.version.detail.relate_attachment.start' position = fields.Selection([ ('previous', 'Previous'), ('following', 'Following'), ], 'Position', required=True) attachment = fields.Many2One('ir.attachment', 'Attachment', required=True, domain=[('id', 'in', Eval('attachment_domain'))], depends=['attachment_domain']) attachment_domain = fields.Many2Many('ir.attachment', None, None, 'Attachment domain') class RelateAttachmentResultsReport(Wizard): 'Relate Attachment to Results Report' __name__ = 'lims.results_report.version.detail.relate_attachment' start = StateView( 'lims.results_report.version.detail.relate_attachment.start', 'lims_report_html.' 'results_report_version_detail_relate_attachment_start_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Relate', 'relate', 'tryton-ok', default=True), ]) relate = StateTransition() def default_start(self, fields): pool = Pool() ResultsDetail = pool.get('lims.results_report.version.detail') detail = ResultsDetail(Transaction().context['active_id']) attachments = self.get_attachments(detail) return {'attachment_domain': [a.id for a in attachments]} def _get_resource(self, obj): return '%s,%s' % (obj.__name__, obj.id) def get_attachments(self, detail): pool = Pool() Attachment = pool.get('ir.attachment') resources = [] resources.append(self._get_resource(detail)) entry = detail.report_version.results_report.entry if entry: resources.append(self._get_resource(entry)) for sample in detail.samples: resources.append(self._get_resource(sample)) resources.append(self._get_resource(sample.notebook)) resources.append(self._get_resource(sample.notebook.fraction)) resources.append(self._get_resource( sample.notebook.fraction.sample)) resources.append(self._get_resource( sample.notebook.fraction.sample.entry)) for line in sample.notebook_lines: if not line.notebook_line: continue resources.append(self._get_resource(line)) resources.append(self._get_resource(line.notebook_line)) attachments = Attachment.search([ ('resource', 'in', resources), ]) return attachments def transition_relate(self): pool = Pool() ResultsDetailSection = pool.get( 'lims.results_report.version.detail.section') detail_id = Transaction().context['active_id'] defaults = { 'version_detail': detail_id, 'position': self.start.position, 'name': self.start.attachment.name, 'data': self.start.attachment.data, 'data_id': self.start.attachment.file_id, 'order': None, } ResultsDetailSection.create([defaults]) return 'end' def end(self): return 'reload' class ResultsReportVersionDetailSample(metaclass=PoolMeta): __name__ = 'lims.results_report.version.detail.sample' trend_charts = fields.Function(fields.Text('Trend Charts'), 'get_trend_charts') attachments = fields.Function(fields.Text('Attachments'), 'get_attachments') def get_trend_charts(self, name): pool = Pool() OpenTrendChart = pool.get('lims.trend.chart.open', type='wizard') ResultReport = pool.get('lims.result_report', type='report') if not self.version_detail.trend_charts: return '' charts = [] for tc in self.version_detail.trend_charts: session_id, _, _ = OpenTrendChart.create() open_chart = OpenTrendChart(session_id) open_chart.start.chart = tc.chart open_chart.start.notebook = self.notebook open_chart.transition_compute() plot = tc.chart.get_plot(session_id) charts.append(plot) OpenTrendChart.delete(session_id) div_row = '
' charts_x_row = int(self.version_detail.charts_x_row) or 1 if charts_x_row == 1: div_col = '
' elif charts_x_row == 2: div_col = '
' end_div = '
' content = '
' count = 0 for chart in charts: if count == 0: content += div_row content += div_col content += ('') content += end_div count += 1 if count == charts_x_row: content += end_div count = 0 if count != 0: content += end_div content += end_div return content def get_trend_charts_odt(self): pool = Pool() OpenTrendChart = pool.get('lims.trend.chart.open', type='wizard') if not self.version_detail.trend_charts: return [] charts = [] for tc in self.version_detail.trend_charts: session_id, _, _ = OpenTrendChart.create() open_chart = OpenTrendChart(session_id) open_chart.start.chart = tc.chart open_chart.start.notebook = self.notebook open_chart.transition_compute() plot = tc.chart.get_plot(session_id) charts.append(plot) OpenTrendChart.delete(session_id) return charts def _get_resource(self, obj): return '%s,%s' % (obj.__name__, obj.id) def get_attachments(self, name): pool = Pool() Attachment = pool.get('ir.attachment') ResultReport = pool.get('lims.result_report', type='report') resources = [] resources.append(self._get_resource(self)) resources.append(self._get_resource(self.notebook)) resources.append(self._get_resource(self.notebook.fraction)) resources.append(self._get_resource( self.notebook.fraction.sample)) resources.append(self._get_resource( self.notebook.fraction.sample.entry)) for line in self.notebook_lines: resources.append(self._get_resource(line)) resources.append(self._get_resource(line.notebook_line)) attachments = Attachment.search([ ('resource', 'in', resources), ]) div_row = '
' div_col = '
' end_div = '
' content = '
' count = 0 extensions = ['png', 'jpg'] for attachment in attachments: if not any(x in attachment.name.lower() for x in extensions): continue if count == 0: content += div_row content += div_col if attachment.title: content += '

%s

' % ( attachment.title, ) content += ('') content += end_div count += 1 if count == 2: content += end_div count = 0 if count != 0: content += end_div content += end_div return content class ResultReport(LimsReport, metaclass=PoolMeta): __name__ = 'lims.result_report' @classmethod def execute(cls, ids, data): 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')) if data is None: data = {} current_data = data.copy() current_data['alt_lang'] = results_report.report_language.code template = results_report.template if template and template.type == 'base': # HTML 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_lims_report(ids, current_data) cached_reports = CachedReport.search([ ('version_detail', '=', results_report.id), ('report_language', '=', results_report.report_language.id), ['OR', ('report_cache', '!=', None), ('report_cache_id', '!=', None)], ]) if cached_reports: result = (cached_reports[0].report_format, cached_reports[0].report_cache) + result[2:] else: if current_data.get('save_cache', False): cached_reports = CachedReport.search([ ('version_detail', '=', results_report.id), ('report_language', '=', results_report.report_language.id), ]) if cached_reports: CachedReport.write(cached_reports, { 'report_cache': result[1], 'report_format': result[0], }) else: CachedReport.create([{ 'version_detail': results_report.id, 'report_language': results_report.report_language.id, 'report_cache': result[1], 'report_format': result[0], }]) return result class GenerateReportStart(metaclass=PoolMeta): __name__ = 'lims.notebook.generate_results_report.start' template = fields.Many2One('lims.report.template', 'Report Template', domain=[ ('report_name', '=', 'lims.result_report'), ('type', 'in', [None, 'base']), ['OR', ('active', '=', True), ('id', '=', Eval('template'))], ], states={'readonly': Bool(Eval('report'))}, depends=['report']) class GenerateReport(metaclass=PoolMeta): __name__ = 'lims.notebook.generate_results_report' def default_start(self, fields): pool = Pool() Notebook = pool.get('lims.notebook') res = super().default_start(fields) res['template'] = None if res['report']: return res template = None for notebook in Notebook.browse(Transaction().context['active_ids']): if not notebook.fraction.sample.result_template: continue if not template: template = notebook.fraction.sample.result_template.id elif template != notebook.fraction.sample.result_template.id: return res res['template'] = template return res