first commit
This commit is contained in:
352
modules/notification_email/notification.py
Normal file
352
modules/notification_email/notification.py
Normal file
@@ -0,0 +1,352 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import mimetypes
|
||||
from email.utils import getaddresses
|
||||
|
||||
from genshi.template import TextTemplate
|
||||
|
||||
import trytond.config as config
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, TimeDelta
|
||||
from trytond.report import get_email
|
||||
from trytond.sendmail import SMTPDataManager, send_message_transactional
|
||||
from trytond.tools import slugify
|
||||
from trytond.tools.email_ import format_address, has_rcpt, set_from_header
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from .exceptions import TemplateError
|
||||
|
||||
|
||||
class Email(ModelSQL, ModelView):
|
||||
__name__ = 'notification.email'
|
||||
|
||||
from_ = fields.Char(
|
||||
"From", translate=True,
|
||||
help="Leave empty for the value defined in the configuration file.")
|
||||
subject = fields.Char(
|
||||
"Subject", translate=True,
|
||||
help="The Genshi syntax can be used "
|
||||
"with 'record' in the evaluation context.\n"
|
||||
"If empty the report name will be used.")
|
||||
recipients = fields.Many2One(
|
||||
'ir.model.field', "Recipients",
|
||||
domain=[
|
||||
('model', '=', Eval('model')),
|
||||
],
|
||||
help="The field that contains the recipient(s).")
|
||||
fallback_recipients = fields.Many2One(
|
||||
'res.user', "Recipients Fallback",
|
||||
domain=[
|
||||
('email', '!=', None),
|
||||
],
|
||||
help="User notified when no recipients email is found.")
|
||||
recipients_secondary = fields.Many2One(
|
||||
'ir.model.field', "Secondary Recipients",
|
||||
domain=[
|
||||
('model', '=', Eval('model')),
|
||||
],
|
||||
help="The field that contains the secondary recipient(s).")
|
||||
fallback_recipients_secondary = fields.Many2One(
|
||||
'res.user', "Secondary Recipients Fallback",
|
||||
domain=[
|
||||
('email', '!=', None),
|
||||
],
|
||||
help="User notified when no secondary recipients email is found.")
|
||||
recipients_hidden = fields.Many2One(
|
||||
'ir.model.field', "Hidden Recipients",
|
||||
domain=[
|
||||
('model', '=', Eval('model')),
|
||||
],
|
||||
help="The field that contains the hidden recipient(s).")
|
||||
fallback_recipients_hidden = fields.Many2One(
|
||||
'res.user', "Hidden Recipients Fallback",
|
||||
domain=[
|
||||
('email', '!=', None),
|
||||
],
|
||||
help="User notified when no hidden recipients email is found.")
|
||||
|
||||
contact_mechanism = fields.Selection(
|
||||
'get_contact_mechanisms', "Contact Mechanism",
|
||||
help="Define which email to use from the party's contact mechanisms")
|
||||
|
||||
content = fields.Many2One(
|
||||
'ir.action.report', "Content", required=True,
|
||||
domain=[('template_extension', 'in', ['txt', 'html', 'xhtml'])],
|
||||
help="The report used as email template.")
|
||||
attachments = fields.Many2Many(
|
||||
'notification.email.attachment', 'notification', 'report',
|
||||
"Attachments",
|
||||
domain=[
|
||||
('model', '=', Eval('model')),
|
||||
],
|
||||
help="The reports used as attachments.")
|
||||
|
||||
triggers = fields.One2Many(
|
||||
'ir.trigger', 'notification_email', "Triggers",
|
||||
domain=[('model.name', '=', Eval('model'))],
|
||||
help="Add a trigger for the notification.")
|
||||
send_after = fields.TimeDelta(
|
||||
"Send After",
|
||||
domain=['OR',
|
||||
('send_after', '=', None),
|
||||
('send_after', '>=', TimeDelta()),
|
||||
],
|
||||
help="The delay after which the email must be sent.\n"
|
||||
"Applied if a worker queue is activated.")
|
||||
|
||||
model = fields.Function(
|
||||
fields.Char("Model"), 'on_change_with_model', searcher='search_model')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
pool = Pool()
|
||||
EmailTemplate = pool.get('ir.email.template')
|
||||
super().__setup__()
|
||||
for field in [
|
||||
'recipients',
|
||||
'recipients_secondary',
|
||||
'recipients_hidden',
|
||||
]:
|
||||
field = getattr(cls, field)
|
||||
field.domain.append(['OR',
|
||||
('relation', 'in', EmailTemplate.email_models()),
|
||||
[
|
||||
('model.name', 'in', EmailTemplate.email_models()),
|
||||
('name', '=', 'id'),
|
||||
],
|
||||
])
|
||||
|
||||
def get_rec_name(self, name):
|
||||
return self.content.rec_name
|
||||
|
||||
@classmethod
|
||||
def search_rec_name(cls, name, clause):
|
||||
return [('content',) + tuple(clause[1:])]
|
||||
|
||||
@classmethod
|
||||
def get_contact_mechanisms(cls):
|
||||
pool = Pool()
|
||||
try:
|
||||
ContactMechanism = pool.get('party.contact_mechanism')
|
||||
except KeyError:
|
||||
return [(None, "")]
|
||||
return ContactMechanism.usages()
|
||||
|
||||
@fields.depends('content')
|
||||
def on_change_with_model(self, name=None):
|
||||
if self.content:
|
||||
return self.content.model
|
||||
|
||||
@classmethod
|
||||
def search_model(cls, name, clause):
|
||||
return [('content.model',) + tuple(clause[1:])]
|
||||
|
||||
def _get_addresses(self, value):
|
||||
pool = Pool()
|
||||
EmailTemplate = pool.get('ir.email.template')
|
||||
with Transaction().set_context(usage=self.contact_mechanism):
|
||||
# reformat addresses with encoding
|
||||
return [
|
||||
format_address(e, n)
|
||||
for n, e in getaddresses(EmailTemplate.get_addresses(value))]
|
||||
|
||||
def _get_languages(self, value):
|
||||
pool = Pool()
|
||||
EmailTemplate = pool.get('ir.email.template')
|
||||
with Transaction().set_context(usage=self.contact_mechanism):
|
||||
return EmailTemplate.get_languages(value)
|
||||
|
||||
def get_email(self, record, sender, to, cc, bcc, languages):
|
||||
pool = Pool()
|
||||
Attachment = pool.get('notification.email.attachment')
|
||||
|
||||
# TODO order languages to get default as last one for title
|
||||
msg, title = get_email(self.content, record, languages)
|
||||
if languages:
|
||||
language = list(languages)[-1].code
|
||||
else:
|
||||
language = None
|
||||
from_ = sender
|
||||
with Transaction().set_context(language=language):
|
||||
notification = self.__class__(self.id)
|
||||
if notification.from_:
|
||||
from_ = notification.from_
|
||||
if self.subject:
|
||||
title = (TextTemplate(notification.subject)
|
||||
.generate(record=record)
|
||||
.render())
|
||||
|
||||
if self.attachments:
|
||||
for report in self.attachments:
|
||||
name, data = Attachment.get(report, record, language)
|
||||
ctype, _ = mimetypes.guess_type(name)
|
||||
if not ctype:
|
||||
ctype = 'application/octet-stream'
|
||||
maintype, subtype = ctype.split('/', 1)
|
||||
msg.add_attachment(
|
||||
data,
|
||||
maintype=maintype,
|
||||
subtype=subtype,
|
||||
filename=('utf-8', '', name))
|
||||
|
||||
set_from_header(msg, sender, from_)
|
||||
if to:
|
||||
msg['To'] = to
|
||||
if cc:
|
||||
msg['Cc'] = cc
|
||||
if bcc:
|
||||
msg['Bcc'] = bcc
|
||||
msg['Subject'] = title
|
||||
msg['Auto-Submitted'] = 'auto-generated'
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def trigger(cls, records, trigger):
|
||||
"Action function for the triggers"
|
||||
notification_email = trigger.notification_email
|
||||
if not notification_email:
|
||||
raise ValueError(
|
||||
'Trigger "%s" is not related to any email notification'
|
||||
% trigger.rec_name)
|
||||
if notification_email.send_after:
|
||||
with Transaction().set_context(
|
||||
queue_scheduled_at=trigger.notification_email.send_after):
|
||||
notification_email.__class__.__queue__._send_email_queued(
|
||||
notification_email, [r.id for r in records], trigger.id)
|
||||
else:
|
||||
notification_email.send_email(records, trigger)
|
||||
|
||||
def _send_email_queued(self, ids, trigger_id):
|
||||
pool = Pool()
|
||||
Model = pool.get(self.model)
|
||||
Trigger = pool.get('ir.trigger')
|
||||
records = Model.browse(ids)
|
||||
trigger = Trigger(trigger_id)
|
||||
self.send_email(records, trigger)
|
||||
|
||||
def send_email(self, records, trigger):
|
||||
pool = Pool()
|
||||
Email = pool.get('ir.email')
|
||||
datamanager = SMTPDataManager()
|
||||
Transaction().join(datamanager)
|
||||
from_ = (config.get('notification_email', 'from')
|
||||
or config.get('email', 'from'))
|
||||
emails = []
|
||||
for record in records:
|
||||
to, to_languages = self._get_to(record)
|
||||
cc, cc_languages = self._get_cc(record)
|
||||
bcc, bcc_languages = self._get_bcc(record)
|
||||
languages = to_languages | cc_languages | bcc_languages
|
||||
msg = self.get_email(record, from_, to, cc, bcc, languages)
|
||||
if has_rcpt(msg):
|
||||
send_message_transactional(msg, datamanager=datamanager)
|
||||
emails.append(Email.from_message(
|
||||
msg, resource=record,
|
||||
notification_email=trigger.notification_email,
|
||||
notification_trigger=trigger))
|
||||
Email.save(emails)
|
||||
|
||||
def _get_recipients(self, record, name):
|
||||
if name == 'id':
|
||||
return record
|
||||
else:
|
||||
return getattr(record, name, None)
|
||||
|
||||
def _get_to(self, record):
|
||||
to = []
|
||||
languages = set()
|
||||
if self.recipients:
|
||||
recipients = self._get_recipients(record, self.recipients.name)
|
||||
if recipients:
|
||||
languages.update(self._get_languages(recipients))
|
||||
to = self._get_addresses(recipients)
|
||||
if not to and self.fallback_recipients:
|
||||
languages.update(
|
||||
self._get_languages(self.fallback_recipients))
|
||||
to = self._get_addresses(self.fallback_recipients)
|
||||
return to, languages
|
||||
|
||||
def _get_cc(self, record):
|
||||
cc = []
|
||||
languages = set()
|
||||
if self.recipients_secondary:
|
||||
recipients_secondary = self._get_recipients(
|
||||
record, self.recipients_secondary.name)
|
||||
if recipients_secondary:
|
||||
languages.update(
|
||||
self._get_languages(recipients_secondary))
|
||||
cc = self._get_addresses(recipients_secondary)
|
||||
if not cc and self.fallback_recipients_secondary:
|
||||
languages.update(
|
||||
self._get_languages(self.fallback_recipients_secondary))
|
||||
cc = self._get_addresses(self.fallback_recipients_secondary)
|
||||
return cc, languages
|
||||
|
||||
def _get_bcc(self, record):
|
||||
bcc = []
|
||||
languages = set()
|
||||
if self.recipients_hidden:
|
||||
recipients_hidden = self._get_recipients(
|
||||
record, self.recipients_hidden.name)
|
||||
if recipients_hidden:
|
||||
languages.update(self._get_languages(recipients_hidden))
|
||||
bcc = self._get_addresses(recipients_hidden)
|
||||
if not bcc and self.fallback_recipients_hidden:
|
||||
languages.update(
|
||||
self._get_languages(self.fallback_recipients_hidden))
|
||||
bcc = self._get_addresses(self.fallback_recipients_hidden)
|
||||
return bcc, languages
|
||||
|
||||
@classmethod
|
||||
def validate_fields(cls, notifications, field_names):
|
||||
super().validate_fields(notifications, field_names)
|
||||
cls.check_subject(notifications, field_names)
|
||||
|
||||
@classmethod
|
||||
def check_subject(cls, notifications, field_names=None):
|
||||
if field_names and 'subject' not in field_names:
|
||||
return
|
||||
for notification in notifications:
|
||||
if not notification.subject:
|
||||
continue
|
||||
try:
|
||||
TextTemplate(notification.subject)
|
||||
except Exception as exception:
|
||||
raise TemplateError(gettext(
|
||||
'notification_email.'
|
||||
'msg_notification_invalid_subject',
|
||||
notification=notification.rec_name,
|
||||
exception=exception)) from exception
|
||||
|
||||
|
||||
class EmailAttachment(ModelSQL):
|
||||
__name__ = 'notification.email.attachment'
|
||||
|
||||
notification = fields.Many2One(
|
||||
'notification.email', "Notification", required=True)
|
||||
report = fields.Many2One(
|
||||
'ir.action.report', "Report", required=True,
|
||||
domain=[
|
||||
('model', '=', Eval('model')),
|
||||
])
|
||||
|
||||
model = fields.Function(fields.Char("Model"), 'get_model')
|
||||
|
||||
def get_model(self, name):
|
||||
return self.notification.model
|
||||
|
||||
@classmethod
|
||||
def get(cls, report, record, language):
|
||||
pool = Pool()
|
||||
Report = pool.get(report.report_name, type='report')
|
||||
with Transaction().set_context(language=language):
|
||||
ext, content, _, title = Report.execute(
|
||||
[record.id], {
|
||||
'action_id': report.id,
|
||||
})
|
||||
name = '%s.%s' % (slugify(title), ext)
|
||||
if isinstance(content, str):
|
||||
content = content.encode('utf-8')
|
||||
return name, content
|
||||
Reference in New Issue
Block a user