667 lines
23 KiB
Python
667 lines
23 KiB
Python
# This file is part of lims_email module for Tryton.
|
|
# The COPYRIGHT file at the top level of this repository contains
|
|
# the full copyright notices and license terms.
|
|
import logging
|
|
from datetime import datetime
|
|
from email import encoders
|
|
from email.mime.base import MIMEBase
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from string import Template
|
|
|
|
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.wizard import Wizard, StateView, StateTransition, Button
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.transaction import Transaction
|
|
from trytond.tools import get_smtp_server
|
|
from trytond.config import config as tconfig
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ResultsReportVersionDetail(metaclass=PoolMeta):
|
|
__name__ = 'lims.results_report.version.detail'
|
|
|
|
sent_date = fields.Function(fields.DateTime('Sent date'),
|
|
'get_sent_date')
|
|
|
|
@classmethod
|
|
def get_sent_date(cls, details, name):
|
|
result = {}
|
|
for d in details:
|
|
if d.state not in ('released', 'annulled'):
|
|
result[d.id] = None
|
|
else:
|
|
result[d.id] = d.report_version.results_report.sent_date
|
|
return result
|
|
|
|
def unsend(self):
|
|
results_report = self.report_version.results_report
|
|
if results_report.sent:
|
|
results_report.sent = False
|
|
results_report.sent_date = None
|
|
results_report.save()
|
|
return True
|
|
|
|
@classmethod
|
|
def do_release(cls, details):
|
|
super().do_release(details)
|
|
for detail in details:
|
|
detail.unsend()
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def release_all_lang(cls, details):
|
|
super().release_all_lang(details)
|
|
for detail in details:
|
|
detail.unsend()
|
|
|
|
|
|
class ResultsReport(metaclass=PoolMeta):
|
|
__name__ = 'lims.results_report'
|
|
|
|
sent = fields.Boolean('Sent', readonly=True)
|
|
sent_date = fields.DateTime('Sent date', readonly=True)
|
|
mailings = fields.One2Many('lims.results_report.mailing',
|
|
'results_report', 'Mailings', readonly=True)
|
|
|
|
@classmethod
|
|
def _get_modified_fields(cls):
|
|
fields = super()._get_modified_fields()
|
|
fields.extend([
|
|
'sent',
|
|
'sent_date',
|
|
])
|
|
return fields
|
|
|
|
@classmethod
|
|
def cron_send_results_report(cls):
|
|
'''
|
|
Cron - Send Results Report
|
|
'''
|
|
logger.info('Cron - Send Results Report: INIT')
|
|
pool = Pool()
|
|
SendResultsReport = pool.get('lims_email.send_results_report',
|
|
type='wizard')
|
|
|
|
results_reports = cls.search([('sent', '=', False)])
|
|
|
|
session_id, _, _ = SendResultsReport.create()
|
|
send_results_report = SendResultsReport(session_id)
|
|
with Transaction().set_context(active_ids=[results_report.id
|
|
for results_report in results_reports]):
|
|
send_results_report.transition_send()
|
|
|
|
logger.info('Cron - Send Results Report: END')
|
|
return True
|
|
|
|
def attach_report(self, report_cache, language):
|
|
'''
|
|
Attach Report file from provided cache
|
|
'''
|
|
pool = Pool()
|
|
Attachment = pool.get('ir.attachment')
|
|
|
|
name = '%s_%s.pdf' % ('informe-global-de-resultados', language.code)
|
|
data = report_cache
|
|
resource = '%s,%s' % (self.__name__, self.id)
|
|
|
|
values = {
|
|
'name': name,
|
|
'type': 'data',
|
|
'data': data,
|
|
'resource': '%s,%s' % (self.__name__, self.id),
|
|
}
|
|
|
|
attachment = Attachment.search([
|
|
('resource', '=', resource),
|
|
('name', '=', name),
|
|
])
|
|
if attachment:
|
|
Attachment.write(attachment, values)
|
|
else:
|
|
Attachment.create([values])
|
|
|
|
def clean_attached_reports(self):
|
|
'''
|
|
Remove attachments reports
|
|
'''
|
|
pool = Pool()
|
|
Attachment = pool.get('ir.attachment')
|
|
|
|
name = 'informe-global-de-resultados'
|
|
resource = '%s,%s' % (self.__name__, self.id)
|
|
|
|
attachment = Attachment.search([
|
|
('resource', '=', resource),
|
|
('name', 'like', name + '_%.pdf'),
|
|
])
|
|
if attachment:
|
|
Attachment.delete(attachment)
|
|
return True
|
|
|
|
def get_attached_report(self, report_cache, language):
|
|
filename = self._get_attached_report_filename(language)
|
|
data = {
|
|
'content': report_cache,
|
|
'format': 'pdf',
|
|
'mimetype': 'pdf',
|
|
'filename': '%s.pdf' % filename,
|
|
'name': str(self.number),
|
|
}
|
|
return data
|
|
|
|
def _get_attached_report_filename(self, language):
|
|
suffix = language.code
|
|
#filename = str(self.number) + '-' + suffix
|
|
filename = self.get_report_filename() + '-' + suffix
|
|
return filename
|
|
|
|
def get_report_filename(self):
|
|
pool = Pool()
|
|
ReportNameFormat = pool.get('lims.result_report.format')
|
|
report_name = Template(ReportNameFormat.get_format(self)).substitute(
|
|
**self._get_name_substitutions())
|
|
return report_name.strip()
|
|
|
|
def _get_name_substitutions(self):
|
|
pool = Pool()
|
|
ResultsSample = pool.get('lims.results_report.version.detail.sample')
|
|
|
|
samples = ResultsSample.search([
|
|
('version_detail.report_version.results_report',
|
|
'=', self.id),
|
|
('version_detail.valid', '=', True),
|
|
], order=[('id', 'ASC')], limit=1)
|
|
sample = samples and samples[0] or None
|
|
|
|
substitutions = {
|
|
'number': getattr(self, 'number', None) or '',
|
|
'sample_number': (sample and
|
|
sample.notebook.fraction.sample.number or ''),
|
|
'party_name': sample and sample.party.rec_name or '',
|
|
}
|
|
for key, value in list(substitutions.items()):
|
|
substitutions[key.upper()] = value.upper()
|
|
return substitutions
|
|
|
|
|
|
class ResultsReportMailing(ModelSQL, ModelView):
|
|
'Results Report Mailing'
|
|
__name__ = 'lims.results_report.mailing'
|
|
|
|
results_report = fields.Many2One('lims.results_report', 'Results Report',
|
|
required=True, ondelete='CASCADE', select=True)
|
|
date = fields.Function(fields.DateTime('Date'),
|
|
'get_date', searcher='search_date')
|
|
addresses = fields.Char('Addresses', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('date', 'DESC'))
|
|
|
|
def get_date(self, name):
|
|
return self.create_date.replace(microsecond=0)
|
|
|
|
@classmethod
|
|
def search_date(cls, name, clause):
|
|
cursor = Transaction().connection.cursor()
|
|
operator_ = clause[1:2][0]
|
|
cursor.execute('SELECT id '
|
|
'FROM "' + cls._table + '" '
|
|
'WHERE create_date' + operator_ + ' %s',
|
|
clause[2:3])
|
|
return [('id', 'in', [x[0] for x in cursor.fetchall()])]
|
|
|
|
@classmethod
|
|
def order_date(cls, tables):
|
|
return cls.create_date.convert_order('create_date', tables, cls)
|
|
|
|
|
|
class ResultsReportAnnulation(metaclass=PoolMeta):
|
|
__name__ = 'lims.results_report_annulation'
|
|
|
|
def transition_annul(self):
|
|
super().transition_annul()
|
|
ResultsDetail = Pool().get('lims.results_report.version.detail')
|
|
|
|
details_annulled = ResultsDetail.search([
|
|
('id', 'in', Transaction().context['active_ids']),
|
|
('state', '=', 'annulled'),
|
|
])
|
|
for detail in details_annulled:
|
|
detail.unsend()
|
|
|
|
# Check if the report is not longer valid details
|
|
if details_annulled:
|
|
results_report = details_annulled[0].report_version.results_report
|
|
details_valid = ResultsDetail.search([
|
|
('report_version.results_report.id', '=', results_report.id),
|
|
('state', '!=', 'annulled'),
|
|
('valid', '=', True),
|
|
])
|
|
if not details_valid:
|
|
results_report.clean_attached_reports()
|
|
return 'end'
|
|
|
|
|
|
class SendResultsReportStart(ModelView):
|
|
"Send Results Report"
|
|
__name__ = 'lims_email.send_results_report.start'
|
|
|
|
summary = fields.Text('Summary', readonly=True)
|
|
|
|
|
|
class SendResultsReportSucceed(ModelView):
|
|
"Send Results Report"
|
|
__name__ = 'lims_email.send_results_report.succeed'
|
|
|
|
|
|
class SendResultsReportFailed(ModelView):
|
|
"Send Results Report"
|
|
__name__ = 'lims_email.send_results_report.failed'
|
|
|
|
reports_not_ready = fields.Many2Many('lims.results_report',
|
|
None, None, 'Reports not ready', readonly=True)
|
|
reports_not_sent = fields.Many2Many('lims.results_report',
|
|
None, None, 'Reports not sent', readonly=True)
|
|
|
|
|
|
class SendResultsReport(Wizard):
|
|
'Send Results Report'
|
|
__name__ = 'lims_email.send_results_report'
|
|
|
|
start = StateView('lims_email.send_results_report.start',
|
|
'lims_email.send_results_report_start_view', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Send', 'send', 'tryton-ok', default=True),
|
|
])
|
|
send = StateTransition()
|
|
succeed = StateView('lims_email.send_results_report.succeed',
|
|
'lims_email.send_results_report_succeed_view', [
|
|
Button('Ok', 'end', 'tryton-ok', default=True),
|
|
])
|
|
failed = StateView('lims_email.send_results_report.failed',
|
|
'lims_email.send_results_report_failed_view', [
|
|
Button('Ok', 'end', 'tryton-ok', default=True),
|
|
])
|
|
|
|
def default_start(self, fields):
|
|
pool = Pool()
|
|
ResultsReport = pool.get('lims.results_report')
|
|
|
|
summary = ''
|
|
|
|
context = Transaction().context
|
|
model = context.get('active_model', None)
|
|
if model and model == 'ir.ui.menu':
|
|
# If it was executed from `menu item`, then search ids
|
|
active_ids = [r.id for r in ResultsReport.search(
|
|
[('sent', '=', False)])]
|
|
else:
|
|
# If it was executed from `actions`, then use context ids
|
|
active_ids = context['active_ids']
|
|
|
|
for group in self.get_grouped_reports(active_ids).values():
|
|
group['reports_ready'] = []
|
|
group['to_addrs'] = {}
|
|
|
|
for report in group['reports']:
|
|
if not report.ready_to_send:
|
|
continue
|
|
if not group['cie_fraction_type']:
|
|
group['reports_ready'].append(report)
|
|
group['to_addrs'].update(self.get_report_addrs(
|
|
report))
|
|
|
|
if not group['reports_ready']:
|
|
continue
|
|
|
|
addresses = ['"%s" <%s>' % (v, k)
|
|
for k, v in group['to_addrs'].items()]
|
|
summary += '%s\n - TO: %s\n\n' % (
|
|
', '.join([r.number for r in group['reports_ready']]),
|
|
', '.join(addresses))
|
|
|
|
default = {'summary': summary}
|
|
return default
|
|
|
|
def transition_send(self):
|
|
logger.info('Send Results Report: INIT')
|
|
pool = Pool()
|
|
Config = pool.get('lims.configuration')
|
|
ResultsReport = pool.get('lims.results_report')
|
|
Lang = pool.get('ir.lang')
|
|
|
|
from_addr = tconfig.get('email', 'from')
|
|
if not from_addr:
|
|
logger.warning('Send Results Report: FAILED')
|
|
self.failed.reports_not_ready = []
|
|
self.failed.reports_not_sent = []
|
|
return 'failed'
|
|
|
|
config = Config(1)
|
|
hide_recipients = config.mail_ack_report_hide_recipients
|
|
email_qa = config.email_qa
|
|
|
|
context = Transaction().context
|
|
model = context.get('active_model', None)
|
|
if model and model == 'ir.ui.menu':
|
|
# If it was executed from `menu item`, then search ids
|
|
active_ids = [r.id for r in ResultsReport.search(
|
|
[('sent', '=', False)])]
|
|
logger.info('Send Results Report: '
|
|
'Processing all Results Reports')
|
|
else:
|
|
# If it was executed from `actions` or `cron`, then use context ids
|
|
active_ids = context['active_ids']
|
|
logger.info('Send Results Report: '
|
|
'Processing context Results Reports')
|
|
|
|
reports_not_ready = []
|
|
reports_not_sent = []
|
|
for group in self.get_grouped_reports(active_ids).values():
|
|
group['reports_ready'] = []
|
|
group['to_addrs'] = {}
|
|
group['attachments_data'] = []
|
|
|
|
for report in group['reports']:
|
|
logger.info('Send Results Report: %s', report.number)
|
|
|
|
if (report.single_sending_report and not
|
|
report.single_sending_report_ready):
|
|
logger.warning('Send Results Report: %s: '
|
|
'IGNORED: NOT READY TO SINGLE SENDING',
|
|
report.number)
|
|
continue
|
|
|
|
report_cache = {}
|
|
for lang in Lang.search([('translatable', '=', True)]):
|
|
if not report.has_report_cached(lang):
|
|
continue
|
|
report_cache[lang] = None
|
|
|
|
try:
|
|
report_cache[lang] = report.build_report(lang)
|
|
except Exception as e:
|
|
break
|
|
|
|
if not report_cache:
|
|
logger.warning('Send Results Report: %s: '
|
|
'IGNORED: HAS NO CACHED REPORTS',
|
|
report.number)
|
|
continue
|
|
|
|
if None in report_cache.values():
|
|
reports_not_ready.append(report)
|
|
logger.warning('Send Results Report: %s: '
|
|
'IGNORED: GLOBAL REPORT BUILD FAILED',
|
|
report.number)
|
|
continue
|
|
|
|
logger.info('Send Results Report: %s: Build',
|
|
report.number)
|
|
group['reports_ready'].append(report)
|
|
|
|
for lang, cache in report_cache.items():
|
|
report.attach_report(cache, lang)
|
|
logger.info('Send Results Report: %s: Attached (%s)' % (
|
|
report.number, lang.name))
|
|
group['attachments_data'].append(
|
|
report.get_attached_report(cache, lang))
|
|
|
|
if group['cie_fraction_type']:
|
|
group['to_addrs'][email_qa] = 'QA'
|
|
else:
|
|
group['to_addrs'].update(self.get_report_addrs(
|
|
report))
|
|
|
|
if not group['reports_ready']:
|
|
continue
|
|
|
|
# Email sending
|
|
to_addrs = list(group['to_addrs'].keys())
|
|
if not to_addrs:
|
|
reports_not_sent.extend(group['reports_ready'])
|
|
logger.warning('Send Results Report: Missing addresses')
|
|
continue
|
|
logger.info('Send Results Report: To addresses: %s',
|
|
', '.join(to_addrs))
|
|
|
|
subject, body = self._get_subject_body(group['reports_ready'])
|
|
|
|
msg = self._create_msg(from_addr, to_addrs, subject,
|
|
body, hide_recipients, group['attachments_data'])
|
|
sent = self._send_msg(from_addr, to_addrs, msg)
|
|
if not sent:
|
|
reports_not_sent.extend(group['reports_ready'])
|
|
logger.warning('Send Results Report: Not sent')
|
|
continue
|
|
logger.info('Send Results Report: Sent')
|
|
|
|
addresses = ', '.join(['"%s" <%s>' % (v, k)
|
|
for k, v in group['to_addrs'].items()])
|
|
ResultsReport.write(group['reports_ready'], {
|
|
'sent': True, 'sent_date': datetime.now(),
|
|
'mailings': [('create', [{'addresses': addresses}])],
|
|
})
|
|
Transaction().commit()
|
|
|
|
if reports_not_ready or reports_not_sent:
|
|
logger.warning('Send Results Report: FAILED')
|
|
self.failed.reports_not_ready = reports_not_ready
|
|
self.failed.reports_not_sent = reports_not_sent
|
|
return 'failed'
|
|
|
|
logger.info('Send Results Report: SUCCEED')
|
|
return 'succeed'
|
|
|
|
def get_grouped_reports(self, report_ids):
|
|
pool = Pool()
|
|
Config = pool.get('lims.configuration')
|
|
ResultsReport = pool.get('lims.results_report')
|
|
|
|
config = Config(1)
|
|
|
|
res = {}
|
|
results_reports = ResultsReport.browse(report_ids)
|
|
|
|
if not config.mail_ack_report_grouping:
|
|
for report in results_reports:
|
|
if (report.invoice_party and
|
|
hasattr(report.invoice_party,
|
|
'block_reports_automatic_sending') and
|
|
getattr(report.invoice_party,
|
|
'block_reports_automatic_sending', False)):
|
|
continue
|
|
res[report.id] = {
|
|
'cie_fraction_type': report.cie_fraction_type,
|
|
'reports': [report],
|
|
}
|
|
return res
|
|
|
|
if config.mail_ack_report_grouping == 'party':
|
|
for report in results_reports:
|
|
if (report.invoice_party and
|
|
hasattr(report.invoice_party,
|
|
'block_reports_automatic_sending') and
|
|
getattr(report.invoice_party,
|
|
'block_reports_automatic_sending', False)):
|
|
continue
|
|
key = (report.party.id, report.cie_fraction_type)
|
|
if key not in res:
|
|
res[key] = {
|
|
'cie_fraction_type': report.cie_fraction_type,
|
|
'reports': [],
|
|
}
|
|
res[key]['reports'].append(report)
|
|
return res
|
|
|
|
return res
|
|
|
|
def get_report_addrs(self, report):
|
|
pool = Pool()
|
|
ResultsSample = pool.get('lims.results_report.version.detail.sample')
|
|
|
|
to_addrs = {}
|
|
|
|
samples = ResultsSample.search([
|
|
('version_detail.report_version.results_report', '=', report),
|
|
])
|
|
for sample in samples:
|
|
entry = sample.notebook.fraction.entry
|
|
if (hasattr(entry.invoice_party,
|
|
'block_reports_automatic_sending') and
|
|
getattr(entry.invoice_party,
|
|
'block_reports_automatic_sending')):
|
|
continue
|
|
for c in entry.report_contacts:
|
|
if c.contact.report_contact:
|
|
to_addrs[c.contact.email] = (
|
|
c.contact.party_full_name)
|
|
return to_addrs
|
|
|
|
def _get_subject_body(self, reports):
|
|
'''
|
|
Subject Body
|
|
'''
|
|
pool = Pool()
|
|
Config = pool.get('lims.configuration')
|
|
User = pool.get('res.user')
|
|
Lang = pool.get('ir.lang')
|
|
|
|
config = Config(1)
|
|
|
|
lang = User(Transaction().user).language
|
|
if not lang:
|
|
lang, = Lang.search([
|
|
('code', '=', 'en'),
|
|
], limit=1)
|
|
|
|
report_list = ', '.join([r.number for r in reports])
|
|
sample_list = self._get_sample_list(reports, language=lang.code)
|
|
with Transaction().set_context(language=lang.code):
|
|
if len(sample_list) == 1:
|
|
label = '%s' % sample_list[0]
|
|
else:
|
|
label = gettext('lims_email.msg_polisample')
|
|
subject = str('%s %s (%s)' % (
|
|
config.mail_ack_report_subject,
|
|
report_list, label)).strip()
|
|
body = str(config.mail_ack_report_body)
|
|
|
|
body = body.replace('<SAMPLES>', '\n'.join(sample_list))
|
|
body = body.replace('<SAMPLES>', '\n'.join(sample_list))
|
|
return subject, body
|
|
|
|
def _get_sample_list(self, reports, language):
|
|
pool = Pool()
|
|
ResultsSample = pool.get('lims.results_report.version.detail.sample')
|
|
|
|
with Transaction().set_context(language=language):
|
|
samples = ResultsSample.search([
|
|
('version_detail.report_version.results_report',
|
|
'in', reports),
|
|
('version_detail.valid', '=', True),
|
|
])
|
|
if not samples:
|
|
return []
|
|
res = []
|
|
for s in samples:
|
|
res.append(s.notebook.label or s.notebook.rec_name)
|
|
return sorted(list(set(res)), key=lambda x: x)
|
|
|
|
def _create_msg(self, from_addr, to_addrs, subject, body,
|
|
hide_recipients, attachments_data=[]):
|
|
if not to_addrs:
|
|
return None
|
|
|
|
msg = MIMEMultipart('mixed')
|
|
msg['From'] = from_addr
|
|
if not hide_recipients:
|
|
msg['To'] = ', '.join(to_addrs)
|
|
msg['Subject'] = subject
|
|
|
|
msg_body = MIMEText('text', 'plain')
|
|
msg_body.set_payload(body.encode('UTF-8'), 'UTF-8')
|
|
msg.attach(msg_body)
|
|
|
|
for attachment_data in attachments_data:
|
|
attachment = MIMEBase('application', 'octet-stream')
|
|
attachment.set_payload(attachment_data['content'])
|
|
encoders.encode_base64(attachment)
|
|
attachment.add_header('Content-Disposition', 'attachment',
|
|
filename=attachment_data['filename'])
|
|
msg.attach(attachment)
|
|
return msg
|
|
|
|
def _send_msg(self, from_addr, to_addrs, msg):
|
|
to_addrs = list(set(to_addrs))
|
|
success = False
|
|
try:
|
|
server = get_smtp_server()
|
|
server.sendmail(from_addr, to_addrs, msg.as_string())
|
|
server.quit()
|
|
success = True
|
|
except Exception as e:
|
|
logger.error('Send Results Report: Unable to deliver mail')
|
|
logger.error(str(e))
|
|
return success
|
|
|
|
def default_failed(self, fields):
|
|
default = {
|
|
'reports_not_ready': [f.id for f in self.failed.reports_not_ready],
|
|
'reports_not_sent': [f.id for f in self.failed.reports_not_sent],
|
|
}
|
|
return default
|
|
|
|
|
|
class ReportNameFormat(ModelSQL, ModelView):
|
|
'Results Report Name Format'
|
|
__name__ = 'lims.result_report.format'
|
|
|
|
name = fields.Char('Name')
|
|
format_ = fields.Char('Format', required=True,
|
|
help=("Available variables (also in upper case):"
|
|
"\n- ${number}"
|
|
"\n- ${sample_number}"
|
|
"\n- ${party_name}"))
|
|
|
|
@classmethod
|
|
def default_format_(cls):
|
|
return '${number}'
|
|
|
|
@classmethod
|
|
def validate(cls, formats):
|
|
super().validate(formats)
|
|
for format_ in formats:
|
|
format_.check_format()
|
|
|
|
def check_format(self):
|
|
pool = Pool()
|
|
ResultsReport = pool.get('lims.results_report')
|
|
report = ResultsReport()
|
|
try:
|
|
Template(self.format_).substitute(
|
|
**report._get_name_substitutions())
|
|
except Exception as exception:
|
|
raise UserError(gettext('lims_email.msg_invalid_report_name',
|
|
format=self.format_,
|
|
exception=exception)) from exception
|
|
|
|
@classmethod
|
|
def get_format(cls, results_report):
|
|
Config = Pool().get('lims.configuration')
|
|
|
|
if results_report.party.result_report_format:
|
|
return results_report.party.result_report_format.format_
|
|
|
|
config_ = Config(1)
|
|
if config_.result_report_format:
|
|
return config_.result_report_format.format_
|
|
|
|
return cls.default_format_()
|