models.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from lxml.builder import E
  4. from markupsafe import Markup
  5. from odoo import api, models, tools, _
  6. class BaseModel(models.AbstractModel):
  7. _inherit = 'base'
  8. def _valid_field_parameter(self, field, name):
  9. # allow tracking on abstract models; see also 'mail.thread'
  10. return (
  11. name == 'tracking' and self._abstract
  12. or super()._valid_field_parameter(field, name)
  13. )
  14. # ------------------------------------------------------------
  15. # GENERIC MAIL FEATURES
  16. # ------------------------------------------------------------
  17. def _mail_track(self, tracked_fields, initial):
  18. """ For a given record, fields to check (tuple column name, column info)
  19. and initial values, return a valid command to create tracking values.
  20. :param tracked_fields: fields_get of updated fields on which tracking
  21. is checked and performed;
  22. :param initial: dict of initial values for each updated fields;
  23. :return: a tuple (changes, tracking_value_ids) where
  24. changes: set of updated column names;
  25. tracking_value_ids: a list of ORM (0, 0, values) commands to create
  26. ``mail.tracking.value`` records;
  27. Override this method on a specific model to implement model-specific
  28. behavior. Also consider inheriting from ``mail.thread``. """
  29. self.ensure_one()
  30. changes = set() # contains onchange tracked fields that changed
  31. tracking_value_ids = []
  32. # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
  33. for col_name, col_info in tracked_fields.items():
  34. if col_name not in initial:
  35. continue
  36. initial_value = initial[col_name]
  37. new_value = self[col_name]
  38. if new_value != initial_value and (new_value or initial_value): # because browse null != False
  39. tracking_sequence = getattr(self._fields[col_name], 'tracking',
  40. getattr(self._fields[col_name], 'track_sequence', 100)) # backward compatibility with old parameter name
  41. if tracking_sequence is True:
  42. tracking_sequence = 100
  43. tracking = self.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info, tracking_sequence, self._name)
  44. if tracking:
  45. if tracking['field_type'] == 'monetary':
  46. tracking['currency_id'] = self[col_info['currency_field']].id
  47. tracking_value_ids.append([0, 0, tracking])
  48. changes.add(col_name)
  49. return changes, tracking_value_ids
  50. def _message_get_default_recipients(self):
  51. """ Generic implementation for finding default recipient to mail on
  52. a recordset. This method is a generic implementation available for
  53. all models as we could send an email through mail templates on models
  54. not inheriting from mail.thread.
  55. Override this method on a specific model to implement model-specific
  56. behavior. Also consider inheriting from ``mail.thread``. """
  57. res = {}
  58. for record in self:
  59. recipient_ids, email_to, email_cc = [], False, False
  60. if 'partner_id' in record and record.partner_id:
  61. recipient_ids.append(record.partner_id.id)
  62. else:
  63. found_email = False
  64. if 'email_from' in record and record.email_from:
  65. found_email = record.email_from
  66. elif 'partner_email' in record and record.partner_email:
  67. found_email = record.partner_email
  68. elif 'email' in record and record.email:
  69. found_email = record.email
  70. elif 'email_normalized' in record and record.email_normalized:
  71. found_email = record.email_normalized
  72. if found_email:
  73. email_to = ','.join(tools.email_normalize_all(found_email))
  74. if not email_to: # keep value to ease debug / trace update
  75. email_to = found_email
  76. res[record.id] = {'partner_ids': recipient_ids, 'email_to': email_to, 'email_cc': email_cc}
  77. return res
  78. def _notify_get_reply_to(self, default=None):
  79. """ Returns the preferred reply-to email address when replying to a thread
  80. on documents. This method is a generic implementation available for
  81. all models as we could send an email through mail templates on models
  82. not inheriting from mail.thread.
  83. Reply-to is formatted like "MyCompany MyDocument <reply.to@domain>".
  84. Heuristic it the following:
  85. * search for specific aliases as they always have priority; it is limited
  86. to aliases linked to documents (like project alias for task for example);
  87. * use catchall address;
  88. * use default;
  89. This method can be used as a generic tools if self is a void recordset.
  90. Override this method on a specific model to implement model-specific
  91. behavior. Also consider inheriting from ``mail.thread``.
  92. An example would be tasks taking their reply-to alias from their project.
  93. :param default: default email if no alias or catchall is found;
  94. :return result: dictionary. Keys are record IDs and value is formatted
  95. like an email "Company_name Document_name <reply_to@email>"/
  96. """
  97. _records = self
  98. model = _records._name if _records and _records._name != 'mail.thread' else False
  99. res_ids = _records.ids if _records and model else []
  100. _res_ids = res_ids or [False] # always have a default value located in False
  101. alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
  102. result = dict.fromkeys(_res_ids, False)
  103. result_email = dict()
  104. doc_names = dict()
  105. if alias_domain:
  106. if model and res_ids:
  107. if not doc_names:
  108. doc_names = dict((rec.id, rec.display_name) for rec in _records)
  109. mail_aliases = self.env['mail.alias'].sudo().search([
  110. ('alias_parent_model_id.model', '=', model),
  111. ('alias_parent_thread_id', 'in', res_ids),
  112. ('alias_name', '!=', False)])
  113. # take only first found alias for each thread_id, to match order (1 found -> limit=1 for each res_id)
  114. for alias in mail_aliases:
  115. result_email.setdefault(alias.alias_parent_thread_id, '%s@%s' % (alias.alias_name, alias_domain))
  116. # left ids: use catchall
  117. left_ids = set(_res_ids) - set(result_email)
  118. if left_ids:
  119. catchall = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias")
  120. if catchall:
  121. result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
  122. for res_id in result_email:
  123. result[res_id] = self._notify_get_reply_to_formatted_email(
  124. result_email[res_id],
  125. doc_names.get(res_id) or '',
  126. )
  127. left_ids = set(_res_ids) - set(result_email)
  128. if left_ids:
  129. result.update(dict((res_id, default) for res_id in left_ids))
  130. return result
  131. def _notify_get_reply_to_formatted_email(self, record_email, record_name):
  132. """ Compute formatted email for reply_to and try to avoid refold issue
  133. with python that splits the reply-to over multiple lines. It is due to
  134. a bad management of quotes (missing quotes after refold). This appears
  135. therefore only when having quotes (aka not simple names, and not when
  136. being unicode encoded).
  137. To avoid that issue when formataddr would return more than 78 chars we
  138. return a simplified name/email to try to stay under 78 chars. If not
  139. possible we return only the email and skip the formataddr which causes
  140. the issue in python. We do not use hacks like crop the name part as
  141. encoding and quoting would be error prone.
  142. """
  143. # address itself is too long for 78 chars limit: return only email
  144. if len(record_email) >= 78:
  145. return record_email
  146. if 'company_id' in self and len(self.company_id) == 1:
  147. company_name = self.sudo().company_id.name
  148. else:
  149. company_name = self.env.company.name
  150. # try company_name + record_name, or record_name alone (or company_name alone)
  151. name = f"{company_name} {record_name}" if record_name else company_name
  152. formatted_email = tools.formataddr((name, record_email))
  153. if len(formatted_email) > 78:
  154. formatted_email = tools.formataddr((record_name or company_name, record_email))
  155. if len(formatted_email) > 78:
  156. formatted_email = record_email
  157. return formatted_email
  158. # ------------------------------------------------------------
  159. # ALIAS MANAGEMENT
  160. # ------------------------------------------------------------
  161. def _alias_get_error_message(self, message, message_dict, alias):
  162. """ Generic method that takes a record not necessarily inheriting from
  163. mail.alias.mixin. """
  164. author = self.env['res.partner'].browse(message_dict.get('author_id', False))
  165. if alias.alias_contact == 'followers':
  166. if not self.ids:
  167. return _('incorrectly configured alias (unknown reference record)')
  168. if not hasattr(self, "message_partner_ids"):
  169. return _('incorrectly configured alias')
  170. if not author or author not in self.message_partner_ids:
  171. return _('restricted to followers')
  172. elif alias.alias_contact == 'partners' and not author:
  173. return _('restricted to known authors')
  174. return False
  175. # ------------------------------------------------------------
  176. # ACTIVITY
  177. # ------------------------------------------------------------
  178. @api.model
  179. def _get_default_activity_view(self):
  180. """ Generates an empty activity view.
  181. :returns: a activity view as an lxml document
  182. :rtype: etree._Element
  183. """
  184. field = E.field(name=self._rec_name_fallback())
  185. activity_box = E.div(field, {'t-name': "activity-box"})
  186. templates = E.templates(activity_box)
  187. return E.activity(templates, string=self._description)
  188. # ------------------------------------------------------------
  189. # GATEWAY: NOTIFICATION
  190. # ------------------------------------------------------------
  191. def _mail_get_message_subtypes(self):
  192. return self.env['mail.message.subtype'].search([
  193. '&', ('hidden', '=', False),
  194. '|', ('res_model', '=', self._name), ('res_model', '=', False)])
  195. def _notify_by_email_get_headers(self):
  196. """ Generate the email headers based on record """
  197. if not self:
  198. return {}
  199. self.ensure_one()
  200. return {
  201. 'X-Odoo-Objects': "%s-%s" % (self._name, self.id),
  202. }
  203. # ------------------------------------------------------------
  204. # TOOLS
  205. # ------------------------------------------------------------
  206. def _get_html_link(self, title=None):
  207. """Generate the record html reference for chatter use.
  208. :param str title: optional reference title, the record display_name
  209. is used if not provided. The title/display_name will be escaped.
  210. :returns: generated html reference,
  211. in the format <a href data-oe-model="..." data-oe-id="...">title</a>
  212. :rtype: str
  213. """
  214. self.ensure_one()
  215. return Markup("<a href=# data-oe-model='%s' data-oe-id='%s'>%s</a>") % (
  216. self._name, self.id, title or self.display_name)