You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
9.6 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2017 Simone Orsi <simone.orsi@camptocamp.com>
  3. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
  4. from odoo import fields, models, api, exceptions, tools, _
  5. import logging
  6. logger = logging.getLogger('[mail_digest]')
  7. class MailDigest(models.Model):
  8. _name = 'mail.digest'
  9. _description = 'Mail digest'
  10. _order = 'create_date desc'
  11. name = fields.Char(
  12. string="Name",
  13. compute="_compute_name",
  14. readonly=True,
  15. )
  16. partner_id = fields.Many2one(
  17. string='Partner',
  18. comodel_name='res.partner',
  19. readonly=True,
  20. required=True,
  21. ondelete='cascade',
  22. )
  23. frequency = fields.Selection(
  24. related='partner_id.notify_frequency',
  25. readonly=True,
  26. )
  27. message_ids = fields.Many2many(
  28. comodel_name='mail.message',
  29. string='Messages'
  30. )
  31. mail_id = fields.Many2one(
  32. 'mail.mail',
  33. 'Mail',
  34. ondelete='set null',
  35. )
  36. state = fields.Selection(related='mail_id.state', readonly=True)
  37. # To my future self: never ever change this field to `template_id`.
  38. # When creating digest records within the context of mail composer
  39. # (and possibly other contexts) you'll have a `default_template_id`
  40. # key in the context which is going to override our safe default.
  41. # This is going to break email generation because the template
  42. # will be completely wrong. Lesson learned :)
  43. digest_template_id = fields.Many2one(
  44. 'ir.ui.view',
  45. 'Qweb mail template',
  46. ondelete='set null',
  47. default=lambda self: self._default_digest_template_id(),
  48. domain=[('type', '=', 'qweb')],
  49. oldname='template_id',
  50. )
  51. sanitize_msg_body = fields.Boolean(
  52. string='Sanitize message body',
  53. help='Collected messages can have different styles applied '
  54. 'on each element. If this flag is enabled (default) '
  55. 'each message content will be sanitized '
  56. 'before generating the email.',
  57. default=True,
  58. )
  59. def _default_digest_template_id(self):
  60. """Retrieve default template to render digest."""
  61. return self.env.ref('mail_digest.default_digest_tmpl',
  62. raise_if_not_found=False)
  63. @api.multi
  64. @api.depends("partner_id", "partner_id.notify_frequency")
  65. def _compute_name(self):
  66. for rec in self:
  67. rec.name = u'{} - {}'.format(
  68. rec.partner_id.name, rec._get_subject())
  69. @api.model
  70. def create_or_update(self, partners, message, subtype_id=None):
  71. """Create or update digest.
  72. :param partners: recipients as `res.partner` browse list
  73. :param message: `mail.message` to include in digest
  74. :param subtype_id: `mail.message.subtype` instance
  75. """
  76. subtype_id = subtype_id or message.subtype_id
  77. for partner in partners:
  78. digest = self._get_or_create_by_partner(partner, message)
  79. digest.message_ids |= message
  80. return True
  81. @api.model
  82. def _get_by_partner(self, partner, mail_id=False):
  83. """Retrieve digest record for given partner.
  84. :param partner: `res.partner` browse record
  85. :param mail_id: `mail.mail` record for further filtering.
  86. By default we lookup for pending digest without notification yet.
  87. """
  88. domain = [
  89. ('partner_id', '=', partner.id),
  90. ('mail_id', '=', mail_id),
  91. ]
  92. return self.search(domain, limit=1)
  93. @api.model
  94. def _get_or_create_by_partner(self, partner, message=None, mail_id=False):
  95. """Retrieve digest record or create it by partner.
  96. :param partner: `res.partner` record to create/get digest for
  97. :param message: `mail.message` to include in digest
  98. :param mail_id: `mail.mail` record to set on digest
  99. """
  100. existing = self._get_by_partner(partner, mail_id=mail_id)
  101. if existing:
  102. return existing
  103. values = {'partner_id': partner.id, }
  104. return self.create(values)
  105. @api.model
  106. def _message_group_by_key(self, msg):
  107. """Return the key to group messages by."""
  108. return msg.subtype_id.id
  109. @api.multi
  110. def _message_group_by(self):
  111. """Group digest messages.
  112. A digest can contain several messages.
  113. To display them in a nice and organized form in your emails
  114. we group them by subtype by default.
  115. """
  116. self.ensure_one()
  117. grouped = {}
  118. for msg in self.message_ids:
  119. grouped.setdefault(self._message_group_by_key(msg), []).append(msg)
  120. return grouped
  121. @api.model
  122. def message_body(self, msg, strip_style=True):
  123. """Return body message prepared for email content.
  124. Message's body can contains styles and other stuff
  125. that can screw the look and feel of digests' mails.
  126. Here we sanitize it if `sanitize_msg_body` is set on the digest.
  127. """
  128. if not self.sanitize_msg_body:
  129. return msg.body
  130. return tools.html_sanitize(msg.body or '', strip_style=strip_style)
  131. def _get_site_name(self):
  132. """Retrieve site name for meaningful mail subject.
  133. If you run a website we get website's name
  134. otherwise we default to current user's company name.
  135. """
  136. # default to company
  137. name = self.env.user.company_id.name
  138. if 'website' in self.env:
  139. # TODO: shall we make this configurable at digest or global level?
  140. # Maybe you have a website but
  141. # your digest msgs are not related to it at all or partially.
  142. ws = None
  143. try:
  144. ws = self.env['website'].get_current_website()
  145. name = ws.name
  146. except RuntimeError:
  147. # RuntimeError: object unbound -> no website request.
  148. # Fallback to default website if any.
  149. ws = self.env['website'].search([], limit=1)
  150. if ws:
  151. name = ws.name
  152. return name
  153. @api.multi
  154. def _get_subject(self):
  155. """Build the full subject for digest's mail."""
  156. # TODO: shall we move this to computed field?
  157. self.ensure_one()
  158. subject = u'[{}] '.format(self._get_site_name())
  159. if self.partner_id.notify_frequency == 'daily':
  160. subject += _('Daily update')
  161. elif self.partner_id.notify_frequency == 'weekly':
  162. subject += _('Weekly update')
  163. return subject
  164. @api.multi
  165. def _get_template_values(self):
  166. """Collect variables to render digest's template."""
  167. self.ensure_one()
  168. subject = self._get_subject()
  169. template_values = {
  170. 'digest': self,
  171. 'subject': subject,
  172. 'grouped_messages': self._message_group_by(),
  173. 'base_url':
  174. self.env['ir.config_parameter'].get_param('web.base.url'),
  175. }
  176. return template_values
  177. @api.multi
  178. def _get_email_values(self, template=None):
  179. """Collect variables to create digest's mail message."""
  180. self.ensure_one()
  181. template = template or self.digest_template_id
  182. if not template:
  183. raise exceptions.UserError(_(
  184. 'You must pass a template or set one on the digest record.'
  185. ))
  186. subject = self._get_subject()
  187. template_values = self._get_template_values()
  188. values = {
  189. 'email_from': self.env.user.company_id.email,
  190. 'recipient_ids': [(4, self.partner_id.id)],
  191. 'subject': subject,
  192. 'body_html': template.with_context(
  193. **self._template_context()
  194. ).render(template_values),
  195. }
  196. return values
  197. def _create_mail_context(self):
  198. """Inject context vars.
  199. By default we make sure that digest's email
  200. will have only digest's partner among recipients.
  201. """
  202. return {
  203. 'notify_only_recipients': True,
  204. }
  205. @api.multi
  206. def _template_context(self):
  207. """Rendering context for digest's template.
  208. By default we enforce partner's language.
  209. """
  210. self.ensure_one()
  211. return {
  212. 'lang': self.partner_id.lang,
  213. }
  214. @api.multi
  215. def create_email(self, template=None):
  216. """Create `mail.message` records for current digests.
  217. :param template: qweb template instance to override default digest one.
  218. """
  219. mail_model = self.env['mail.mail'].with_context(
  220. **self._create_mail_context())
  221. created = []
  222. for item in self:
  223. if not item.message_ids:
  224. # useless to create a mail for a digest w/ messages
  225. # messages could be deleted by admin for instance.
  226. continue
  227. values = item.with_context(
  228. **item._template_context()
  229. )._get_email_values(template=template)
  230. item.mail_id = mail_model.create(values)
  231. created.append(item.id)
  232. if created:
  233. logger.info('Create email for digest IDS=%s', str(created))
  234. return created
  235. @api.multi
  236. def action_create_email(self):
  237. return self.create_email()
  238. @api.model
  239. def process(self, frequency='daily', domain=None):
  240. """Process existing digest records to create emails via cron.
  241. :param frequency: lookup digest records by partners' `notify_frequency`
  242. :param domain: pass custom domain to lookup only specific digests
  243. """
  244. if not domain:
  245. domain = [
  246. ('mail_id', '=', False),
  247. ('partner_id.notify_frequency', '=', frequency),
  248. ]
  249. self.search(domain).create_email()