[MERGE] mail, various: update notification emails computation and layouting
PURPOSE Cleanup "Pay Now" notification template and their usage in "Send by email" flows. Cleanup overall notification by email flow: rename methods, remove outdated code, improve code clarity, prepare future improvements. SPECIFICATIONS Cleanup "Pay Npw" template: move buttons on the top, add subtitle, overall cleaning of the content. It now looks more like the standard notification layout with some model-based tweaks for invoices / SOs / POs, with less view-based content. Improve recipients computation: do it in batch, add more information when fetching notification recipients information (follower status, lang, ...). Add tests. Cleanup MailThread API and docstrings for notification methods. Improve flow of values through notification methods. Make their purpose clearer. Provide some master fixes for corner cases spotted while working on this PR. In any case see individual commits for more details as otherwise this message would be a long concatenation of various purposes. LINKS Task-2712450 (Mail/Sale: Improve 'Pay Now' notification template) Task-2726501 (Mail: Propagate message values in post methods) Task-2732660 (Mail: Remove duplicate layout field on composer) Task-2710804 (Mail: Clean Mail.Thread API) Task-2739294 (Mail: Batch recipients fetch and improve its usage) closes odoo/odoo#82167 Related: odoo/enterprise#23326 Related: odoo/upgrade#3142 Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
</t>
|
||||
<br /><br />
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="object.invoice_user_id.signature">
|
||||
<t t-if="not is_html_empty(object.invoice_user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.invoice_user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
@@ -75,7 +75,7 @@
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<br/><br/>
|
||||
Best regards,
|
||||
<t t-if="user.signature">
|
||||
<t t-if="not is_html_empty(user.signature)">
|
||||
<br/>
|
||||
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
@@ -119,7 +119,7 @@
|
||||
from <t t-out="object.company_id.name or ''">YourCompany</t>.
|
||||
<br /><br />
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="object.invoice_user_id.signature">
|
||||
<t t-if="not is_html_empty(object.invoice_user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.invoice_user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from odoo import api, fields, models, Command, _
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError, AccessError
|
||||
from odoo.tools import float_compare, date_utils, email_split, email_re, html_escape, is_html_empty, sql
|
||||
from odoo.tools.misc import formatLang, format_date, get_lang
|
||||
from odoo.tools.misc import format_amount, formatLang, format_date, get_lang
|
||||
|
||||
from datetime import date, timedelta
|
||||
from collections import defaultdict
|
||||
@@ -11,6 +11,7 @@ from contextlib import contextmanager
|
||||
from itertools import zip_longest
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
from markupsafe import Markup
|
||||
|
||||
import ast
|
||||
import json
|
||||
@@ -2346,41 +2347,6 @@ class AccountMove(models.Model):
|
||||
result.append((move.id, name))
|
||||
return result
|
||||
|
||||
def _creation_subtype(self):
|
||||
# OVERRIDE
|
||||
if self.move_type in ('out_invoice', 'out_refund', 'out_receipt'):
|
||||
return self.env.ref('account.mt_invoice_created')
|
||||
else:
|
||||
return super(AccountMove, self)._creation_subtype()
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
# OVERRIDE to add custom subtype depending of the state.
|
||||
self.ensure_one()
|
||||
|
||||
if not self.is_invoice(include_receipts=True):
|
||||
if self.payment_id and 'state' in init_values:
|
||||
self.payment_id._message_track(['state'], {self.payment_id.id: init_values})
|
||||
return super(AccountMove, self)._track_subtype(init_values)
|
||||
|
||||
if 'payment_state' in init_values and self.payment_state == 'paid':
|
||||
return self.env.ref('account.mt_invoice_paid')
|
||||
elif 'state' in init_values and self.state == 'posted' and self.is_sale_document(include_receipts=True):
|
||||
return self.env.ref('account.mt_invoice_validated')
|
||||
return super(AccountMove, self)._track_subtype(init_values)
|
||||
|
||||
def _creation_message(self):
|
||||
# OVERRIDE
|
||||
if not self.is_invoice(include_receipts=True):
|
||||
return super()._creation_message()
|
||||
return {
|
||||
'out_invoice': _('Invoice Created'),
|
||||
'out_refund': _('Credit Note Created'),
|
||||
'in_invoice': _('Vendor Bill Created'),
|
||||
'in_refund': _('Refund Created'),
|
||||
'out_receipt': _('Sales Receipt Created'),
|
||||
'in_receipt': _('Purchase Receipt Created'),
|
||||
}[self.move_type]
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# RECONCILIATION METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2857,59 +2823,6 @@ class AccountMove(models.Model):
|
||||
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')],
|
||||
}
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg_dict, custom_values=None):
|
||||
# OVERRIDE
|
||||
# Add custom behavior when receiving a new invoice through the mail's gateway.
|
||||
if (custom_values or {}).get('move_type', 'entry') not in ('out_invoice', 'in_invoice'):
|
||||
return super().message_new(msg_dict, custom_values=custom_values)
|
||||
|
||||
def is_internal_partner(partner):
|
||||
# Helper to know if the partner is an internal one.
|
||||
return partner.user_ids and all(user.has_group('base.group_user') for user in partner.user_ids)
|
||||
|
||||
extra_domain = False
|
||||
if custom_values.get('company_id'):
|
||||
extra_domain = ['|', ('company_id', '=', custom_values['company_id']), ('company_id', '=', False)]
|
||||
|
||||
# Search for partners in copy.
|
||||
cc_mail_addresses = email_split(msg_dict.get('cc', ''))
|
||||
followers = [partner for partner in self._mail_find_partner_from_emails(cc_mail_addresses, extra_domain) if partner]
|
||||
|
||||
# Search for partner that sent the mail.
|
||||
from_mail_addresses = email_split(msg_dict.get('from', ''))
|
||||
senders = partners = [partner for partner in self._mail_find_partner_from_emails(from_mail_addresses, extra_domain) if partner]
|
||||
|
||||
# Search for partners using the user.
|
||||
if not senders:
|
||||
senders = partners = list(self._mail_search_on_user(from_mail_addresses))
|
||||
|
||||
if partners:
|
||||
# Check we are not in the case when an internal user forwarded the mail manually.
|
||||
if is_internal_partner(partners[0]):
|
||||
# Search for partners in the mail's body.
|
||||
body_mail_addresses = set(email_re.findall(msg_dict.get('body')))
|
||||
partners = [partner for partner in self._mail_find_partner_from_emails(body_mail_addresses, extra_domain) if not is_internal_partner(partner)]
|
||||
|
||||
# Little hack: Inject the mail's subject in the body.
|
||||
if msg_dict.get('subject') and msg_dict.get('body'):
|
||||
msg_dict['body'] = '<div><div><h3>%s</h3></div>%s</div>' % (msg_dict['subject'], msg_dict['body'])
|
||||
|
||||
# Create the invoice.
|
||||
values = {
|
||||
'name': '/', # we have to give the name otherwise it will be set to the mail's subject
|
||||
'invoice_source_email': from_mail_addresses[0],
|
||||
'partner_id': partners and partners[0].id or False,
|
||||
}
|
||||
move_ctx = self.with_context(default_move_type=custom_values['move_type'], default_journal_id=custom_values['journal_id'])
|
||||
move = super(AccountMove, move_ctx).message_new(msg_dict, custom_values=values)
|
||||
move._compute_name() # because the name is given, we need to recompute in case it is the first invoice of the journal
|
||||
|
||||
# Assign followers.
|
||||
all_followers_ids = set(partner.id for partner in followers + senders + partners if is_internal_partner(partner))
|
||||
move.message_subscribe(list(all_followers_ids))
|
||||
return move
|
||||
|
||||
def post(self):
|
||||
warnings.warn(
|
||||
"RedirectWarning method 'post()' is a deprecated alias to 'action_post()' or _post()",
|
||||
@@ -3403,6 +3316,92 @@ class AccountMove(models.Model):
|
||||
|
||||
return rslt
|
||||
|
||||
def _get_create_invoice_from_attachment_decoders(self):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
|
||||
def _get_update_invoice_from_attachment_decoders(self, invoice):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
|
||||
:param invoice: The invoice on which to update the data.
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
|
||||
@api.depends('move_type', 'partner_id', 'company_id')
|
||||
def _compute_narration(self):
|
||||
use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms')
|
||||
for move in self.filtered(lambda am: not am.narration):
|
||||
if not use_invoice_terms or not move.is_sale_document(include_receipts=True):
|
||||
move.narration = False
|
||||
else:
|
||||
if not move.company_id.terms_type == 'html':
|
||||
narration = move.company_id.invoice_terms if not is_html_empty(move.company_id.invoice_terms) else ''
|
||||
else:
|
||||
baseurl = self.env.company.get_base_url() + '/terms'
|
||||
narration = _('Terms & Conditions: %s', baseurl)
|
||||
move.narration = narration or False
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAIL.THREAD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg_dict, custom_values=None):
|
||||
# OVERRIDE
|
||||
# Add custom behavior when receiving a new invoice through the mail's gateway.
|
||||
if (custom_values or {}).get('move_type', 'entry') not in ('out_invoice', 'in_invoice'):
|
||||
return super().message_new(msg_dict, custom_values=custom_values)
|
||||
|
||||
def is_internal_partner(partner):
|
||||
# Helper to know if the partner is an internal one.
|
||||
return partner.user_ids and all(user.has_group('base.group_user') for user in partner.user_ids)
|
||||
|
||||
extra_domain = False
|
||||
if custom_values.get('company_id'):
|
||||
extra_domain = ['|', ('company_id', '=', custom_values['company_id']), ('company_id', '=', False)]
|
||||
|
||||
# Search for partners in copy.
|
||||
cc_mail_addresses = email_split(msg_dict.get('cc', ''))
|
||||
followers = [partner for partner in self._mail_find_partner_from_emails(cc_mail_addresses, extra_domain) if partner]
|
||||
|
||||
# Search for partner that sent the mail.
|
||||
from_mail_addresses = email_split(msg_dict.get('from', ''))
|
||||
senders = partners = [partner for partner in self._mail_find_partner_from_emails(from_mail_addresses, extra_domain) if partner]
|
||||
|
||||
# Search for partners using the user.
|
||||
if not senders:
|
||||
senders = partners = list(self._mail_search_on_user(from_mail_addresses))
|
||||
|
||||
if partners:
|
||||
# Check we are not in the case when an internal user forwarded the mail manually.
|
||||
if is_internal_partner(partners[0]):
|
||||
# Search for partners in the mail's body.
|
||||
body_mail_addresses = set(email_re.findall(msg_dict.get('body')))
|
||||
partners = [partner for partner in self._mail_find_partner_from_emails(body_mail_addresses, extra_domain) if not is_internal_partner(partner)]
|
||||
|
||||
# Little hack: Inject the mail's subject in the body.
|
||||
if msg_dict.get('subject') and msg_dict.get('body'):
|
||||
msg_dict['body'] = '<div><div><h3>%s</h3></div>%s</div>' % (msg_dict['subject'], msg_dict['body'])
|
||||
|
||||
# Create the invoice.
|
||||
values = {
|
||||
'name': '/', # we have to give the name otherwise it will be set to the mail's subject
|
||||
'invoice_source_email': from_mail_addresses[0],
|
||||
'partner_id': partners and partners[0].id or False,
|
||||
}
|
||||
move_ctx = self.with_context(default_move_type=custom_values['move_type'], default_journal_id=custom_values['journal_id'])
|
||||
move = super(AccountMove, move_ctx).message_new(msg_dict, custom_values=values)
|
||||
move._compute_name() # because the name is given, we need to recompute in case it is the first invoice of the journal
|
||||
|
||||
# Assign followers.
|
||||
all_followers_ids = set(partner.id for partner in followers + senders + partners if is_internal_partner(partner))
|
||||
move.message_subscribe(list(all_followers_ids))
|
||||
return move
|
||||
|
||||
def _message_post_after_hook(self, new_message, message_values):
|
||||
# OVERRIDE
|
||||
# When posting a message, check the attachment to see if it's an invoice and update with the imported data.
|
||||
@@ -3437,34 +3436,56 @@ class AccountMove(models.Model):
|
||||
|
||||
return res
|
||||
|
||||
def _get_create_invoice_from_attachment_decoders(self):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
def _creation_subtype(self):
|
||||
# OVERRIDE
|
||||
if self.move_type in ('out_invoice', 'out_refund', 'out_receipt'):
|
||||
return self.env.ref('account.mt_invoice_created')
|
||||
else:
|
||||
return super(AccountMove, self)._creation_subtype()
|
||||
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
def _track_subtype(self, init_values):
|
||||
# OVERRIDE to add custom subtype depending of the state.
|
||||
self.ensure_one()
|
||||
|
||||
def _get_update_invoice_from_attachment_decoders(self, invoice):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
if not self.is_invoice(include_receipts=True):
|
||||
if self.payment_id and 'state' in init_values:
|
||||
self.payment_id._message_track(['state'], {self.payment_id.id: init_values})
|
||||
return super(AccountMove, self)._track_subtype(init_values)
|
||||
|
||||
:param invoice: The invoice on which to update the data.
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
if 'payment_state' in init_values and self.payment_state == 'paid':
|
||||
return self.env.ref('account.mt_invoice_paid')
|
||||
elif 'state' in init_values and self.state == 'posted' and self.is_sale_document(include_receipts=True):
|
||||
return self.env.ref('account.mt_invoice_validated')
|
||||
return super(AccountMove, self)._track_subtype(init_values)
|
||||
|
||||
@api.depends('move_type', 'partner_id', 'company_id')
|
||||
def _compute_narration(self):
|
||||
use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms')
|
||||
for move in self.filtered(lambda am: not am.narration):
|
||||
if not use_invoice_terms or not move.is_sale_document(include_receipts=True):
|
||||
move.narration = False
|
||||
else:
|
||||
if not move.company_id.terms_type == 'html':
|
||||
narration = move.company_id.invoice_terms if not is_html_empty(move.company_id.invoice_terms) else ''
|
||||
else:
|
||||
baseurl = self.env.company.get_base_url() + '/terms'
|
||||
narration = _('Terms & Conditions: %s', baseurl)
|
||||
move.narration = narration or False
|
||||
def _creation_message(self):
|
||||
# OVERRIDE
|
||||
if not self.is_invoice(include_receipts=True):
|
||||
return super()._creation_message()
|
||||
return {
|
||||
'out_invoice': _('Invoice Created'),
|
||||
'out_refund': _('Credit Note Created'),
|
||||
'in_invoice': _('Vendor Bill Created'),
|
||||
'in_refund': _('Refund Created'),
|
||||
'out_receipt': _('Sales Receipt Created'),
|
||||
'in_receipt': _('Purchase Receipt Created'),
|
||||
}[self.move_type]
|
||||
|
||||
def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False,
|
||||
force_email_company=False, force_email_lang=False):
|
||||
render_context = super()._notify_by_email_prepare_rendering_context(
|
||||
message, msg_vals, model_description=model_description,
|
||||
force_email_company=force_email_company, force_email_lang=force_email_lang
|
||||
)
|
||||
if self.invoice_date_due:
|
||||
amount_txt = _('%(amount)s due %(date)s',
|
||||
amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')),
|
||||
date=format_date(self.env, self.invoice_date_due, date_format='short', lang_code=render_context.get('lang'))
|
||||
)
|
||||
else:
|
||||
amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang'))
|
||||
render_context['subtitle'] = Markup("<span>%s<br />%s</span>") % (self.name, amount_txt)
|
||||
return render_context
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<field name="email_from">"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}></field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
@@ -104,8 +104,8 @@
|
||||
<field name="email_from">"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}></field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
@@ -199,8 +199,8 @@
|
||||
<field name="email_from">{{ object.company_id.partner_id.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
@@ -250,8 +250,8 @@
|
||||
<field name="email_from">"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}></field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
|
||||
@@ -1751,10 +1751,10 @@ class Lead(models.Model):
|
||||
return self.env.ref('crm.mt_lead_lost')
|
||||
return super(Lead, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Handle salesman recipients that can convert leads into opportunities
|
||||
and set opportunities as won / lost. """
|
||||
groups = super(Lead, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(Lead, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
@@ -1777,19 +1777,20 @@ class Lead(models.Model):
|
||||
|
||||
salesman_group_id = self.env.ref('sales_team.group_sale_salesman').id
|
||||
new_group = (
|
||||
'group_sale_salesman', lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'], {
|
||||
'actions': salesman_actions,
|
||||
})
|
||||
'group_sale_salesman',
|
||||
lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'],
|
||||
{'actions': salesman_actions}
|
||||
)
|
||||
|
||||
return [new_group] + groups
|
||||
|
||||
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Override to set alias of lead and opportunities to their sales team if any. """
|
||||
aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
|
||||
aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default)
|
||||
res = {lead.id: aliases.get(lead.team_id.id) for lead in self}
|
||||
leftover = self.filtered(lambda rec: not rec.team_id)
|
||||
if leftover:
|
||||
res.update(super(Lead, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
|
||||
res.update(super(Lead, leftover)._notify_get_reply_to(default=default))
|
||||
return res
|
||||
|
||||
def _message_get_default_recipients(self):
|
||||
|
||||
@@ -70,7 +70,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
})
|
||||
partner_model = self.env.ref('base.model_res_partner')
|
||||
partner = self.env['res.partner'].search([], limit=1)
|
||||
with self.assertQueryCount(__system__=1211):
|
||||
with self.assertQueryCount(__system__=1112):
|
||||
events = self.env['calendar.event'].create([{
|
||||
'name': "Event %s" % (i),
|
||||
'start': datetime(2020, 1, 15, 8, 0),
|
||||
@@ -102,7 +102,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
|
||||
})
|
||||
partner_model = self.env.ref('base.model_res_partner')
|
||||
partner = self.env['res.partner'].search([], limit=1)
|
||||
with self.assertQueryCount(__system__=3635):
|
||||
with self.assertQueryCount(__system__=2916):
|
||||
event = self.env['calendar.event'].create({
|
||||
'name': "Event",
|
||||
'start': datetime(2020, 1, 15, 8, 0),
|
||||
|
||||
@@ -1430,10 +1430,10 @@ class HolidaysRequest(models.Model):
|
||||
return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave')
|
||||
return super(HolidaysRequest, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Handle HR users and officers recipients that can validate or refuse holidays
|
||||
directly from email. """
|
||||
groups = super(HolidaysRequest, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(HolidaysRequest, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
@@ -1447,9 +1447,10 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
|
||||
new_group = (
|
||||
'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], {
|
||||
'actions': hr_actions,
|
||||
})
|
||||
'group_hr_holidays_user',
|
||||
lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
|
||||
{'actions': hr_actions}
|
||||
)
|
||||
|
||||
return [new_group] + groups
|
||||
|
||||
|
||||
@@ -743,10 +743,10 @@ class HolidaysAllocation(models.Model):
|
||||
return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation')
|
||||
return super(HolidaysAllocation, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Handle HR users and officers recipients that can validate or refuse holidays
|
||||
directly from email. """
|
||||
groups = super(HolidaysAllocation, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
@@ -760,9 +760,10 @@ class HolidaysAllocation(models.Model):
|
||||
|
||||
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
|
||||
new_group = (
|
||||
'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], {
|
||||
'actions': hr_actions,
|
||||
})
|
||||
'group_hr_holidays_user',
|
||||
lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
|
||||
{'actions': hr_actions}
|
||||
)
|
||||
|
||||
return [new_group] + groups
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ class TestCompanyLeave(TransactionCase):
|
||||
})
|
||||
company_leave._compute_date_from_to()
|
||||
|
||||
with self.assertQueryCount(__system__=773, admin=865):
|
||||
with self.assertQueryCount(__system__=659, admin=865):
|
||||
# Original query count: 1987
|
||||
# Without tracking/activity context keys: 5154
|
||||
company_leave.action_validate()
|
||||
|
||||
@@ -500,13 +500,13 @@ class Applicant(models.Model):
|
||||
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
|
||||
return super(Applicant, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Override to set alias of applicants to their job definition if any. """
|
||||
aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
|
||||
aliases = self.mapped('job_id')._notify_get_reply_to(default=default)
|
||||
res = {app.id: aliases.get(app.job_id.id) for app in self}
|
||||
leftover = self.filtered(lambda rec: not rec.job_id)
|
||||
if leftover:
|
||||
res.update(super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
|
||||
res.update(super(Applicant, leftover)._notify_get_reply_to(default=default))
|
||||
return res
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{
|
||||
'name': 'Discuss',
|
||||
'version': '1.6',
|
||||
'version': '1.7',
|
||||
'category': 'Productivity/Discuss',
|
||||
'sequence': 145,
|
||||
'summary': 'Chat, mail gateway and private channels',
|
||||
|
||||
@@ -93,9 +93,9 @@ class MailController(http.Controller):
|
||||
except AccessError:
|
||||
return cls._redirect_to_messaging()
|
||||
else:
|
||||
record_action = record_sudo.get_access_action(access_uid=uid)
|
||||
record_action = record_sudo._get_access_action(access_uid=uid)
|
||||
else:
|
||||
record_action = record_sudo.get_access_action()
|
||||
record_action = record_sudo._get_access_action()
|
||||
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
|
||||
return cls._redirect_to_messaging()
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<li><t t-esc="tracking[0]"/>: <t t-esc="tracking[1]"/> -> <t t-esc="tracking[2]"/></li>
|
||||
</t>
|
||||
</ul>
|
||||
<div t-if="signature" t-out="signature" style="font-size: 13px;"/>
|
||||
<div t-if="email_add_signature and not is_html_empty(signature)" t-out="signature" style="font-size: 13px;"/>
|
||||
<p style="color: #555555; margin-top:32px;">
|
||||
Sent
|
||||
<span t-if="company.name">
|
||||
@@ -131,33 +131,39 @@
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<!-- Information on model to use this notification template
|
||||
* if the record has an online access defined in get_access_action, having
|
||||
a _get_share_url methods is required (like sale order and invoice);
|
||||
* this template works best with portal-enable models although it is not
|
||||
a complete requirement currently;
|
||||
-->
|
||||
<!-- Used mainly in conjunction of portal models (SO, PO, Invoices) although not mandatory -->
|
||||
<template id="mail_notification_paynow" name="Mail: Pay Now mail notification template">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 24px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #FFFFFF; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 24px; background-color: #FFFFFF; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<t t-set="access_action" t-value="record.with_context(force_website=True).get_access_action()"/>
|
||||
<t t-set="is_online" t-value="access_action and access_action['type'] == 'ir.actions.act_url'"/>
|
||||
<t t-set="base_url" t-value="record.get_base_url()"/>
|
||||
<t t-set="share_url" t-value="is_online and record._get_share_url(redirect=True, signup_partner=notification_is_customer, share_token=notification_is_customer)"/>
|
||||
<t t-set="access_url" t-value="is_online and share_url and base_url + share_url or ''"/>
|
||||
<t t-set="access_name">
|
||||
View <t t-esc="model_description or 'document'"/>
|
||||
</t>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; padding: 0; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #FFFFFF; padding: 0; border-collapse:separate;">
|
||||
<tr><td valign="middle">
|
||||
<span style="font-size: 10px;">Your <t t-esc="model_description or 'document'"/></span><br/>
|
||||
<span style="font-size: 20px; font-weight: bold;">
|
||||
<t t-esc="message.record_name"/>
|
||||
</span>
|
||||
<t t-if="has_button_access">
|
||||
<a t-att-href="button_access['url']"
|
||||
style="float: left; padding: 8px 12px; font-size: 12px; color: #FFFFFF; text-decoration: none !important; font-weight: 400; background-color: #875A7B; border: 0px solid #875A7B; border-radius:3px">
|
||||
<t t-esc="button_access['title']"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span style="font-size: 10px;">Your <t t-esc="model_description or 'document'"/></span>
|
||||
<br />
|
||||
<span style="font-size: 20px; font-weight: bold;">
|
||||
<t t-esc="message.record_name and message.record_name.replace('/','-') or ''"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="actions">
|
||||
<t t-foreach="actions" t-as="action">
|
||||
|
|
||||
<a t-att-href="action['url']" style="color: #875A7B; text-decoration:none !important;">
|
||||
<t t-esc="action['title']"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not is_html_empty(subtitle)" t-out='subtitle'
|
||||
style="margin-left: 20px; display: inline-block;"/>
|
||||
</td><td valign="middle" align="right">
|
||||
<img t-att-src="'/logo.png?company=%s' % (company.id or 0)" style="padding: 0px; margin: 0px; height: 48px;" t-att-alt="'%s' % company.name"/>
|
||||
</td></tr>
|
||||
@@ -171,18 +177,12 @@
|
||||
<tr>
|
||||
<td style="padding: 0">
|
||||
<t t-out="message.body"/>
|
||||
<div t-if="is_online and not record._context.get('proforma')" style="margin: 32px 0px 32px 0px; text-align: center;">
|
||||
<a t-att-href="access_url"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
<t t-esc="access_name"/>
|
||||
</a>
|
||||
</div>
|
||||
<t t-if="'user_id' in record and record.user_id and not record.env.user._is_superuser() and signature != ''">
|
||||
<t t-if="email_add_signature and 'user_id' in record and record.user_id and not record.env.user._is_superuser() and not is_html_empty(record.user_id.sudo().signature)">
|
||||
<div style="margin: 0px; padding: 0px; font-size:13px;">
|
||||
Best regards,
|
||||
</div>
|
||||
<div>&nbsp;</div>
|
||||
<div t-if="record.user_id.sudo().signature" style="font-size: 13px;">
|
||||
<div style="font-size: 13px;">
|
||||
<div t-out="record.user_id.sudo().signature"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -349,14 +349,14 @@ class Channel(models.Model):
|
||||
new_partner_id=channel_partner.partner_id.id,
|
||||
new_partner_name=channel_partner.partner_id.name,
|
||||
)
|
||||
channel_partner.channel_id.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", notify_by_email=False)
|
||||
channel_partner.channel_id.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
|
||||
members_data.append({
|
||||
'id': channel_partner.partner_id.id,
|
||||
'im_status': channel_partner.partner_id.im_status,
|
||||
'name': channel_partner.partner_id.name,
|
||||
})
|
||||
for channel_partner in new_members.filtered(lambda channel_partner: channel_partner.guest_id):
|
||||
channel_partner.channel_id.message_post(body=_('<div class="o_mail_notification">joined the channel</div>'), message_type="notification", subtype_xmlid="mail.mt_comment", notify_by_email=False)
|
||||
channel_partner.channel_id.message_post(body=_('<div class="o_mail_notification">joined the channel</div>'), message_type="notification", subtype_xmlid="mail.mt_comment")
|
||||
guest_members_data.append({
|
||||
'id': channel_partner.guest_id.id,
|
||||
'name': channel_partner.guest_id.name,
|
||||
@@ -479,13 +479,13 @@ class Channel(models.Model):
|
||||
return False
|
||||
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
|
||||
|
||||
def _notify_compute_recipients(self, message, msg_vals):
|
||||
def _notify_get_recipients(self, message, msg_vals):
|
||||
""" Override recipients computation as channel is not a standard
|
||||
mail.thread document. Indeed there are no followers on a channel.
|
||||
Instead of followers it has members that should be notified.
|
||||
|
||||
:param message: see ``MailThread._notify_compute_recipients()``;
|
||||
:param msg_vals: see ``MailThread._notify_compute_recipients()``;
|
||||
:param message: see ``MailThread._notify_get_recipients()``;
|
||||
:param msg_vals: see ``MailThread._notify_get_recipients()``;
|
||||
|
||||
:return recipients: structured data holding recipients data. See
|
||||
``MailThread._notify_thread()`` for more details about its content
|
||||
@@ -536,13 +536,13 @@ class Channel(models.Model):
|
||||
|
||||
return recipients_data
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" All recipients of a message on a channel are considered as partners.
|
||||
This means they will receive a minimal email, without a link to access
|
||||
in the backend. Mailing lists should indeed send minimal emails to avoid
|
||||
the noise. """
|
||||
groups = super(Channel, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
for (index, (group_name, group_func, group_data)) in enumerate(groups):
|
||||
groups = super(Channel, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
for (index, (group_name, _group_func, group_data)) in enumerate(groups):
|
||||
if group_name != 'customer':
|
||||
groups[index] = (group_name, lambda partner: False, group_data)
|
||||
return groups
|
||||
|
||||
@@ -83,78 +83,225 @@ class Followers(models.Model):
|
||||
subtype if records and subtype are set;
|
||||
* partners if pids is given;
|
||||
|
||||
:param records: fetch data from followers of records that follow subtype_id;
|
||||
:param message_type: mail.message.message_type in order to allow custom behavior depending on it (SMS for example);
|
||||
:param records: fetch data from followers of ``records`` that follow
|
||||
``subtype_id``;
|
||||
:param message_type: mail.message.message_type in order to allow custom
|
||||
behavior depending on it (SMS for example);
|
||||
:param subtype_id: mail.message.subtype to check against followers;
|
||||
:param pids: additional set of partner IDs from which to fetch recipient data;
|
||||
:param pids: additional set of partner IDs from which to fetch recipient
|
||||
data independently from following status;
|
||||
|
||||
:return: list of recipient data which is a tuple containing
|
||||
partner ID ,
|
||||
active value (always True for channels),
|
||||
share status of partner,
|
||||
notification status of partner or channel (email or inbox),
|
||||
user groups of partner,
|
||||
:return dict: recipients data based on record.ids if given, else a generic
|
||||
'0' key to keep a dict-like return format. Each item is a dict based on
|
||||
recipients partner ids formatted like
|
||||
{'active': whether partner is active;
|
||||
'id': res.partner ID;
|
||||
'is_follower': True if linked to a record and if partner is a follower;
|
||||
'lang': lang of the partner;
|
||||
'groups': groups of the partner's user. If several users exist preference
|
||||
is given to internal user, then share users. In case of multiples
|
||||
users of same kind groups are unioned;
|
||||
'notif': notification type ('inbox' or 'email'). Overrides may change
|
||||
this value (e.g. 'sms' in sms module);
|
||||
'share': if partner is a customer (no user or share user);
|
||||
'ushare': if partner has users, whether all are shared (public or portal);
|
||||
'type': summary of partner 'usage' (portal, customer, internal user);
|
||||
}
|
||||
"""
|
||||
self.env['mail.followers'].flush(['partner_id', 'subtype_ids'])
|
||||
self.env['mail.message.subtype'].flush(['internal'])
|
||||
self.env['res.users'].flush(['notification_type', 'active', 'partner_id', 'groups_id'])
|
||||
self.env['res.partner'].flush(['active', 'partner_share'])
|
||||
self.env['res.groups'].flush(['users'])
|
||||
# if we have records and a subtype: we have to fetch followers
|
||||
if records and subtype_id:
|
||||
query = """
|
||||
SELECT DISTINCT ON (pid) * FROM (
|
||||
WITH sub_followers AS (
|
||||
SELECT fol.partner_id,
|
||||
coalesce(subtype.internal, false) as internal
|
||||
SELECT fol.partner_id AS pid,
|
||||
fol.id AS fid,
|
||||
fol.res_id AS res_id,
|
||||
TRUE as is_follower,
|
||||
COALESCE(subrel.follow, FALSE) AS subtype_follower,
|
||||
COALESCE(subrel.internal, FALSE) AS internal
|
||||
FROM mail_followers fol
|
||||
JOIN mail_followers_mail_message_subtype_rel subrel ON subrel.mail_followers_id = fol.id
|
||||
JOIN mail_message_subtype subtype ON subtype.id = subrel.mail_message_subtype_id
|
||||
WHERE subrel.mail_message_subtype_id = %s
|
||||
AND fol.res_model = %s
|
||||
AND fol.res_id IN %s
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT TRUE AS follow,
|
||||
subtype.internal AS internal
|
||||
FROM mail_followers_mail_message_subtype_rel m
|
||||
LEFT JOIN mail_message_subtype subtype ON subtype.id = m.mail_message_subtype_id
|
||||
WHERE m.mail_followers_id = fol.id AND m.mail_message_subtype_id = %s
|
||||
) subrel ON TRUE
|
||||
WHERE fol.res_model = %s
|
||||
AND fol.res_id IN %s
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT id,
|
||||
FALSE
|
||||
SELECT res_partner.id AS pid,
|
||||
0 AS fid,
|
||||
0 AS res_id,
|
||||
FALSE as is_follower,
|
||||
FALSE as subtype_follower,
|
||||
FALSE as internal
|
||||
FROM res_partner
|
||||
WHERE id=ANY(%s)
|
||||
WHERE res_partner.id = ANY(%s)
|
||||
)
|
||||
SELECT partner.id as pid,
|
||||
partner.active as active,
|
||||
partner.lang as lang,
|
||||
partner.partner_share as pshare,
|
||||
users.notification_type AS notif,
|
||||
array_agg(groups_rel.gid) AS groups
|
||||
sub_user.uid as uid,
|
||||
COALESCE(sub_user.share, FALSE) as ushare,
|
||||
COALESCE(sub_user.notification_type, 'email') as notif,
|
||||
sub_user.groups as groups,
|
||||
sub_followers.res_id as res_id,
|
||||
sub_followers.is_follower as _insert_followerslower
|
||||
FROM res_partner partner
|
||||
LEFT JOIN res_users users ON users.partner_id = partner.id
|
||||
AND users.active
|
||||
LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
|
||||
JOIN sub_followers ON sub_followers.partner_id = partner.id
|
||||
AND NOT (sub_followers.internal AND partner.partner_share)
|
||||
GROUP BY partner.id,
|
||||
users.notification_type
|
||||
) AS x
|
||||
ORDER BY pid, notif
|
||||
JOIN sub_followers ON sub_followers.pid = partner.id
|
||||
AND (NOT sub_followers.internal OR NOT partner.partner_share)
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT users.id AS uid,
|
||||
users.share AS share,
|
||||
users.notification_type AS notification_type,
|
||||
ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
|
||||
FROM res_users users
|
||||
LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
|
||||
WHERE users.partner_id = partner.id AND users.active
|
||||
GROUP BY users.id,
|
||||
users.share,
|
||||
users.notification_type
|
||||
ORDER BY users.share ASC NULLS FIRST, users.id ASC
|
||||
FETCH FIRST ROW ONLY
|
||||
) sub_user ON TRUE
|
||||
|
||||
WHERE sub_followers.subtype_follower OR partner.id = ANY(%s)
|
||||
"""
|
||||
params = [subtype_id, records._name, tuple(records.ids), list(pids) or []]
|
||||
params = [subtype_id, records._name, tuple(records.ids), list(pids or []), list(pids or [])]
|
||||
self.env.cr.execute(query, tuple(params))
|
||||
res = self.env.cr.fetchall()
|
||||
elif pids:
|
||||
# partner_ids and records: no sub query for followers but check for follower status
|
||||
elif pids and records:
|
||||
params = []
|
||||
query_pid = """
|
||||
SELECT partner.id as pid,
|
||||
partner.active as active, partner.partner_share as pshare,
|
||||
users.notification_type AS notif, NULL AS groups
|
||||
FROM res_partner partner
|
||||
LEFT JOIN res_users users ON users.partner_id = partner.id AND users.active
|
||||
WHERE partner.id IN %s"""
|
||||
params.append(tuple(pids))
|
||||
query = 'SELECT DISTINCT ON (pid) * FROM (%s) AS x ORDER BY pid, notif' % query_pid
|
||||
query = """
|
||||
SELECT partner.id as pid,
|
||||
partner.active as active,
|
||||
partner.lang as lang,
|
||||
partner.partner_share as pshare,
|
||||
sub_user.uid as uid,
|
||||
COALESCE(sub_user.share, FALSE) as ushare,
|
||||
COALESCE(sub_user.notification_type, 'email') as notif,
|
||||
sub_user.groups as groups,
|
||||
ARRAY_AGG(fol.res_id) FILTER (WHERE fol.res_id IS NOT NULL) AS res_ids
|
||||
FROM res_partner partner
|
||||
LEFT JOIN mail_followers fol ON fol.partner_id = partner.id
|
||||
AND fol.res_model = %s
|
||||
AND fol.res_id IN %s
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT users.id AS uid,
|
||||
users.share AS share,
|
||||
users.notification_type AS notification_type,
|
||||
ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
|
||||
FROM res_users users
|
||||
LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
|
||||
WHERE users.partner_id = partner.id AND users.active
|
||||
GROUP BY users.id,
|
||||
users.share,
|
||||
users.notification_type
|
||||
ORDER BY users.share ASC NULLS FIRST, users.id ASC
|
||||
FETCH FIRST ROW ONLY
|
||||
) sub_user ON TRUE
|
||||
|
||||
WHERE partner.id IN %s
|
||||
GROUP BY partner.id,
|
||||
sub_user.uid,
|
||||
sub_user.share,
|
||||
sub_user.notification_type,
|
||||
sub_user.groups
|
||||
"""
|
||||
params = [records._name, tuple(records.ids), tuple(pids)]
|
||||
self.env.cr.execute(query, tuple(params))
|
||||
simplified_res = self.env.cr.fetchall()
|
||||
# simplified query contains res_ids -> flatten it by making it a list
|
||||
# with res_id and add follower status
|
||||
res = []
|
||||
for item in simplified_res:
|
||||
res_ids = item[-1]
|
||||
if not res_ids: # keep res_ids Falsy (global), set as not follower
|
||||
flattened = [list(item) + [False]]
|
||||
else: # generate an entry for each res_id with partner being follower
|
||||
flattened = [list(item[:-1]) + [res_id, True]
|
||||
for res_id in res_ids]
|
||||
res += flattened
|
||||
# only partner ids: no follower status involved, fetch only direct recipients information
|
||||
elif pids:
|
||||
query = """
|
||||
SELECT partner.id as pid,
|
||||
partner.active as active,
|
||||
partner.lang as lang,
|
||||
partner.partner_share as pshare,
|
||||
sub_user.uid as uid,
|
||||
COALESCE(sub_user.share, FALSE) as ushare,
|
||||
COALESCE(sub_user.notification_type, 'email') as notif,
|
||||
sub_user.groups as groups,
|
||||
0 as res_id,
|
||||
FALSE as is_follower
|
||||
FROM res_partner partner
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT users.id AS uid,
|
||||
users.share AS share,
|
||||
users.notification_type AS notification_type,
|
||||
ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
|
||||
FROM res_users users
|
||||
LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
|
||||
WHERE users.partner_id = partner.id AND users.active
|
||||
GROUP BY users.id,
|
||||
users.share,
|
||||
users.notification_type
|
||||
ORDER BY users.share ASC NULLS FIRST, users.id ASC
|
||||
FETCH FIRST ROW ONLY
|
||||
) sub_user ON TRUE
|
||||
|
||||
WHERE partner.id IN %s
|
||||
GROUP BY partner.id,
|
||||
sub_user.uid,
|
||||
sub_user.share,
|
||||
sub_user.notification_type,
|
||||
sub_user.groups
|
||||
"""
|
||||
params = [tuple(pids)]
|
||||
self.env.cr.execute(query, tuple(params))
|
||||
res = self.env.cr.fetchall()
|
||||
else:
|
||||
res = []
|
||||
return res
|
||||
|
||||
res_ids = records.ids if records else [0]
|
||||
doc_infos = dict((res_id, {}) for res_id in res_ids)
|
||||
for (partner_id, is_active, lang, pshare, uid, ushare, notif, groups, res_id, is_follower) in res:
|
||||
to_update = [res_id] if res_id else res_ids
|
||||
for res_id_to_update in to_update:
|
||||
# avoid updating already existing information, unnecessary dict update
|
||||
if not res_id and partner_id in doc_infos[res_id_to_update]:
|
||||
continue
|
||||
follower_data = {
|
||||
'active': is_active,
|
||||
'id': partner_id,
|
||||
'is_follower': is_follower,
|
||||
'lang': lang,
|
||||
'groups': set(groups or []),
|
||||
'notif': notif,
|
||||
'share': pshare,
|
||||
'uid': uid,
|
||||
'ushare': ushare,
|
||||
}
|
||||
# additional information
|
||||
if follower_data['ushare']: # any type of share user
|
||||
follower_data['type'] = 'portal'
|
||||
elif follower_data['share']: # no user, is share -> customer (partner only)
|
||||
follower_data['type'] = 'customer'
|
||||
else: # has a user not share -> internal user
|
||||
follower_data['type'] = 'user'
|
||||
doc_infos[res_id_to_update][partner_id] = follower_data
|
||||
|
||||
return doc_infos
|
||||
|
||||
def _get_subscription_data(self, doc_data, pids, include_pshare=False, include_active=False):
|
||||
""" Private method allowing to fetch follower data from several documents of a given model.
|
||||
|
||||
@@ -166,7 +166,7 @@ class Message(models.Model):
|
||||
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
|
||||
# keep notification layout informations to be able to generate mail again
|
||||
email_layout_xmlid = fields.Char('Layout', copy=False) # xml id of layout
|
||||
add_sign = fields.Boolean(default=True)
|
||||
email_add_signature = fields.Boolean(default=True)
|
||||
# `test_adv_activity`, `test_adv_activity_full`, `test_message_assignation_inbox`,...
|
||||
# By setting an inverse for mail.mail_message_id, the number of SQL queries done by `modified` is reduced.
|
||||
# 'mail.mail' inherits from `mail.message`: `_inherits = {'mail.message': 'mail_message_id'}`
|
||||
|
||||
@@ -44,7 +44,7 @@ class MailNotification(models.Model):
|
||||
failure_reason = fields.Text('Failure reason', copy=False)
|
||||
|
||||
_sql_constraints = [
|
||||
# email notification;: partner is required
|
||||
# email notification: partner is required
|
||||
('notification_partner_required',
|
||||
"CHECK(notification_type NOT IN ('email', 'inbox') OR res_partner_id IS NOT NULL)",
|
||||
'Customer is required for inbox / email notification'),
|
||||
@@ -59,6 +59,11 @@ class MailNotification(models.Model):
|
||||
CREATE INDEX IF NOT EXISTS mail_notification_res_partner_id_is_read_notification_status_mail_message_id
|
||||
ON mail_notification (res_partner_id, is_read, notification_status, mail_message_id)
|
||||
""")
|
||||
self.env.cr.execute(
|
||||
"""CREATE UNIQUE INDEX IF NOT EXISTS unique_mail_message_id_res_partner_id_if_set
|
||||
ON %s (mail_message_id, res_partner_id)
|
||||
WHERE res_partner_id IS NOT NULL""" % self._table
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
|
||||
from odoo import _, api, fields, models, tools, Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import is_html_empty
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -301,10 +302,21 @@ class MailTemplate(models.Model):
|
||||
model = model.with_context(lang=lang)
|
||||
|
||||
template_ctx = {
|
||||
# message
|
||||
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
|
||||
'subtype': self.env['mail.message.subtype'].sudo(),
|
||||
# record
|
||||
'model_description': model.display_name,
|
||||
'company': 'company_id' in record and record['company_id'] or self.env.company,
|
||||
'record': record,
|
||||
'record_name': False,
|
||||
'subtitle': False,
|
||||
# user / environment
|
||||
'company': 'company_id' in record and record['company_id'] or self.env.company,
|
||||
'email_add_signature': False,
|
||||
'signature': '',
|
||||
'website_url': '',
|
||||
# tools
|
||||
'is_html_empty': is_html_empty,
|
||||
}
|
||||
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
|
||||
values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
|
||||
|
||||
+385
-270
@@ -26,7 +26,7 @@ from xmlrpc import client as xmlrpclib
|
||||
from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_ID, Command
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo.osv import expression
|
||||
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.tools.misc import clean_context, split_every
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -1638,18 +1638,28 @@ class MailThread(models.AbstractModel):
|
||||
]).write({'author_id': partner.id})
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# MESSAGE POST MAIN
|
||||
# ------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_post_process_attachments(self, attachments, attachment_ids, message_values):
|
||||
""" Preprocess attachments for mail_thread.message_post() or mail_mail.create().
|
||||
Purpose is to
|
||||
|
||||
:param list attachments: list of attachment tuples in the form ``(name,content)``, #todo xdo update that
|
||||
where content is NOT base64 encoded
|
||||
:param list attachment_ids: a list of attachment ids, not in tomany command form
|
||||
:param dict message_data: model: the model of the attachments parent record,
|
||||
res_id: the id of the attachments parent record
|
||||
* transfer attachments given by ``attachment_ids`` from the composer to
|
||||
the record (if any);
|
||||
* limit attachments manipulation when being a shared user;
|
||||
* create attachments from ``attachments``. If those are linked to the
|
||||
content (body) through CIDs body is updated accordingly;
|
||||
|
||||
:param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
|
||||
tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
|
||||
is NOT base64 encoded;
|
||||
:param list attachment_ids: list of existing attachments to link to this message;
|
||||
:param message_values: dictionary of values that will be used to create the
|
||||
message. It is used to find back record- or content- context;
|
||||
|
||||
:return dict: new values for message: 'attachment_ids' and optionally 'body'
|
||||
"""
|
||||
return_values = {}
|
||||
body = message_values.get('body')
|
||||
@@ -1703,7 +1713,7 @@ class MailThread(models.AbstractModel):
|
||||
content = content.as_bytes()
|
||||
elif content is None:
|
||||
continue
|
||||
attachement_values= {
|
||||
attachement_values = {
|
||||
'name': name,
|
||||
'datas': base64.b64encode(content),
|
||||
'type': 'binary',
|
||||
@@ -1753,29 +1763,37 @@ class MailThread(models.AbstractModel):
|
||||
email_from=None, author_id=None, parent_id=False,
|
||||
subtype_xmlid=None, subtype_id=False, partner_ids=None,
|
||||
attachments=None, attachment_ids=None,
|
||||
add_sign=True, record_name=False,
|
||||
**kwargs):
|
||||
""" Post a new message in an existing thread, returning the new
|
||||
mail.message ID.
|
||||
:param str body: body of the message, usually raw HTML that will
|
||||
be sanitized
|
||||
:param str subject: subject of the message
|
||||
:param str message_type: see mail_message.message_type field. Can be anything but
|
||||
user_notification, reserved for message_notify
|
||||
:param int parent_id: handle thread formation
|
||||
:param int subtype_id: subtype_id of the message, used mainly use for
|
||||
followers notification mechanism;
|
||||
:param list(int) partner_ids: partner_ids to notify in addition to partners
|
||||
computed based on subtype / followers matching;
|
||||
:param list(tuple(str,str), tuple(str,str, dict) or int) attachments : list of attachment tuples in the form
|
||||
``(name,content)`` or ``(name,content, info)``, where content is NOT base64 encoded
|
||||
:param list id attachment_ids: list of existing attachement to link to this message
|
||||
-Should only be setted by chatter
|
||||
-Attachement object attached to mail.compose.message(0) will be attached
|
||||
to the related document.
|
||||
Extra keyword arguments will be used as default column values for the
|
||||
new mail.message record.
|
||||
:return int: ID of newly created mail.message
|
||||
""" Post a new message in an existing thread, returning the new mail.message.
|
||||
|
||||
:param str body: body of the message, usually raw HTML that will
|
||||
be sanitized
|
||||
:param str subject: subject of the message
|
||||
:param str message_type: see mail_message.message_type field. Can be anything but
|
||||
user_notification, reserved for message_notify
|
||||
:param str email_from: from address of the author. See ``_message_compute_author``
|
||||
that uses it to make email_from / author_id coherent;
|
||||
:param int author_id: optional ID of partner record being the author. See
|
||||
``_message_compute_author`` that uses it to make email_from / author_id coherent;
|
||||
:param int parent_id: handle thread formation
|
||||
:param int subtype_id: subtype_id of the message, used mainly for followers
|
||||
notification mechanism;
|
||||
:param list(int) partner_ids: partner_ids to notify in addition to partners
|
||||
computed based on subtype / followers matching;
|
||||
:param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
|
||||
tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
|
||||
is NOT base64 encoded;
|
||||
:param list attachment_ids: list of existing attachments to link to this message
|
||||
-Should only be set by chatter
|
||||
-Attachment object attached to mail.compose.message(0) will be attached
|
||||
to the related document.
|
||||
|
||||
Extra keyword arguments will be used either
|
||||
* as default column values for the new mail.message record if they match
|
||||
mail.message fields;
|
||||
* propagated to notification methods;
|
||||
|
||||
:return record: newly create mail.message
|
||||
"""
|
||||
self.ensure_one() # should always be posted on a record, use message_notify if no record
|
||||
# split message additional values from notify additional values
|
||||
@@ -1795,12 +1813,11 @@ class MailThread(models.AbstractModel):
|
||||
if any(not isinstance(pc_id, int) for pc_id in partner_ids):
|
||||
raise ValueError(_('message_post partner_ids and must be integer list, not commands.'))
|
||||
|
||||
self = self._fallback_lang() # add lang to context imediatly since it will be usefull in various flows latter.
|
||||
self = self._fallback_lang() # add lang to context immediately since it will be useful in various flows latter.
|
||||
|
||||
# Explicit access rights check, because display_name is computed as sudo.
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
record_name = record_name or self.display_name
|
||||
|
||||
# Find the message's author
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
@@ -1821,38 +1838,43 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
parent_id = self._message_compute_parent_id(parent_id)
|
||||
|
||||
values = dict(msg_kwargs)
|
||||
values.update({
|
||||
msg_values = dict(msg_kwargs)
|
||||
if 'email_add_signature' not in msg_values:
|
||||
msg_values['email_add_signature'] = True
|
||||
if not msg_values.get('record_name'):
|
||||
msg_values['record_name'] = self.display_name
|
||||
msg_values.update({
|
||||
'author_id': author_id,
|
||||
'author_guest_id': author_guest_id,
|
||||
'email_from': email_from,
|
||||
'model': self._name,
|
||||
'res_id': self.id,
|
||||
# content
|
||||
'body': body,
|
||||
'subject': subject or False,
|
||||
'message_type': message_type,
|
||||
'parent_id': parent_id,
|
||||
'subtype_id': subtype_id,
|
||||
# recipients
|
||||
'partner_ids': partner_ids,
|
||||
'add_sign': add_sign,
|
||||
'record_name': record_name,
|
||||
})
|
||||
|
||||
attachments = attachments or []
|
||||
attachment_ids = attachment_ids or []
|
||||
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, values)
|
||||
values.update(attachement_values) # attachement_ids, [body]
|
||||
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, msg_values)
|
||||
msg_values.update(attachement_values) # attachement_ids, [body]
|
||||
|
||||
new_message = self._message_create(values)
|
||||
new_message = self._message_create(msg_values)
|
||||
|
||||
# Set main attachment field if necessary
|
||||
self._message_set_main_attachment_id(values['attachment_ids'])
|
||||
self._message_set_main_attachment_id(msg_values['attachment_ids'])
|
||||
|
||||
if values['author_id'] and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||
if self.env['res.partner'].browse(values['author_id']).active: # we dont want to add odoobot/inactive as a follower
|
||||
self._message_subscribe(partner_ids=[values['author_id']])
|
||||
if msg_values['author_id'] and msg_values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||
if self.env['res.partner'].browse(msg_values['author_id']).active: # we dont want to add odoobot/inactive as a follower
|
||||
self._message_subscribe(partner_ids=[msg_values['author_id']])
|
||||
|
||||
self._message_post_after_hook(new_message, values)
|
||||
self._notify_thread(new_message, values, **notif_kwargs)
|
||||
self._message_post_after_hook(new_message, msg_values)
|
||||
self._notify_thread(new_message, msg_values, **notif_kwargs)
|
||||
return new_message
|
||||
|
||||
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
|
||||
@@ -1868,9 +1890,9 @@ class MailThread(models.AbstractModel):
|
||||
message and computed value are given, to try to lessen query count by
|
||||
using already-computed values instead of having to rebrowse things. """
|
||||
|
||||
# ------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# MESSAGE POST API / WRAPPERS
|
||||
# ------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_compose_with_view(self, views_or_xmlid, message_log=False, **kwargs):
|
||||
""" Helper method to send a mail / post a message / log a note using
|
||||
@@ -1957,7 +1979,7 @@ class MailThread(models.AbstractModel):
|
||||
res_id = False
|
||||
|
||||
MailThread = self.env['mail.thread']
|
||||
values = {
|
||||
msg_values = {
|
||||
'parent_id': parent_id,
|
||||
'model': self._name if self else model,
|
||||
'res_id': self.id if self else res_id,
|
||||
@@ -1970,12 +1992,15 @@ class MailThread(models.AbstractModel):
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'is_internal': True,
|
||||
'record_name': False,
|
||||
'reply_to': MailThread._notify_get_reply_to(default=email_from, records=None)[False],
|
||||
'reply_to': MailThread._notify_get_reply_to(default=email_from)[False],
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'),
|
||||
}
|
||||
values.update(msg_kwargs)
|
||||
new_message = MailThread._message_create(values)
|
||||
MailThread._notify_thread(new_message, values, **notif_kwargs)
|
||||
msg_values.update(msg_kwargs)
|
||||
if 'email_add_signature' not in msg_values:
|
||||
msg_values['email_add_signature'] = True
|
||||
|
||||
new_message = MailThread._message_create(msg_values)
|
||||
MailThread._notify_thread(new_message, msg_values, **notif_kwargs)
|
||||
return new_message
|
||||
|
||||
def _message_log_with_view(self, views_or_xmlid, **kwargs):
|
||||
@@ -1992,7 +2017,7 @@ class MailThread(models.AbstractModel):
|
||||
self.ensure_one()
|
||||
author_id, email_from = self._message_compute_author(author_id, email_from, raise_exception=False)
|
||||
|
||||
message_values = {
|
||||
msg_values = {
|
||||
'subject': subject,
|
||||
'body': body,
|
||||
'author_id': author_id,
|
||||
@@ -2003,11 +2028,12 @@ class MailThread(models.AbstractModel):
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'is_internal': True,
|
||||
'record_name': False,
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
|
||||
'email_add_signature': False, # False as no notification -> no need to compute signature
|
||||
}
|
||||
message_values.update(kwargs)
|
||||
return self.sudo()._message_create(message_values)
|
||||
msg_values.update(kwargs)
|
||||
return self.sudo()._message_create(msg_values)
|
||||
|
||||
def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification'):
|
||||
""" Shortcut allowing to post notes on a batch of documents. It achieve the
|
||||
@@ -2026,8 +2052,9 @@ class MailThread(models.AbstractModel):
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'is_internal': True,
|
||||
'record_name': False,
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
|
||||
'email_add_signature': False,
|
||||
}
|
||||
values_list = [dict(base_message_values,
|
||||
res_id=record.id,
|
||||
@@ -2035,6 +2062,10 @@ class MailThread(models.AbstractModel):
|
||||
for record in self]
|
||||
return self.sudo()._message_create(values_list)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAIL.MESSAGE HELPERS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_compute_author(self, author_id=None, email_from=None, raise_exception=True):
|
||||
""" Tool method computing author information for messages. Purpose is
|
||||
to ensure maximum coherence between author / current user / email_from
|
||||
@@ -2097,43 +2128,60 @@ class MailThread(models.AbstractModel):
|
||||
# NOTIFICATION API
|
||||
# ------------------------------------------------------
|
||||
|
||||
def _notify_thread(self, message, msg_vals=False, notify_by_email=True, **kwargs):
|
||||
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||
""" Main notification method. This method basically does two things
|
||||
|
||||
* call ``_notify_compute_recipients`` that computes recipients to
|
||||
* call ``_notify_get_recipients`` that computes recipients to
|
||||
notify based on message record or message creation values if given
|
||||
(to optimize performance if we already have data computed);
|
||||
* performs the notification process by calling the various notification
|
||||
methods implemented;
|
||||
|
||||
:param message: mail.message record to notify;
|
||||
:param msg_vals: dictionary of values used to create the message. If given
|
||||
it is used instead of accessing ``self`` to lessen query count in some
|
||||
simple cases where no notification is actually required;
|
||||
:param message: ``mail.message`` record to notify;
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
|
||||
Kwargs allow to pass various parameters that are given to sub notification
|
||||
methods. See those methods for more details about the additional parameters.
|
||||
Parameters used for email-style notifications
|
||||
|
||||
:return: recipients data (see ``MailThread._notify_get_recipients()``)
|
||||
"""
|
||||
# add lang to context immediately since it will be useful in various rendering later
|
||||
self = self._fallback_lang()
|
||||
|
||||
msg_vals = msg_vals if msg_vals else {}
|
||||
rdata = self._notify_compute_recipients(message, msg_vals)
|
||||
if not rdata:
|
||||
return rdata
|
||||
recipients_data = self._notify_get_recipients(message, msg_vals)
|
||||
if not recipients_data:
|
||||
return recipients_data
|
||||
|
||||
self._notify_record_by_inbox(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||
if notify_by_email:
|
||||
self._notify_record_by_email(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_thread_by_email(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
return rdata
|
||||
return recipients_data
|
||||
|
||||
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Notification method: inbox. Do two main things
|
||||
def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Notification method: inbox. Does two main things :
|
||||
|
||||
* create an inbox notification for users;
|
||||
* create inbox notifications for users;
|
||||
* send bus notifications;
|
||||
|
||||
TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
|
||||
and correctly override notify_recipients
|
||||
:param message: ``mail.message`` record to notify;
|
||||
:param recipients_data: list of recipients information (based on res.partner
|
||||
records), formatted like
|
||||
[{'active': partner.active;
|
||||
'id': id of the res.partner being recipient to notify;
|
||||
'groups': res.group IDs if linked to a user;
|
||||
'notif': 'inbox', 'email', 'sms' (SMS App);
|
||||
'share': partner.partner_share;
|
||||
'type': 'customer', 'portal', 'user;'
|
||||
}, {...}].
|
||||
See ``MailThread._notify_get_recipients``;
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
"""
|
||||
bus_notifications = []
|
||||
inbox_pids = [r['id'] for r in recipients_data if r['notif'] == 'inbox']
|
||||
@@ -2151,22 +2199,38 @@ class MailThread(models.AbstractModel):
|
||||
bus_notifications.append((self.env['res.partner'].browse(partner_id), 'mail.message/inbox', dict(message_format_values)))
|
||||
self.env['bus.bus'].sudo()._sendmany(bus_notifications)
|
||||
|
||||
def _notify_record_by_email(self, message, recipients_data, msg_vals=False,
|
||||
model_description=False, mail_auto_delete=True, check_existing=False,
|
||||
force_send=True, send_after_commit=True,
|
||||
def _notify_thread_by_email(self, message, recipients_data, msg_vals=False,
|
||||
mail_auto_delete=True, # mail.mail
|
||||
model_description=False, force_email_company=False, force_email_lang=False, # rendering
|
||||
check_existing=False, force_send=True, send_after_commit=True, # email send
|
||||
**kwargs):
|
||||
""" Method to send email linked to notified messages.
|
||||
|
||||
:param message: mail.message record to notify;
|
||||
:param recipients_data: see ``_notify_thread``;
|
||||
:param msg_vals: see ``_notify_thread``;
|
||||
:param message: ``mail.message`` record to notify;
|
||||
:param recipients_data: list of recipients information (based on res.partner
|
||||
records), formatted like
|
||||
[{'active': partner.active;
|
||||
'id': id of the res.partner being recipient to notify;
|
||||
'groups': res.group IDs if linked to a user;
|
||||
'notif': 'inbox', 'email', 'sms' (SMS App);
|
||||
'share': partner.partner_share;
|
||||
'type': 'customer', 'portal', 'user;'
|
||||
}, {...}].
|
||||
See ``MailThread._notify_get_recipients``;
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
|
||||
:param mail_auto_delete: delete notification emails once sent;
|
||||
|
||||
:param model_description: model description used in email notification process
|
||||
(computed if not given);
|
||||
:param mail_auto_delete: delete notification emails once sent;
|
||||
:param force_email_company: see ``_notify_by_email_prepare_rendering_context``;
|
||||
:param force_email_lang: see ``_notify_by_email_prepare_rendering_context``;
|
||||
|
||||
:param check_existing: check for existing notifications to update based on
|
||||
mailed recipient, otherwise create new notifications;
|
||||
|
||||
:param force_send: send emails directly instead of using queue;
|
||||
:param send_after_commit: if force_send, tells whether to send emails after
|
||||
the transaction has been committed using a post-commit hook;
|
||||
@@ -2176,14 +2240,18 @@ class MailThread(models.AbstractModel):
|
||||
return True
|
||||
|
||||
model = msg_vals.get('model') if msg_vals else message.model
|
||||
model_name = model_description or (self._fallback_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name
|
||||
recipients_groups_data = self._notify_classify_recipients(partners_data, model_name, msg_vals=msg_vals)
|
||||
model_name = model_description or (self.env['ir.model']._get(model).display_name if model else False) # one query for display name
|
||||
recipients_groups_data = self._notify_get_recipients_classify(partners_data, model_name, msg_vals=msg_vals)
|
||||
|
||||
if not recipients_groups_data:
|
||||
return True
|
||||
force_send = self.env.context.get('mail_notify_force_send', force_send)
|
||||
|
||||
template_values = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) # 10 queries
|
||||
template_values = self._notify_by_email_prepare_rendering_context(
|
||||
message, msg_vals=msg_vals, model_description=model_description,
|
||||
force_email_company=force_email_company,
|
||||
force_email_lang=force_email_lang,
|
||||
) # 10 queries
|
||||
|
||||
email_layout_xmlid = msg_vals.get('email_layout_xmlid') if msg_vals else message.email_layout_xmlid
|
||||
template_xmlid = email_layout_xmlid if email_layout_xmlid else 'mail.message_notification_email'
|
||||
@@ -2193,19 +2261,7 @@ class MailThread(models.AbstractModel):
|
||||
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
|
||||
base_template = False
|
||||
|
||||
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
|
||||
# Replace new lines by spaces to conform to email headers requirements
|
||||
mail_subject = ' '.join((mail_subject or '').splitlines())
|
||||
# prepare notification mail values
|
||||
base_mail_values = {
|
||||
'mail_message_id': message.id,
|
||||
'mail_server_id': message.mail_server_id.id, # 2 query, check acces + read, may be useless, Falsy, when will it be used?
|
||||
'auto_delete': mail_auto_delete,
|
||||
# due to ir.rule, user have no right to access parent message if message is not published
|
||||
'references': message.parent_id.sudo().message_id if message.parent_id else False,
|
||||
'subject': mail_subject,
|
||||
}
|
||||
base_mail_values = self._notify_by_email_add_values(base_mail_values)
|
||||
base_mail_values = self._notify_by_email_get_base_mail_values(message, additional_values={'auto_delete': mail_auto_delete})
|
||||
|
||||
# Clean the context to get rid of residual default_* keys that could cause issues during
|
||||
# the mail.mail creation.
|
||||
@@ -2235,22 +2291,15 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
# create email
|
||||
for recipients_ids_chunk in split_every(recipients_max, recipients_ids):
|
||||
recipient_values = self._notify_email_recipient_values(recipients_ids_chunk)
|
||||
email_to = recipient_values['email_to']
|
||||
recipient_ids = recipient_values['recipient_ids']
|
||||
mail_values = self._notify_by_email_get_final_mail_values(
|
||||
recipients_ids_chunk,
|
||||
base_mail_values,
|
||||
additional_values={'body_html': mail_body}
|
||||
)
|
||||
new_email = SafeMail.create(mail_values)
|
||||
|
||||
create_values = {
|
||||
'body_html': mail_body,
|
||||
'subject': mail_subject,
|
||||
'recipient_ids': [Command.link(pid) for pid in recipient_ids],
|
||||
}
|
||||
if email_to:
|
||||
create_values['email_to'] = email_to
|
||||
create_values.update(base_mail_values) # mail_message_id, mail_server_id, auto_delete, references, headers
|
||||
email = SafeMail.create(create_values)
|
||||
|
||||
if email and recipient_ids:
|
||||
tocreate_recipient_ids = list(recipient_ids)
|
||||
if new_email and recipients_ids_chunk:
|
||||
tocreate_recipient_ids = list(recipients_ids_chunk)
|
||||
if check_existing:
|
||||
existing_notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', message.id),
|
||||
@@ -2258,20 +2307,20 @@ class MailThread(models.AbstractModel):
|
||||
('res_partner_id', 'in', tocreate_recipient_ids)
|
||||
])
|
||||
if existing_notifications:
|
||||
tocreate_recipient_ids = [rid for rid in recipient_ids if rid not in existing_notifications.mapped('res_partner_id.id')]
|
||||
tocreate_recipient_ids = [rid for rid in recipients_ids_chunk if rid not in existing_notifications.mapped('res_partner_id.id')]
|
||||
existing_notifications.write({
|
||||
'notification_status': 'ready',
|
||||
'mail_mail_id': email.id,
|
||||
'mail_mail_id': new_email.id,
|
||||
})
|
||||
notif_create_values += [{
|
||||
'mail_message_id': message.id,
|
||||
'res_partner_id': recipient_id,
|
||||
'notification_type': 'email',
|
||||
'mail_mail_id': email.id,
|
||||
'mail_mail_id': new_email.id,
|
||||
'is_read': True, # discard Inbox notification
|
||||
'notification_status': 'ready',
|
||||
} for recipient_id in tocreate_recipient_ids]
|
||||
emails |= email
|
||||
emails |= new_email
|
||||
|
||||
if notif_create_values:
|
||||
SafeNotification.create(notif_create_values)
|
||||
@@ -2301,75 +2350,114 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _notify_prepare_template_context(self, message, msg_vals, model_description=False, mail_auto_delete=True):
|
||||
# compute send user and its related signature
|
||||
def _notify_by_email_prepare_rendering_context(self, message, msg_vals=False, model_description=False,
|
||||
force_email_company=False, force_email_lang=False):
|
||||
""" Prepare rendering context for notification email.
|
||||
|
||||
Signature: if asked a default signature is computed based on author. Either
|
||||
it has an user and we use the user's signature. Either we do not find any
|
||||
user and we compute a default one based on the author's name.
|
||||
|
||||
Company: either there is one defined on the record (company_id field set
|
||||
with a value), either we use env.company. A new parameter allows to force
|
||||
its value.
|
||||
|
||||
Lang: when calling this method, ``_fallback_lang`` should already been
|
||||
called, or a lang set in context with another way. A wild guess is done
|
||||
based on templates to try to retrieve the recipient's language when a flow
|
||||
like "send by email" is performed. Lang is used to try to have the
|
||||
notification layout in the same language as the email content. A new
|
||||
parameter allows to force its value.
|
||||
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
:param model_description: model description used in email notification process
|
||||
(computed if not given);
|
||||
:param force_email_company: res.company record used when rendering notification
|
||||
layout. Otherwise computed based on current record;
|
||||
:param force_email_lang: lang used when rendering content, used notably to
|
||||
compute model name;
|
||||
"""
|
||||
if msg_vals is False:
|
||||
msg_vals = {}
|
||||
|
||||
# compute send user and its related signature; try to use self.env.user instead of browsing
|
||||
# user_ids if he is the author will give a sudo user, improving access performances and cache usage.
|
||||
signature = ''
|
||||
user = self.env.user
|
||||
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if msg_vals else message.author_id
|
||||
model = msg_vals.get('model') if msg_vals else message.model
|
||||
add_sign = msg_vals.get('add_sign') if msg_vals else message.add_sign
|
||||
subtype_id = msg_vals.get('subtype_id') if msg_vals else message.subtype_id.id
|
||||
message_id = message.id
|
||||
record_name = msg_vals.get('record_name') if msg_vals else message.record_name
|
||||
author_user = user if user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
|
||||
# trying to use user (self.env.user) instead of browing user_ids if he is the author will give a sudo user,
|
||||
# improving access performances and cache usage.
|
||||
if author_user:
|
||||
user = author_user
|
||||
if add_sign:
|
||||
signature = user.signature
|
||||
else:
|
||||
if add_sign:
|
||||
email_add_signature = msg_vals.get('email_add_signature') if msg_vals and 'email_add_signature' in msg_vals else message.email_add_signature
|
||||
if email_add_signature:
|
||||
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if 'author_id' in msg_vals else message.author_id
|
||||
author_user = self.env.user if self.env.user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
|
||||
if author_user:
|
||||
signature = author_user.signature
|
||||
else:
|
||||
signature = "<p>-- <br/>%s</p>" % author.name
|
||||
|
||||
# company value should fall back on env.company if:
|
||||
# - no company_id field on record
|
||||
# - company_id field available but not set
|
||||
company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
|
||||
if force_email_company:
|
||||
company = force_email_company
|
||||
else:
|
||||
company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
|
||||
if company.website:
|
||||
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
|
||||
else:
|
||||
website_url = False
|
||||
|
||||
# Retrieve the language in which the template was rendered, in order to render the custom
|
||||
# layout in the same language.
|
||||
# TDE FIXME: this whole brol should be cleaned !
|
||||
lang = self.env.context.get('lang')
|
||||
if {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
|
||||
# compute lang in which content was rendered or typed
|
||||
lang = False
|
||||
if force_email_lang:
|
||||
lang = force_email_lang
|
||||
elif {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
|
||||
# TDE FIXME: this whole brol should be cleaned !
|
||||
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
|
||||
if template and template.lang:
|
||||
lang = template._render_lang([self.env.context['default_res_id']])[self.env.context['default_res_id']]
|
||||
if not lang:
|
||||
lang = self.env.context.get('lang')
|
||||
|
||||
if not model_description and model:
|
||||
model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name
|
||||
# record, model
|
||||
if not model_description:
|
||||
model = msg_vals.get('model') if 'model' in msg_vals else message.model
|
||||
if model:
|
||||
model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name
|
||||
record_name = msg_vals.get('record_name') if 'record_name' in msg_vals else message.record_name
|
||||
|
||||
# tracking
|
||||
tracking = []
|
||||
if msg_vals.get('tracking_value_ids', True) if msg_vals else bool(self): # could be tracking
|
||||
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
|
||||
groups = tracking_value.field_groups
|
||||
if not groups or self.env.is_superuser() or self.user_has_groups(groups):
|
||||
tracking.append((tracking_value.field_desc,
|
||||
tracking_value.get_old_display_value()[0],
|
||||
tracking_value.get_new_display_value()[0]))
|
||||
tracking_value.get_old_display_value()[0],
|
||||
tracking_value.get_new_display_value()[0]))
|
||||
|
||||
subtype_id = msg_vals.get('subtype_id') if msg_vals and 'subtype_id' in msg_vals else message.subtype_id.id
|
||||
is_discussion = subtype_id == self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
||||
|
||||
return {
|
||||
# message
|
||||
'is_discussion': is_discussion,
|
||||
'message': message,
|
||||
'signature': signature,
|
||||
'website_url': website_url,
|
||||
'company': company,
|
||||
'subtype': message.subtype_id,
|
||||
'tracking_values': tracking,
|
||||
# record
|
||||
'model_description': model_description,
|
||||
'record': self,
|
||||
'record_name': record_name,
|
||||
'tracking_values': tracking,
|
||||
'is_discussion': is_discussion,
|
||||
'subtype': message.subtype_id,
|
||||
'subtitle': False,
|
||||
# user / environment
|
||||
'company': company,
|
||||
'email_add_signature': email_add_signature,
|
||||
'lang': lang,
|
||||
'signature': signature,
|
||||
'website_url': website_url,
|
||||
# tools
|
||||
'is_html_empty': is_html_empty,
|
||||
}
|
||||
|
||||
def _notify_by_email_add_values(self, base_mail_values):
|
||||
def _notify_by_email_get_base_mail_values(self, message, additional_values=None):
|
||||
""" Add model-specific values to the dictionary used to create the
|
||||
notification email. Its base behavior is to compute model-specific
|
||||
headers.
|
||||
@@ -2377,14 +2465,55 @@ class MailThread(models.AbstractModel):
|
||||
:param dict base_mail_values: base mail.mail values, holding message
|
||||
to notify (mail_message_id and its fields), server, references, subject.
|
||||
"""
|
||||
headers = self._notify_email_headers()
|
||||
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
|
||||
# Replace new lines by spaces to conform to email headers requirements
|
||||
mail_subject = ' '.join((mail_subject or '').splitlines())
|
||||
# prepare notification mail values
|
||||
base_mail_values = {
|
||||
'mail_message_id': message.id,
|
||||
'mail_server_id': message.mail_server_id.id, # 2 query, check acces + read, may be useless, Falsy, when will it be used?
|
||||
# due to ir.rule, user have no right to access parent message if message is not published
|
||||
'references': message.parent_id.sudo().message_id if message.parent_id else False,
|
||||
'subject': mail_subject,
|
||||
}
|
||||
if additional_values:
|
||||
base_mail_values.update(additional_values)
|
||||
|
||||
headers = self._notify_by_email_get_headers()
|
||||
if headers:
|
||||
base_mail_values['headers'] = headers
|
||||
base_mail_values['headers'] = repr(headers)
|
||||
return base_mail_values
|
||||
|
||||
def _notify_compute_recipients(self, message, msg_vals):
|
||||
def _notify_by_email_get_final_mail_values(self, recipient_ids, base_mail_values, additional_values=None):
|
||||
""" Format email notification recipient values to store on the notification
|
||||
mail.mail. Basic method just set the recipient partners as mail_mail
|
||||
recipients. Override to generate other mail values like email_to or
|
||||
email_cc.
|
||||
:param recipient_ids: res.partner recordset to notify
|
||||
"""
|
||||
final_mail_values = dict(base_mail_values)
|
||||
final_mail_values['recipient_ids'] = [Command.link(pid) for pid in recipient_ids]
|
||||
if additional_values:
|
||||
final_mail_values.update(additional_values)
|
||||
return final_mail_values
|
||||
|
||||
def _notify_get_recipients(self, message, msg_vals):
|
||||
""" Compute recipients to notify based on subtype and followers. This
|
||||
method returns data structured as expected for ``_notify_recipients``. """
|
||||
method returns data structured as expected for ``_notify_recipients``.
|
||||
|
||||
TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
|
||||
and correctly override _notify_get_recipients
|
||||
|
||||
:return list recipients_data: this is a list of recipients information (see
|
||||
``MailFollowers._get_recipient_data()`` for more details) formatted like
|
||||
[{'active': partner.active;
|
||||
'id': id of the res.partner;
|
||||
'groups': res.group IDs if linked to a user;
|
||||
'notif': 'inbox', 'email', 'sms' (SMS App);
|
||||
'share': partner.partner_share;
|
||||
'type': 'customer', 'portal', 'user;'
|
||||
}, {...}]
|
||||
"""
|
||||
msg_sudo = message.sudo()
|
||||
# get values from msg_vals or from message if msg_vals doen't exists
|
||||
pids = msg_vals.get('partner_ids', []) if msg_vals else msg_sudo.partner_ids.ids
|
||||
@@ -2393,70 +2522,20 @@ class MailThread(models.AbstractModel):
|
||||
# is it possible to have record but no subtype_id ?
|
||||
recipients_data = []
|
||||
|
||||
res = self.env['mail.followers']._get_recipient_data(self, message_type, subtype_id, pids)
|
||||
res = self.env['mail.followers']._get_recipient_data(self, message_type, subtype_id, pids)[self.id if self else 0]
|
||||
if not res:
|
||||
return recipients_data
|
||||
|
||||
author_id = msg_vals.get('author_id') or message.author_id.id
|
||||
for pid, active, pshare, notif, groups in res:
|
||||
for pid, pdata in res.items():
|
||||
if pid and pid == author_id and not self.env.context.get('mail_notify_author'): # do not notify the author of its own messages
|
||||
continue
|
||||
if pid:
|
||||
if active is False:
|
||||
continue
|
||||
pdata = {'id': pid, 'active': active, 'share': pshare, 'groups': groups or []}
|
||||
if notif == 'inbox':
|
||||
recipients_data.append(dict(pdata, notif=notif, type='user'))
|
||||
elif not pshare and notif: # has an user and is not shared, is therefore user
|
||||
recipients_data.append(dict(pdata, notif=notif, type='user'))
|
||||
elif pshare and notif: # has an user but is shared, is therefore portal
|
||||
recipients_data.append(dict(pdata, notif=notif, type='portal'))
|
||||
else: # has no user, is therefore customer
|
||||
recipients_data.append(dict(pdata, notif=notif if notif else 'email', type='customer'))
|
||||
|
||||
if pdata['active'] is False:
|
||||
continue
|
||||
recipients_data.append(pdata)
|
||||
return recipients_data
|
||||
|
||||
@api.model
|
||||
def _notify_encode_link(self, base_link, params):
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params)))
|
||||
hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
|
||||
return hm
|
||||
|
||||
def _notify_get_action_link(self, link_type, **kwargs):
|
||||
""" Prepare link to an action: view document, follow document, ... """
|
||||
params = {
|
||||
'model': kwargs.get('model', self._name),
|
||||
'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
|
||||
}
|
||||
# whitelist accepted parameters: action (deprecated), token (assign), access_token
|
||||
# (view), auth_signup_token and auth_login (for auth_signup support)
|
||||
params.update(dict(
|
||||
(key, value)
|
||||
for key, value in kwargs.items()
|
||||
if key in ('action', 'token', 'access_token', 'auth_signup_token', 'auth_login')
|
||||
))
|
||||
|
||||
if link_type in ['view', 'assign', 'follow', 'unfollow']:
|
||||
base_link = '/mail/%s' % link_type
|
||||
elif link_type == 'controller':
|
||||
controller = kwargs.get('controller')
|
||||
params.pop('model')
|
||||
base_link = '%s' % controller
|
||||
else:
|
||||
return ''
|
||||
|
||||
if link_type not in ['view']:
|
||||
token = self._notify_encode_link(base_link, params)
|
||||
params['token'] = token
|
||||
|
||||
link = '%s?%s' % (base_link, urls.url_encode(params))
|
||||
if self:
|
||||
link = self[0].get_base_url() + link
|
||||
|
||||
return link
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Return groups used to classify recipients of a notification email.
|
||||
Groups is a list of tuple containing of form (group_name, group_func,
|
||||
group_data) where
|
||||
@@ -2477,58 +2556,65 @@ class MailThread(models.AbstractModel):
|
||||
* actions: list of action buttons to display in the notification email.
|
||||
Each action is a dict containing url and title of the button.
|
||||
Groups has a default value that you can find in mail_thread
|
||||
``_notify_classify_recipients`` method.
|
||||
``_notify_get_recipients_classify`` method.
|
||||
"""
|
||||
return [
|
||||
(
|
||||
[
|
||||
'user',
|
||||
lambda pdata: pdata['type'] == 'user',
|
||||
{'has_button_access': True}
|
||||
), (
|
||||
], [
|
||||
'portal',
|
||||
lambda pdata: pdata['type'] == 'portal',
|
||||
{'has_button_access': False}
|
||||
), (
|
||||
{'active': False, # activate only on demand if rights are enabled
|
||||
'has_button_access': False,
|
||||
}
|
||||
], [
|
||||
'follower',
|
||||
lambda pdata: pdata['is_follower'],
|
||||
{'active': False, # activate only on demand if rights are enabled
|
||||
'has_button_access': False,
|
||||
}
|
||||
], [
|
||||
'customer',
|
||||
lambda pdata: True,
|
||||
{'has_button_access': False}
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
def _notify_classify_recipients(self, recipient_data, model_name, msg_vals=None):
|
||||
def _notify_get_recipients_classify(self, recipient_data, model_name, msg_vals=None):
|
||||
""" Classify recipients to be notified of a message in groups to have
|
||||
specific rendering depending on their group. For example users could
|
||||
have access to buttons customers should not have in their emails.
|
||||
Module-specific grouping should be done by overriding ``_notify_get_groups``
|
||||
Module-specific grouping should be done by overriding ``_notify_get_recipients_groups``
|
||||
method defined here-under.
|
||||
:param recipient_data:todo xdo UPDATE ME
|
||||
return example:
|
||||
[{
|
||||
'actions': [],
|
||||
'button_access': {'title': 'View Simple Chatter Model',
|
||||
|
||||
:param recipient_data: list of recipients information (based on res.partner
|
||||
records). See ``MailThread._notify_get_recipients()``;
|
||||
|
||||
:return list: list of groups formatted for notification processing like
|
||||
[{'active': True,
|
||||
'actions': [],
|
||||
'button_access': {},
|
||||
'has_button_access': False,
|
||||
'recipients': [11],},
|
||||
{'active': True,
|
||||
'actions': [],
|
||||
'button_access': {'title': 'View Simple Chatter Model',
|
||||
'url': '/mail/view?model=mail.test.simple&res_id=1497'},
|
||||
'has_button_access': False,
|
||||
'recipients': [11]
|
||||
},
|
||||
{
|
||||
'actions': [],
|
||||
'button_access': {'title': 'View Simple Chatter Model',
|
||||
'url': '/mail/view?model=mail.test.simple&res_id=1497'},
|
||||
'has_button_access': False,
|
||||
'recipients': [4, 5, 6]
|
||||
},
|
||||
{
|
||||
'actions': [],
|
||||
'button_access': {'title': 'View Simple Chatter Model',
|
||||
'has_button_access': True,
|
||||
'recipients': [4, 5, 6],},
|
||||
{'active': True,
|
||||
'actions': [],
|
||||
'button_access': {'title': 'View Simple Chatter Model',
|
||||
'url': '/mail/view?model=mail.test.simple&res_id=1497'},
|
||||
'has_button_access': True,
|
||||
'recipients': [10, 11, 12]
|
||||
}]
|
||||
only return groups with recipients
|
||||
'has_button_access': True,
|
||||
'recipients': [10, 11, 12],}
|
||||
]
|
||||
"""
|
||||
# keep a local copy of msg_vals as it may be modified to include more information about groups or links
|
||||
local_msg_vals = dict(msg_vals) if msg_vals else {}
|
||||
groups = self._notify_get_groups(msg_vals=local_msg_vals)
|
||||
groups = self._notify_get_recipients_groups(msg_vals=local_msg_vals)
|
||||
access_link = self._notify_get_action_link('view', **local_msg_vals)
|
||||
|
||||
if model_name:
|
||||
@@ -2538,40 +2624,69 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
# fill group_data with default_values if they are not complete
|
||||
for group_name, group_func, group_data in groups:
|
||||
group_data.setdefault('notification_group_name', group_name)
|
||||
group_data.setdefault('notification_is_customer', False)
|
||||
group_data.setdefault('active', True)
|
||||
group_data.setdefault('actions', list())
|
||||
group_data.setdefault('has_button_access', True)
|
||||
group_data.setdefault('notification_is_customer', False)
|
||||
group_data.setdefault('notification_group_name', group_name)
|
||||
group_data.setdefault('recipients', list())
|
||||
group_button_access = group_data.setdefault('button_access', {})
|
||||
group_button_access.setdefault('url', access_link)
|
||||
group_button_access.setdefault('title', view_title)
|
||||
group_data.setdefault('actions', list())
|
||||
group_data.setdefault('recipients', list())
|
||||
|
||||
# classify recipients in each group
|
||||
for recipient in recipient_data:
|
||||
for group_name, group_func, group_data in groups:
|
||||
if group_func(recipient):
|
||||
if group_data['active'] and group_func(recipient):
|
||||
group_data['recipients'].append(recipient['id'])
|
||||
break
|
||||
|
||||
result = []
|
||||
for group_name, group_method, group_data in groups:
|
||||
if group_data['recipients']:
|
||||
result.append(group_data)
|
||||
# filter out groups without recipients
|
||||
return [group_data for _group_name, _group_func, group_data in groups
|
||||
if group_data['recipients']]
|
||||
|
||||
return result
|
||||
@api.model
|
||||
def _notify_encode_link(self, base_link, params):
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params)))
|
||||
hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
|
||||
return hm
|
||||
|
||||
def _notify_email_recipient_values(self, recipient_ids):
|
||||
""" Format email notification recipient values to store on the notification
|
||||
mail.mail. Basic method just set the recipient partners as mail_mail
|
||||
recipients. Override to generate other mail values like email_to or
|
||||
email_cc.
|
||||
:param recipient_ids: res.partner recordset to notify
|
||||
"""
|
||||
return {
|
||||
'email_to': False,
|
||||
'recipient_ids': recipient_ids,
|
||||
def _notify_get_action_link(self, link_type, **kwargs):
|
||||
""" Prepare link to an action: view document, follow document, ... """
|
||||
params = {
|
||||
'model': kwargs.get('model', self._name),
|
||||
'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
|
||||
}
|
||||
# keep only accepted parameters:
|
||||
# - action (deprecated), token (assign), access_token (view)
|
||||
# - auth_signup: auth_signup_token and auth_login
|
||||
# - portal: pid, hash
|
||||
params.update(dict(
|
||||
(key, value)
|
||||
for key, value in kwargs.items()
|
||||
if key in ('action', 'token', 'access_token', 'auth_signup_token',
|
||||
'auth_login', 'pid', 'hash')
|
||||
))
|
||||
|
||||
if link_type in ['view', 'assign', 'follow', 'unfollow']:
|
||||
base_link = '/mail/%s' % link_type
|
||||
elif link_type == 'controller':
|
||||
controller = kwargs.get('controller')
|
||||
params.pop('model')
|
||||
base_link = '%s' % controller
|
||||
else:
|
||||
return ''
|
||||
|
||||
if link_type not in ['view']:
|
||||
token = self._notify_encode_link(base_link, params)
|
||||
params['token'] = token
|
||||
|
||||
link = '%s?%s' % (base_link, urls.url_encode(params))
|
||||
if self:
|
||||
link = self[0].get_base_url() + link
|
||||
|
||||
return link
|
||||
|
||||
# ------------------------------------------------------
|
||||
# FOLLOWERS API
|
||||
|
||||
@@ -84,7 +84,7 @@ class BaseModel(models.AbstractModel):
|
||||
res[record.id] = {'partner_ids': recipient_ids, 'email_to': email_to, 'email_cc': email_cc}
|
||||
return res
|
||||
|
||||
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Returns the preferred reply-to email address when replying to a thread
|
||||
on documents. This method is a generic implementation available for
|
||||
all models as we could send an email through mail templates on models
|
||||
@@ -104,19 +104,9 @@ class BaseModel(models.AbstractModel):
|
||||
An example would be tasks taking their reply-to alias from their project.
|
||||
|
||||
:param default: default email if no alias or catchall is found;
|
||||
:param records: DEPRECATED, self should be a valid record set or an
|
||||
empty recordset if a generic reply-to is required;
|
||||
:param company: used to compute company name part of the from name; provide
|
||||
it if already known, otherwise fall back on user company;
|
||||
:param doc_names: dict(res_id, doc_name) used to compute doc name part of
|
||||
the from name; provide it if already known to avoid queries, otherwise
|
||||
name_get on document will be performed;
|
||||
:return result: dictionary. Keys are record IDs and value is formatted
|
||||
like an email "Company_name Document_name <reply_to@email>"/
|
||||
"""
|
||||
if records:
|
||||
raise ValueError('Use of records is deprecated as this method is available on BaseModel.')
|
||||
|
||||
_records = self
|
||||
model = _records._name if _records and _records._name != 'mail.thread' else False
|
||||
res_ids = _records.ids if _records and model else []
|
||||
@@ -125,7 +115,7 @@ class BaseModel(models.AbstractModel):
|
||||
alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
|
||||
result = dict.fromkeys(_res_ids, False)
|
||||
result_email = dict()
|
||||
doc_names = doc_names if doc_names else dict()
|
||||
doc_names = dict()
|
||||
|
||||
if alias_domain:
|
||||
if model and res_ids:
|
||||
@@ -148,7 +138,7 @@ class BaseModel(models.AbstractModel):
|
||||
result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
|
||||
|
||||
# compute name of reply-to - TDE tocheck: quotes and stuff like that
|
||||
company_name = company.name if company else self.env.company.name
|
||||
company_name = self.env.company.name
|
||||
for res_id in result_email:
|
||||
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
|
||||
result[res_id] = tools.formataddr((name, result_email[res_id]))
|
||||
@@ -203,16 +193,11 @@ class BaseModel(models.AbstractModel):
|
||||
'&', ('hidden', '=', False),
|
||||
'|', ('res_model', '=', self._name), ('res_model', '=', False)])
|
||||
|
||||
def _notify_email_headers(self):
|
||||
"""
|
||||
Generate the email headers based on record
|
||||
"""
|
||||
def _notify_by_email_get_headers(self):
|
||||
""" Generate the email headers based on record """
|
||||
if not self:
|
||||
return {}
|
||||
self.ensure_one()
|
||||
return repr(self._notify_email_header_dict())
|
||||
|
||||
def _notify_email_header_dict(self):
|
||||
return {
|
||||
'X-Odoo-Objects': "%s-%s" % (self._name, self.id),
|
||||
}
|
||||
|
||||
+172
-4
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import email
|
||||
import email.policy
|
||||
import time
|
||||
@@ -601,21 +602,23 @@ class MailCase(MockEmail):
|
||||
return cls.email_template
|
||||
|
||||
|
||||
def _generate_notify_recipients(self, partners):
|
||||
def _generate_notify_recipients(self, partners, record=None):
|
||||
""" Tool method to generate recipients data according to structure used
|
||||
in notification methods. Purpose is to allow testing of internals of
|
||||
some notification methods, notably testing links or group-based notification
|
||||
details.
|
||||
|
||||
See notably ``MailThread._notify_compute_recipients()``.
|
||||
See notably ``MailThread._notify_get_recipients()``.
|
||||
"""
|
||||
return [
|
||||
{'id': partner.id,
|
||||
'active': True,
|
||||
'share': partner.partner_share,
|
||||
'active': partner.active,
|
||||
'is_follower': partner in record.message_partner_ids if record else False,
|
||||
'groups': partner.user_ids.groups_id.ids,
|
||||
'notif': partner.user_ids.notification_type or 'email',
|
||||
'share': partner.partner_share,
|
||||
'type': 'user' if partner.user_ids and not partner.partner_share else partner.user_ids and 'portal' or 'customer',
|
||||
'ushare': all(user.share for user in partner.user_ids) if partner.user_ids else False,
|
||||
} for partner in partners
|
||||
]
|
||||
|
||||
@@ -940,3 +943,168 @@ class MailCommon(common.TransactionCase, MailCase):
|
||||
signature='--\nEnguerrand'
|
||||
)
|
||||
cls.partner_employee_c2 = cls.user_employee_c2.partner_id
|
||||
|
||||
@classmethod
|
||||
def _activate_multi_lang(cls, lang_code='es_ES', layout_arch_db=None, test_record=False, test_template=False):
|
||||
""" Summary of es_ES matching done here (a bit hardcoded to ease tests)
|
||||
|
||||
* layout
|
||||
* 'English Layout for' -> Spanish Layout para
|
||||
* model
|
||||
* description: English: Lang Chatter Model (depends on test_record._name)
|
||||
translated: Spanish description
|
||||
* module
|
||||
* _('TestStuff') -> TestSpanishStuff (used as link button name in layout)
|
||||
* _('View %s') -> SpanishView %s
|
||||
* template
|
||||
* body: English: <p>EnglishBody for <t t-out="object.name"/></p> (depends on test_template.body)
|
||||
translated: <p>SpanishBody for <t t-out="object.name" /></p>
|
||||
* subject: English: EnglishSubject for {{ object.name }} (depends on test_template.subject)
|
||||
translated: SpanishSubject for {{ object.name }}
|
||||
"""
|
||||
# activate translations
|
||||
cls.env['res.lang']._activate_lang(lang_code)
|
||||
cls.env.ref('base.module_base')._update_translations([lang_code])
|
||||
|
||||
# Make sure Spanish translations have not been altered
|
||||
if test_record:
|
||||
description_translations = cls.env['ir.translation'].search([
|
||||
('module', '=', 'test_mail'),
|
||||
('src', '=', test_record._description),
|
||||
('lang', '=', lang_code)
|
||||
])
|
||||
if description_translations:
|
||||
description_translations.update({'value': 'Spanish description'})
|
||||
else:
|
||||
description_translations.create({
|
||||
'lang': lang_code,
|
||||
'module': 'test_mail',
|
||||
'name': 'ir.model,name',
|
||||
'res_id': cls.env['ir.model']._get_id(test_record._name),
|
||||
'src': test_record._description,
|
||||
'state': 'translated',
|
||||
'type': 'model',
|
||||
'value': 'Spanish description',
|
||||
})
|
||||
|
||||
translations_tocreate = []
|
||||
# Have a TestStuff always available
|
||||
test_stuff_translations = cls.env['ir.translation'].search([
|
||||
('module', '=', 'test_mail'),
|
||||
('src', '=', 'TestStuff'),
|
||||
('lang', '=', lang_code)
|
||||
])
|
||||
if test_stuff_translations:
|
||||
test_stuff_translations.update({'value': 'TestSpanishStuff'})
|
||||
else:
|
||||
translations_tocreate.append({
|
||||
'lang': lang_code,
|
||||
'name': 'idontknow',
|
||||
'module': 'test_mail',
|
||||
'res_id': False,
|
||||
'src': 'TestStuff',
|
||||
'state': 'translated',
|
||||
'type': 'code',
|
||||
'value': 'TestSpanishStuff',
|
||||
})
|
||||
|
||||
view_translations = cls.env['ir.translation'].search([
|
||||
('module', '=', 'mail'),
|
||||
('src', '=', 'View %s'),
|
||||
('lang', '=', lang_code)
|
||||
])
|
||||
if view_translations:
|
||||
view_translations.update({'value': 'SpanishView'})
|
||||
else:
|
||||
translations_tocreate.append({
|
||||
'lang': lang_code,
|
||||
'name': 'idontknow',
|
||||
'module': 'mail',
|
||||
'res_id': False,
|
||||
'src': 'View %s',
|
||||
'state': 'translated',
|
||||
'type': 'code',
|
||||
'value': 'SpanishView %s',
|
||||
})
|
||||
|
||||
# Prepare some translated value for template if given
|
||||
if test_template:
|
||||
translations_tocreate += [{
|
||||
'lang': lang_code,
|
||||
'module': 'mail',
|
||||
'name': 'mail.template,subject',
|
||||
'res_id': test_template.id,
|
||||
'state': 'translated',
|
||||
'type': 'model',
|
||||
'value': 'SpanishSubject for {{ object.name }}',
|
||||
}, {
|
||||
'lang': lang_code,
|
||||
'module': 'mail',
|
||||
'name': 'mail.template,body_html',
|
||||
'res_id': test_template.id,
|
||||
'state': 'translated',
|
||||
'type': 'model',
|
||||
'value': '<p>SpanishBody for <t t-out="object.name" /></p>',
|
||||
}]
|
||||
|
||||
# create a custom layout for email notification
|
||||
if not layout_arch_db:
|
||||
layout_arch_db = """
|
||||
<body>
|
||||
<p>English Layout for <t t-esc="model_description"/></p>
|
||||
<img t-att-src="'/logo.png?company=%s' % (company.id or 0)" t-att-alt="'%s' % company.name"/>
|
||||
<a t-if="has_button_access" t-att-href="button_access['url']">
|
||||
<t t-esc="button_access['title']"/>
|
||||
</a>
|
||||
<t t-if="actions">
|
||||
<t t-foreach="actions" t-as="action">
|
||||
<a t-att-href="action['url']">
|
||||
<t t-esc="action['title']"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<t t-out="message.body"/>
|
||||
<ul t-if="tracking_values">
|
||||
<li t-foreach="tracking_values" t-as="tracking">
|
||||
<t t-esc="tracking[0]"/>: <t t-esc="tracking[1]"/> -> <t t-esc="tracking[2]"/>
|
||||
</li>
|
||||
</ul>
|
||||
<div t-if="signature" t-out="signature"/>
|
||||
<p>Sent by <t t-esc="company.name"/></p>
|
||||
</body>"""
|
||||
view = cls.env['ir.ui.view'].create({
|
||||
'arch_db': layout_arch_db,
|
||||
'key': 'test_layout',
|
||||
'name': 'test_layout',
|
||||
'type': 'qweb',
|
||||
})
|
||||
cls.env['ir.model.data'].create({
|
||||
'model': 'ir.ui.view',
|
||||
'module': 'mail',
|
||||
'name': 'test_layout',
|
||||
'res_id': view.id
|
||||
})
|
||||
translations_tocreate.append({
|
||||
'lang': lang_code,
|
||||
'module': 'mail',
|
||||
'name': 'ir.ui.view,arch_db',
|
||||
'res_id': view.id,
|
||||
'src': 'English Layout for',
|
||||
'state': 'translated',
|
||||
'type': 'model_terms',
|
||||
'value': 'Spanish Layout para',
|
||||
})
|
||||
cls.env['ir.translation'].create(translations_tocreate)
|
||||
|
||||
def _generate_attachments_data(self, count, res_model=None, res_id=None):
|
||||
# attachment visibility depends on what they are attached to
|
||||
if res_model is None:
|
||||
res_model = self.template._name
|
||||
if res_id is None:
|
||||
res_id = self.template.id
|
||||
return [{
|
||||
'name': '%02d.txt' % x,
|
||||
'datas': base64.b64encode(b'Att%02d' % x),
|
||||
'res_model': res_model,
|
||||
'res_id': res_id,
|
||||
} for x in range(count)]
|
||||
|
||||
@@ -100,8 +100,7 @@ class MailComposer(models.TransientModel):
|
||||
'ir.attachment', 'mail_compose_message_ir_attachments_rel',
|
||||
'wizard_id', 'attachment_id', 'Attachments')
|
||||
email_layout_xmlid = fields.Char('Email Notification Layout', copy=False)
|
||||
layout = fields.Char('Layout', copy=False) # xml id of layout
|
||||
add_sign = fields.Boolean(default=True)
|
||||
email_add_signature = fields.Boolean(default=True)
|
||||
# origin
|
||||
email_from = fields.Char('From', help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.")
|
||||
author_id = fields.Many2one(
|
||||
@@ -252,7 +251,7 @@ class MailComposer(models.TransientModel):
|
||||
# 'purchase.order' which is used for a RFQ and and PO. To avoid confusion, we must use a
|
||||
# different wording depending on the state of the object.
|
||||
# Therefore, we can set the description in the context from the beginning to avoid falling
|
||||
# back on the regular display_name retrieved in '_notify_prepare_template_context'.
|
||||
# back on the regular display_name retrieved in ``_notify_by_email_prepare_rendering_context()``.
|
||||
model_description = self._context.get('model_description')
|
||||
|
||||
for wizard in self:
|
||||
@@ -311,7 +310,7 @@ class MailComposer(models.TransientModel):
|
||||
message_type=wizard.message_type,
|
||||
subtype_id=subtype_id,
|
||||
email_layout_xmlid=wizard.email_layout_xmlid,
|
||||
add_sign=not bool(wizard.template_id),
|
||||
email_add_signature=not bool(wizard.template_id) and wizard.email_add_signature,
|
||||
mail_auto_delete=wizard.template_id.auto_delete if wizard.template_id else self._context.get('mail_auto_delete', True),
|
||||
model_description=model_description)
|
||||
post_params.update(mail_values)
|
||||
@@ -395,7 +394,7 @@ class MailComposer(models.TransientModel):
|
||||
# mass mailing: rendering override wizard static values
|
||||
if mass_mail_mode and self.model:
|
||||
record = self.env[self.model].browse(res_id)
|
||||
mail_values['headers'] = record._notify_email_headers()
|
||||
mail_values['headers'] = repr(record._notify_by_email_get_headers())
|
||||
# keep a copy unless specifically requested, reset record name (avoid browsing records)
|
||||
mail_values.update(is_notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False)
|
||||
# auto deletion of mail_mail
|
||||
|
||||
@@ -68,17 +68,16 @@ class MailResendMessage(models.TransientModel):
|
||||
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
|
||||
|
||||
email_partners_data = []
|
||||
for pid, active, pshare, notif, groups in self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids):
|
||||
if pid and notif == 'email' or not notif:
|
||||
pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
|
||||
if not pshare and notif: # has an user and is not shared, is therefore user
|
||||
email_partners_data.append(dict(pdata, type='user'))
|
||||
elif pshare and notif: # has an user and is shared, is therefore portal
|
||||
email_partners_data.append(dict(pdata, type='portal'))
|
||||
else: # has no user, is therefore customer
|
||||
email_partners_data.append(dict(pdata, type='customer'))
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids)[0]
|
||||
for pid, pdata in recipients_data.items():
|
||||
if pid and pdata.get('notif', 'email') == 'email':
|
||||
email_partners_data.append(pdata)
|
||||
|
||||
record._notify_record_by_email(message, email_partners_data, check_existing=True, send_after_commit=False)
|
||||
record._notify_thread_by_email(
|
||||
message, email_partners_data,
|
||||
check_existing=True,
|
||||
send_after_commit=False
|
||||
)
|
||||
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -68,20 +68,18 @@ class Invite(models.TransientModel):
|
||||
'model': wizard.res_model,
|
||||
'res_id': wizard.res_id,
|
||||
'reply_to_force_new': True,
|
||||
'add_sign': True,
|
||||
'email_add_signature': True,
|
||||
})
|
||||
partners_data = []
|
||||
recipient_data = self.env['mail.followers']._get_recipient_data(document, 'comment', False, pids=new_partners.ids)
|
||||
for pid, active, pshare, notif, groups in recipient_data:
|
||||
pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
|
||||
if not pshare and notif: # has an user and is not shared, is therefore user
|
||||
partners_data.append(dict(pdata, type='user'))
|
||||
elif pshare and notif: # has an user and is shared, is therefore portal
|
||||
partners_data.append(dict(pdata, type='portal'))
|
||||
else: # has no user, is therefore customer
|
||||
partners_data.append(dict(pdata, type='customer'))
|
||||
email_partners_data = []
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(document, 'comment', False, pids=new_partners.ids)[document.id]
|
||||
for _pid, pdata in recipients_data.items():
|
||||
pdata['notif'] = 'email'
|
||||
email_partners_data.append(pdata)
|
||||
|
||||
document._notify_record_by_email(message, partners_data, send_after_commit=False)
|
||||
document._notify_thread_by_email(
|
||||
message, email_partners_data,
|
||||
send_after_commit=False
|
||||
)
|
||||
# in case of failure, the web client must know the message was
|
||||
# deleted to discard the related failure notification
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.message/delete', {'message_ids': message.ids})
|
||||
|
||||
@@ -424,7 +424,7 @@ class MailGroup(models.Model):
|
||||
# SMTP headers related to the subscription
|
||||
email_url_encoded = urls.url_quote(email_member)
|
||||
headers = {
|
||||
** self._notify_email_header_dict(),
|
||||
** self._notify_by_email_get_headers(),
|
||||
'List-Archive': f'<{base_url}/groups/{slug(self)}>',
|
||||
'List-Subscribe': f'<{base_url}/groups?email={email_url_encoded}>',
|
||||
'List-Unsubscribe': f'<{base_url}/groups?unsubscribe&email={email_url_encoded}>',
|
||||
|
||||
@@ -230,7 +230,7 @@ class MailController(mail.MailController):
|
||||
record_sudo.with_user(uid).check_access_rule('read')
|
||||
except AccessError:
|
||||
if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token):
|
||||
record_action = record_sudo.with_context(force_website=True).get_access_action()
|
||||
record_action = record_sudo._get_access_action(force_website=True)
|
||||
if record_action['type'] == 'ir.actions.act_url':
|
||||
pid = kwargs.get('pid')
|
||||
hash = kwargs.get('hash')
|
||||
|
||||
@@ -46,10 +46,14 @@ class PortalMixin(models.AbstractModel):
|
||||
:return: the url of the record with access parameters, if any.
|
||||
"""
|
||||
self.ensure_one()
|
||||
params = {
|
||||
'model': self._name,
|
||||
'res_id': self.id,
|
||||
}
|
||||
if redirect:
|
||||
# model / res_id used by mail/view to check access on record
|
||||
params = {
|
||||
'model': self._name,
|
||||
'res_id': self.id,
|
||||
}
|
||||
else:
|
||||
params = {}
|
||||
if share_token and hasattr(self, 'access_token'):
|
||||
params['access_token'] = self._portal_ensure_token()
|
||||
if pid:
|
||||
@@ -60,14 +64,16 @@ class PortalMixin(models.AbstractModel):
|
||||
|
||||
return '%s?%s' % ('/mail/view' if redirect else self.access_url, url_encode(params))
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
access_token = self._portal_ensure_token()
|
||||
groups = super(PortalMixin, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(PortalMixin, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
if access_token and 'partner_id' in self._fields and self['partner_id']:
|
||||
customer = self['partner_id']
|
||||
local_msg_vals['access_token'] = self.access_token
|
||||
local_msg_vals['pid'] = customer.id
|
||||
local_msg_vals['hash'] = self._sign_token(customer.id)
|
||||
local_msg_vals.update(customer.signup_get_auth_param()[customer.id])
|
||||
access_link = self._notify_get_action_link('view', **local_msg_vals)
|
||||
|
||||
@@ -82,11 +88,18 @@ class PortalMixin(models.AbstractModel):
|
||||
]
|
||||
else:
|
||||
new_group = []
|
||||
|
||||
# enable portal users that should have access through portal (if not access rights
|
||||
# will do their duty)
|
||||
portal_group = next(group for group in groups if group[0] == 'portal')
|
||||
portal_group[2]['active'] = True
|
||||
portal_group[2]['has_button_access'] = True
|
||||
|
||||
return new_group + groups
|
||||
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the online document for
|
||||
portal users or if force_website=True in the context. """
|
||||
portal users or if force_website=True. """
|
||||
self.ensure_one()
|
||||
|
||||
user, record = self.env.user, self
|
||||
@@ -95,15 +108,17 @@ class PortalMixin(models.AbstractModel):
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule("read")
|
||||
except exceptions.AccessError:
|
||||
return super(PortalMixin, self).get_access_action(access_uid)
|
||||
return super(PortalMixin, self)._get_access_action(
|
||||
access_uid=access_uid, force_website=force_website
|
||||
)
|
||||
user = self.env['res.users'].sudo().browse(access_uid)
|
||||
if user.share or force_website:
|
||||
record = self.with_user(user)
|
||||
if user.share or self.env.context.get('force_website'):
|
||||
try:
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule('read')
|
||||
except exceptions.AccessError:
|
||||
if self.env.context.get('force_website'):
|
||||
if force_website:
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': record.access_url,
|
||||
@@ -119,7 +134,9 @@ class PortalMixin(models.AbstractModel):
|
||||
'target': 'self',
|
||||
'res_id': record.id,
|
||||
}
|
||||
return super(PortalMixin, self).get_access_action(access_uid)
|
||||
return super(PortalMixin, self)._get_access_action(
|
||||
access_uid=access_uid, force_website=force_website
|
||||
)
|
||||
|
||||
@api.model
|
||||
def action_share(self):
|
||||
|
||||
@@ -1951,12 +1951,12 @@ class Task(models.Model):
|
||||
res -= dependency_subtype
|
||||
return res
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Handle project users and managers recipients that can assign
|
||||
tasks and create new one directly from notification emails. Also give
|
||||
access button to portal users and portal customers. If they are notified
|
||||
they should probably have access to the document. """
|
||||
groups = super(Task, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(Task, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
self.ensure_one()
|
||||
|
||||
@@ -1983,13 +1983,13 @@ class Task(models.Model):
|
||||
|
||||
return groups
|
||||
|
||||
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Override to set alias of tasks to their project if any. """
|
||||
aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
|
||||
aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default)
|
||||
res = {task.id: aliases.get(task.project_id.id) for task in self}
|
||||
leftover = self.filtered(lambda rec: not rec.project_id)
|
||||
if leftover:
|
||||
res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
|
||||
res.update(super(Task, leftover)._notify_get_reply_to(default=default))
|
||||
return res
|
||||
|
||||
def email_split(self, msg):
|
||||
@@ -2044,8 +2044,8 @@ class Task(models.Model):
|
||||
task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
|
||||
return recipients
|
||||
|
||||
def _notify_email_header_dict(self):
|
||||
headers = super(Task, self)._notify_email_header_dict()
|
||||
def _notify_by_email_get_headers(self):
|
||||
headers = super(Task, self)._notify_by_email_get_headers()
|
||||
if self.project_id:
|
||||
current_objects = [h for h in headers.get('X-Odoo-Objects', '').split(',') if h]
|
||||
current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
If you have any questions, please do not hesitate to contact us.
|
||||
<br/><br/>
|
||||
Best regards,
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="report_purchase_quotation"/>
|
||||
@@ -56,6 +60,11 @@
|
||||
<br/><br/>
|
||||
Could you please acknowledge the receipt of this order?
|
||||
</t>
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_purchase_order"/>
|
||||
@@ -90,6 +99,11 @@
|
||||
<strong>undefined</strong>.
|
||||
</t>
|
||||
Could you please confirm it will be delivered on time?
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div></field>
|
||||
<field name="report_template" ref="action_report_purchase_order"/>
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<template id="mail_notification_confirm" inherit_id="mail.mail_notification_paynow" name="Purchase: Confirmation mail notification template">
|
||||
<xpath expr="//t[@t-set='access_name']" position="after">
|
||||
<t t-if="record._name == 'purchase.order'">
|
||||
<t t-if="record.state == 'purchase' and not record.env.context.get('is_reminder')">
|
||||
<t t-set="access_name">Confirm</t>
|
||||
<t t-set="access_url" t-value="record.get_confirm_url(confirm_type='reception')"/>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//a[@t-att-href='access_url']" position="replace">
|
||||
<t t-if="record._name == 'purchase.order' and record.env.context.get('is_reminder')">
|
||||
<a t-att-href="record.get_confirm_url(confirm_type='reminder')"
|
||||
style="margin-right: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
Yes
|
||||
</a>
|
||||
<a t-att-href="record.get_update_url()"
|
||||
style="margin-left: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
No, Update Dates
|
||||
</a>
|
||||
<div>&nbsp;</div>
|
||||
<div style="margin: 0px; padding: 0px; font-size:13px; text-align: left;">
|
||||
If you have any questions, please do not hesitate to contact us.
|
||||
<div>&nbsp;</div>
|
||||
Best regards,
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-att-href="access_url"
|
||||
style="margin-left: 10px; background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
<t t-esc="access_name"/>
|
||||
</a>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="track_po_line_template">
|
||||
<div>
|
||||
<strong>The ordered quantity has been updated.</strong>
|
||||
|
||||
@@ -10,7 +10,7 @@ from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_amount, formatLang, get_lang, groupby
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_amount, format_date, formatLang, get_lang, groupby
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
|
||||
@@ -289,18 +289,6 @@ class PurchaseOrder(models.Model):
|
||||
del line[2]['date_planned']
|
||||
return result
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'state' in init_values and self.state == 'purchase':
|
||||
if init_values['state'] == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_approved')
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'done':
|
||||
return self.env.ref('purchase.mt_rfq_done')
|
||||
return super(PurchaseOrder, self)._track_subtype(init_values)
|
||||
|
||||
def _get_report_base_filename(self):
|
||||
self.ensure_one()
|
||||
return 'Purchase Order-%s' % (self.name)
|
||||
@@ -353,6 +341,69 @@ class PurchaseOrder(models.Model):
|
||||
return {'warning': warning}
|
||||
return {}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAIL.THREAD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
if self.env.context.get('mark_rfq_as_sent'):
|
||||
self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'})
|
||||
return super(PurchaseOrder, self.with_context(mail_post_autofollow=self.env.context.get('mail_post_autofollow', True))).message_post(**kwargs)
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Tweak 'view document' button for portal customers, calling directly
|
||||
routes for confirm specific to PO model. """
|
||||
groups = super(PurchaseOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
|
||||
customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
|
||||
if customer_portal_group:
|
||||
access_opt = customer_portal_group[2].setdefault('button_access', {})
|
||||
if self.env.context.get('is_reminder'):
|
||||
access_opt['title'] = _('View')
|
||||
actions = customer_portal_group[2].setdefault('actions', list())
|
||||
actions.extend([
|
||||
{'url': self.get_confirm_url(confirm_type='reminder'), 'title': _('Accept')},
|
||||
{'url': self.get_update_url(), 'title': _('Update Dates')},
|
||||
])
|
||||
else:
|
||||
access_opt['title'] = _('Confirm')
|
||||
access_opt['url'] = self.get_confirm_url(confirm_type='reception')
|
||||
|
||||
return groups
|
||||
|
||||
def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False,
|
||||
force_email_company=False, force_email_lang=False):
|
||||
render_context = super()._notify_by_email_prepare_rendering_context(
|
||||
message, msg_vals, model_description=model_description,
|
||||
force_email_company=force_email_company, force_email_lang=force_email_lang
|
||||
)
|
||||
if self.date_order:
|
||||
amount_txt = _('%(amount)s due %(date)s',
|
||||
amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')),
|
||||
date=format_date(self.env, self.date_order, date_format='short', lang_code=render_context.get('lang'))
|
||||
)
|
||||
else:
|
||||
amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang'))
|
||||
render_context['subtitle'] = Markup("<span>%s<br />%s</span>") % (self.name, amount_txt)
|
||||
return render_context
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'state' in init_values and self.state == 'purchase':
|
||||
if init_values['state'] == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_approved')
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'to approve':
|
||||
return self.env.ref('purchase.mt_rfq_confirmed')
|
||||
elif 'state' in init_values and self.state == 'done':
|
||||
return self.env.ref('purchase.mt_rfq_done')
|
||||
return super(PurchaseOrder, self)._track_subtype(init_values)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def action_rfq_send(self):
|
||||
'''
|
||||
This function opens a window to compose an email, with the edi purchase template message loaded by default
|
||||
@@ -410,12 +461,6 @@ class PurchaseOrder(models.Model):
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
if self.env.context.get('mark_rfq_as_sent'):
|
||||
self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'})
|
||||
return super(PurchaseOrder, self.with_context(mail_post_autofollow=self.env.context.get('mail_post_autofollow', True))).message_post(**kwargs)
|
||||
|
||||
def print_quotation(self):
|
||||
self.write({'state': "sent"})
|
||||
return self.env.ref('purchase.report_purchase_quotation').report_action(self)
|
||||
|
||||
@@ -24,7 +24,6 @@ This module contains all the common features of Sales Management and eCommerce.
|
||||
'data/ir_sequence_data.xml',
|
||||
'data/mail_data_various.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/sale_data.xml',
|
||||
|
||||
'wizard/account_accrued_orders_wizard_views.xml',
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
</t>
|
||||
<br/><br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<br/>
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
@@ -65,6 +69,10 @@
|
||||
</t>
|
||||
<br/><br/>
|
||||
Do not hesitate to contact us if you have any questions.
|
||||
<t t-if="not is_html_empty(object.user_id.signature)">
|
||||
<br />
|
||||
<t t-out="object.user_id.signature or ''">--<br/>Mitchell Admin</t>
|
||||
</t>
|
||||
<br/><br/>
|
||||
</p>
|
||||
<t t-if="hasattr(object, 'website_id') and object.website_id">
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<template id="mail_notification_paynow_online"
|
||||
inherit_id="mail.mail_notification_paynow"
|
||||
name="Quotation: Sign and Pay mail notification template">
|
||||
<xpath expr="//t[@t-set='access_name']" position="after">
|
||||
<t t-if="record._name == 'sale.order'">
|
||||
<t t-set="is_transaction_pending" t-value="record.get_portal_last_transaction().state == 'pending'"/>
|
||||
<t t-if="record.has_to_be_signed(include_draft=True)">
|
||||
<t t-if="record.has_to_be_paid()" t-set="access_name">
|
||||
<t t-if="is_transaction_pending">View Quotation</t>
|
||||
<t t-else="">Review, Sign & Pay Quotation</t>
|
||||
</t>
|
||||
<t t-else="" t-set="access_name">Review, Accept & Sign Quotation</t>
|
||||
</t>
|
||||
<t t-elif="record.has_to_be_paid(include_draft=True) and not is_transaction_pending">
|
||||
<t t-set="access_name">Review, Accept & Pay Quotation</t>
|
||||
</t>
|
||||
<t t-elif="record.state in ('draft', 'sent')">
|
||||
<t t-set="access_name">View Quotation</t>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
from markupsafe import Markup
|
||||
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero, html_keep_url, is_html_empty
|
||||
from odoo.tools import float_is_zero, format_amount, format_date, html_keep_url, is_html_empty
|
||||
|
||||
READONLY_FIELD_STATES = {
|
||||
state: [('readonly', True)]
|
||||
@@ -1069,6 +1071,64 @@ class SaleOrder(models.Model):
|
||||
transaction = self.get_portal_last_transaction()
|
||||
return (self.state == 'sent' or (self.state == 'draft' and include_draft)) and not self.is_expired and self.require_payment and transaction.state != 'done' and self.amount_total
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Give access button to users and portal customer as portal is integrated
|
||||
in sale. Customer and portal group have probably no right to see
|
||||
the document so they don't have the access button. """
|
||||
groups = super(SaleOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
if self._context.get('proforma'):
|
||||
for group in [g for g in groups if g[0] in ('portal_customer', 'portal', 'follower', 'customer')]:
|
||||
group[2]['has_button_access'] = False
|
||||
return groups
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
# portal customers have full access (existence not granted, depending on partner_id)
|
||||
try:
|
||||
customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
access_opt = customer_portal_group[2].setdefault('button_access', {})
|
||||
is_tx_pending = self.get_portal_last_transaction().state == 'pending'
|
||||
if self.has_to_be_signed(include_draft=True):
|
||||
if self.has_to_be_paid():
|
||||
access_opt['title'] = _('View Quotation') if is_tx_pending else _('Sign & Pay Quotation')
|
||||
else:
|
||||
access_opt['title'] = _('Accept & Sign Quotation')
|
||||
elif self.has_to_be_paid(include_draft=True) and not is_tx_pending:
|
||||
access_opt['title'] = _('Accept & Pay Quotation')
|
||||
elif self.state in ('draft', 'sent'):
|
||||
access_opt['title'] = _('View Quotation')
|
||||
|
||||
# enable followers that have access through portal
|
||||
follower_group = next(group for group in groups if group[0] == 'follower')
|
||||
follower_group[2]['active'] = True
|
||||
follower_group[2]['has_button_access'] = True
|
||||
access_opt = follower_group[2].setdefault('button_access', {})
|
||||
if self.state in ('draft', 'sent'):
|
||||
access_opt['title'] = _('View Quotation')
|
||||
else:
|
||||
access_opt['title'] = _('View Order')
|
||||
access_opt['url'] = self._notify_get_action_link('view', **local_msg_vals)
|
||||
|
||||
return groups
|
||||
|
||||
def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False,
|
||||
force_email_company=False, force_email_lang=False):
|
||||
render_context = super()._notify_by_email_prepare_rendering_context(
|
||||
message, msg_vals, model_description=model_description,
|
||||
force_email_company=force_email_company, force_email_lang=force_email_lang
|
||||
)
|
||||
if self.validity_date:
|
||||
amount_txt = _('%(amount)s due %(date)s',
|
||||
amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')),
|
||||
date=format_date(self.env, self.validity_date, date_format='short', lang_code=render_context.get('lang'))
|
||||
)
|
||||
else:
|
||||
amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang'))
|
||||
render_context['subtitle'] = Markup("<span>%s<br />%s</span>") % (self.name, amount_txt)
|
||||
return render_context
|
||||
|
||||
def preview_sale_order(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
|
||||
@@ -160,6 +160,6 @@ class TestPERF(common.TransactionCase):
|
||||
],
|
||||
} for i in range(self.ENTITIES)]
|
||||
|
||||
# 1592 locally, 1593 in nightly runbot
|
||||
with self.assertQueryCount(admin=1593):
|
||||
# 1592 locally, 1593 in nightly runbot, 1954 sometimes
|
||||
with self.assertQueryCount(admin=1594):
|
||||
self.env["sale.order"].create(vals_list)
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Followers(models.Model):
|
||||
_inherit = ['mail.followers']
|
||||
|
||||
def _get_recipient_data(self, records, message_type, subtype_id, pids=None):
|
||||
if message_type == 'sms':
|
||||
if pids is None:
|
||||
sms_pids = records._sms_get_default_partners().ids
|
||||
else:
|
||||
sms_pids = pids
|
||||
res = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
new_res = []
|
||||
for pid, active, pshare, notif, groups in res:
|
||||
if pid and pid in sms_pids:
|
||||
notif = 'sms'
|
||||
new_res.append((pid, active, pshare, notif, groups))
|
||||
return new_res
|
||||
else:
|
||||
if message_type != 'sms' or not (pids or records):
|
||||
return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
|
||||
if pids is None and records:
|
||||
records_pids = dict(
|
||||
(record.id, record._sms_get_default_partners().ids)
|
||||
for record in records
|
||||
)
|
||||
elif pids and records:
|
||||
records_pids = dict((record.id, pids) for record in records)
|
||||
else:
|
||||
records_pids = {0: pids if pids else []}
|
||||
recipients_data = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
for rid, rdata in recipients_data.items():
|
||||
sms_pids = records_pids.get(rid) or []
|
||||
for pid, pdata in rdata.items():
|
||||
if pid in sms_pids:
|
||||
pdata['notif'] = 'sms'
|
||||
return recipients_data
|
||||
|
||||
@@ -204,8 +204,8 @@ class MailThread(models.AbstractModel):
|
||||
:param partner_ids: if set is a record set of partners to notify;
|
||||
:param number_field: if set is a name of field to use on current record
|
||||
to compute a number to notify;
|
||||
:param sms_numbers: see ``_notify_record_by_sms``;
|
||||
:param sms_pid_to_number: see ``_notify_record_by_sms``;
|
||||
:param sms_numbers: see ``_notify_thread_by_sms``;
|
||||
:param sms_pid_to_number: see ``_notify_thread_by_sms``;
|
||||
"""
|
||||
self.ensure_one()
|
||||
sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
|
||||
@@ -237,17 +237,29 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||
recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_record_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_thread_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
return recipients_data
|
||||
|
||||
def _notify_record_by_sms(self, message, recipients_data, msg_vals=False,
|
||||
def _notify_thread_by_sms(self, message, recipients_data, msg_vals=False,
|
||||
sms_numbers=None, sms_pid_to_number=None,
|
||||
check_existing=False, put_in_queue=False, **kwargs):
|
||||
""" Notification method: by SMS.
|
||||
|
||||
:param message: mail.message record to notify;
|
||||
:param recipients_data: see ``_notify_thread``;
|
||||
:param msg_vals: see ``_notify_thread``;
|
||||
:param message: ``mail.message`` record to notify;
|
||||
:param recipients_data: list of recipients information (based on res.partner
|
||||
records), formatted like
|
||||
[{'active': partner.active;
|
||||
'id': id of the res.partner being recipient to notify;
|
||||
'groups': res.group IDs if linked to a user;
|
||||
'notif': 'inbox', 'email', 'sms' (SMS App);
|
||||
'share': partner.partner_share;
|
||||
'type': 'customer', 'portal', 'user;'
|
||||
}, {...}].
|
||||
See ``MailThread._notify_get_recipients``;
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
|
||||
:param sms_numbers: additional numbers to notify in addition to partners
|
||||
and classic recipients;
|
||||
@@ -264,7 +276,7 @@ class MailThread(models.AbstractModel):
|
||||
sms_all = self.env['sms.sms'].sudo()
|
||||
|
||||
# pre-compute SMS data
|
||||
body = msg_vals['body'] if msg_vals and msg_vals.get('body') else message.body
|
||||
body = msg_vals['body'] if msg_vals and 'body' in msg_vals else message.body
|
||||
sms_base_vals = {
|
||||
'body': html2plaintext(body),
|
||||
'mail_message_id': message.id,
|
||||
|
||||
@@ -90,15 +90,16 @@ class SMSResend(models.TransientModel):
|
||||
pids = list(sms_pid_to_number.keys())
|
||||
numbers = [r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id]
|
||||
|
||||
rdata = []
|
||||
for pid, active, pshare, notif, groups in self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids):
|
||||
if pid and notif == 'sms':
|
||||
rdata.append({'id': pid, 'share': pshare, 'active': active, 'notif': notif, 'groups': groups or [], 'type': 'customer' if pshare else 'user'})
|
||||
if rdata or numbers:
|
||||
record._notify_record_by_sms(
|
||||
self.mail_message_id, rdata, check_existing=True,
|
||||
recipients_data = []
|
||||
all_recipients_data = self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids)[record.id]
|
||||
for pid, pdata in all_recipients_data.items():
|
||||
if pid and pdata['notif'] == 'sms':
|
||||
recipients_data.append(pdata)
|
||||
if recipients_data or numbers:
|
||||
record._notify_thread_by_sms(
|
||||
self.mail_message_id, recipients_data,
|
||||
sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number,
|
||||
put_in_queue=False
|
||||
check_existing=True, put_in_queue=False
|
||||
)
|
||||
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class MailPerformanceThread(models.Model):
|
||||
@@ -66,6 +66,19 @@ class MailTestLang(models.Model):
|
||||
customer_id = fields.Many2one('res.partner')
|
||||
lang = fields.Char('Lang')
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
groups = super(MailTestLang, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
for group in [g for g in groups if g[0] in('follower', 'customer')]:
|
||||
group_options = group[2]
|
||||
group_options['has_button_access'] = True
|
||||
group_options['actions'] = [
|
||||
{'url': self._notify_get_action_link('controller', controller='/test_mail/do_stuff', **local_msg_vals),
|
||||
'title': _('TestStuff')}
|
||||
]
|
||||
return groups
|
||||
|
||||
|
||||
class MailTestTrackCompute(models.Model):
|
||||
_name = 'mail.test.track.compute'
|
||||
|
||||
@@ -41,11 +41,6 @@ class MailTestStandard(models.Model):
|
||||
container_id = fields.Many2one('mail.test.container', tracking=True)
|
||||
company_id = fields.Many2one('res.company')
|
||||
|
||||
def _get_share_url(self, redirect, signup_partner, share_token):
|
||||
"""This function is required for a test on 'mail.mail_notification_paynow' template (test_message_post/test_mail_add_signature),
|
||||
another model should be created in master"""
|
||||
return '/mail/view'
|
||||
|
||||
|
||||
class MailTestActivity(models.Model):
|
||||
""" This model can be used to test activities in addition to simple chatter
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
class TestInvite(TestMailCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
@@ -69,19 +67,6 @@ class TestMailComposer(TestMailCommon, TestRecipients):
|
||||
'auto_delete': True,
|
||||
})
|
||||
|
||||
def _generate_attachments_data(self, count, res_model=None, res_id=None):
|
||||
# attachment visibility depends on what they are attached to
|
||||
if res_model is None:
|
||||
res_model = self.template._name
|
||||
if res_id is None:
|
||||
res_id = self.template.id
|
||||
return [{
|
||||
'name': '%02d.txt' % x,
|
||||
'datas': base64.b64encode(b'Att%02d' % x),
|
||||
'res_model': res_model,
|
||||
'res_id': res_id,
|
||||
} for x in range(count)]
|
||||
|
||||
def _get_web_context(self, records, add_web=True, **values):
|
||||
""" Helper to generate composer context. Will make tests a bit less
|
||||
verbose.
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import users
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_followers')
|
||||
@@ -417,62 +414,264 @@ class AdvancedResponsibleNotifiedTest(TestMailCommon):
|
||||
self.assertEqual(mail_notification.mail_mail_id.state, 'outgoing')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class DuplicateNotificationTest(TestMailCommon):
|
||||
def test_no_duplicate_notification(self):
|
||||
"""
|
||||
Check that we only create one mail.notification per partner
|
||||
@tagged('mail_followers', 'post_install', '-at_install')
|
||||
class RecipientsNotificationTest(TestMailCommon):
|
||||
""" Test advanced and complex recipients computation / notification, such
|
||||
as multiple users, batch computation, ... Post install because we need the
|
||||
registry to be ready to send notifications."""
|
||||
|
||||
Post install because we need the registery to be ready to send notification
|
||||
"""
|
||||
#Simulate case of 2 users that got their partner merged
|
||||
common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
|
||||
user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'email'})
|
||||
user_2 = self.env['res.users'].create({'login': 'demo2', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(RecipientsNotificationTest, cls).setUpClass()
|
||||
|
||||
#Trigger auto subscribe notification
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_2.id})
|
||||
# portal user for testing share status / internal subtypes
|
||||
cls.user_portal = cls._create_portal_user()
|
||||
cls.partner_portal = cls.user_portal.partner_id
|
||||
|
||||
# simple customer
|
||||
cls.customer = cls.env['res.partner'].create({
|
||||
'email': 'customer@test.customer.com',
|
||||
'name': 'Customer',
|
||||
'phone': '+32455778899',
|
||||
})
|
||||
|
||||
# Simulate case of 2 users that got their partner merged
|
||||
cls.common_partner = cls.env['res.partner'].create({
|
||||
'email': 'common.partner@test.customer.com',
|
||||
'name': 'Common Partner',
|
||||
'phone': '+32455998877',
|
||||
})
|
||||
cls.user_1, cls.user_2 = cls.env['res.users'].with_context(no_reset_password=True).create([
|
||||
{'groups_id': [(4, cls.env.ref('base.group_portal').id)],
|
||||
'login': '_login_portal',
|
||||
'notification_type': 'email',
|
||||
'partner_id': cls.common_partner.id,
|
||||
},
|
||||
{'groups_id': [(4, cls.env.ref('base.group_user').id)],
|
||||
'login': '_login_internal',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': cls.common_partner.id,
|
||||
}
|
||||
])
|
||||
(cls.user_1 + cls.user_2).flush()
|
||||
|
||||
def assertRecipientsData(self, recipients_data, records, partners, partner_to_users=None):
|
||||
""" Custom assert as recipients structure is custom and may change due
|
||||
to some implementation choice. """
|
||||
self.assertEqual(set(recipients_data.keys()), set(records.ids))
|
||||
for record in records:
|
||||
record_data = recipients_data[record.id]
|
||||
self.assertEqual(set(record_data.keys()), set(partners.ids))
|
||||
for partner in partners:
|
||||
partner_data = record_data[partner.id]
|
||||
if partner_to_users and partner_to_users.get(partner.id): #helps making test explicit
|
||||
user = partner_to_users[partner.id]
|
||||
else:
|
||||
user = next((user for user in partner.user_ids if not user.share), self.env['res.users'])
|
||||
if not user:
|
||||
user = next((user for user in partner.user_ids), self.env['res.users'])
|
||||
self.assertEqual(partner_data['active'], partner.active)
|
||||
if user:
|
||||
self.assertEqual(partner_data['groups'], set(user.groups_id.ids))
|
||||
self.assertEqual(partner_data['notif'], user.notification_type)
|
||||
self.assertEqual(partner_data['uid'], user.id)
|
||||
else:
|
||||
self.assertEqual(partner_data['groups'], set())
|
||||
self.assertEqual(partner_data['notif'], 'email')
|
||||
self.assertFalse(partner_data['uid'])
|
||||
self.assertEqual(partner_data['is_follower'], partner in record.message_partner_ids)
|
||||
self.assertEqual(partner_data['share'], partner.partner_share)
|
||||
self.assertEqual(partner_data['ushare'], user.share)
|
||||
|
||||
@users('employee')
|
||||
def test_notification_nodupe(self):
|
||||
""" Check that we only create one mail.notification per partner. """
|
||||
# Trigger auto subscribe notification
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_2.id})
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
])
|
||||
notif = self.env['mail.notification'].search([
|
||||
('mail_message_id', '=', mail_message.id),
|
||||
('res_partner_id', '=', common_partner.id)
|
||||
('res_partner_id', '=', self.common_partner.id)
|
||||
])
|
||||
self.assertEqual(len(notif), 1)
|
||||
self.assertEqual(notif.notification_type, 'email')
|
||||
self.assertEqual(notif.notification_type, 'inbox', 'Multi users should take internal users if possible')
|
||||
|
||||
subtype = self.env.ref('mail.mt_comment')
|
||||
res = self.env['mail.followers']._get_recipient_data(test, 'comment', subtype.id, pids=common_partner.ids)
|
||||
partner_notif = [r for r in res if r[0] == common_partner.id]
|
||||
self.assertEqual(len(partner_notif), 1)
|
||||
self.assertEqual(partner_notif[0][3], 'email')
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=self.common_partner.ids)
|
||||
self.assertRecipientsData(recipients_data, test, self.common_partner + self.partner_employee,
|
||||
partner_to_users={self.common_partner.id: self.user_2})
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class UnlinkedNotificationTest(TestMailCommon):
|
||||
def test_unlinked_notification(self):
|
||||
"""
|
||||
Check that we unlink the created user_notification after unlinked the related document
|
||||
|
||||
Post install because we need the registery to be ready to send notification
|
||||
"""
|
||||
common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
|
||||
user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
|
||||
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_1.id})
|
||||
test_id = test.id
|
||||
@users('employee')
|
||||
def test_notification_unlink(self):
|
||||
""" Check that we unlink the created user_notification after unlinked the
|
||||
related document. """
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_1.id})
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('res_id', '=', test_id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
])
|
||||
self.assertEqual(len(mail_message), 1)
|
||||
test.unlink()
|
||||
mail_message = self.env['mail.message'].search([
|
||||
('res_id', '=', test_id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
self.assertEqual(
|
||||
self.env['mail.message'].search_count([
|
||||
('res_id', '=', test.id),
|
||||
('model', '=', 'mail.test.track'),
|
||||
('message_type', '=', 'user_notification')
|
||||
]), 0
|
||||
)
|
||||
|
||||
@users('employee')
|
||||
def test_notification_user_choice(self):
|
||||
""" Check fetching user information when notifying someone with multiple
|
||||
users (more complex use case). """
|
||||
company_other = self.env['res.company'].sudo().create({
|
||||
'currency_id': self.env.ref('base.CAD').id,
|
||||
'email': 'company_other@test.example.com',
|
||||
'name': 'Company Other',
|
||||
})
|
||||
shared_partner = self.env['res.partner'].sudo().create({
|
||||
'email': 'common.partner@test.customer.com',
|
||||
'name': 'Common Partner',
|
||||
'phone': '+32455998877',
|
||||
})
|
||||
cids = (company_other + self.company_admin).ids
|
||||
user_2_1, user_2_2, user_2_3 = self.env['res.users'].sudo().with_context(no_reset_password=True).create([
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': self.company_admin.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_portal').id)],
|
||||
'login': '_login2_portal',
|
||||
'notification_type': 'email',
|
||||
'partner_id': shared_partner.id,
|
||||
},
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': self.company_admin.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_user').id)],
|
||||
'login': '_login2_internal',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': shared_partner.id,
|
||||
},
|
||||
{'company_ids': [(6, 0, cids)],
|
||||
'company_id': company_other.id,
|
||||
'groups_id': [(4, self.env.ref('base.group_user').id), (4, self.env.ref('base.group_partner_manager').id)],
|
||||
'login': '_login2_manager',
|
||||
'notification_type': 'inbox',
|
||||
'partner_id': shared_partner.id,
|
||||
}
|
||||
])
|
||||
self.assertEqual(len(mail_message), 0)
|
||||
(user_2_1 + user_2_2 + user_2_3).flush()
|
||||
|
||||
# just ensure current share status
|
||||
self.assertFalse(shared_partner.partner_share)
|
||||
self.assertTrue(user_2_1.share)
|
||||
self.assertFalse(user_2_2.share or user_2_3.share)
|
||||
|
||||
test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": False})
|
||||
self.assertEqual(test.message_partner_ids, self.partner_employee)
|
||||
|
||||
with self.assertSinglePostNotifications(
|
||||
[{'group': 'customer', 'partner': shared_partner,
|
||||
'status': 'sent', 'type': 'inbox'}],
|
||||
message_info={'content': 'User Choice Notification'}):
|
||||
test.message_post(
|
||||
body='<p>User Choice Notification</p>',
|
||||
message_type='comment',
|
||||
partner_ids=shared_partner.ids,
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=shared_partner.ids)
|
||||
self.assertRecipientsData(recipients_data, test, self.partner_employee + shared_partner,
|
||||
partner_to_users={shared_partner.id: user_2_2})
|
||||
|
||||
@users('employee')
|
||||
def test_recipients_fetch(self):
|
||||
test_records = self.env['mail.test.simple'].create([
|
||||
{'email_from': 'ignasse@example.com',
|
||||
'name': 'Test %s' % idx,
|
||||
} for idx in range(5)
|
||||
])
|
||||
# make followers listen to notes to use it and check portal will never be notified of it (internal)
|
||||
test_records.message_follower_ids.sudo().write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
|
||||
for test_record in test_records:
|
||||
self.assertEqual(test_record.message_partner_ids, self.env.user.partner_id)
|
||||
|
||||
test_records[0].message_subscribe(self.partner_portal.ids)
|
||||
self.assertNotIn(
|
||||
self.env.ref('mail.mt_note'),
|
||||
test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).subtype_ids,
|
||||
'Portal user should not follow notes by default')
|
||||
|
||||
# just fetch followers
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=None
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.partner_portal)
|
||||
|
||||
# followers + additional recipients
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=(self.customer + self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0],
|
||||
self.env.user.partner_id + self.partner_portal + self.customer + self.common_partner + self.partner_admin)
|
||||
|
||||
# ensure filtering on internal: should exclude Portal even if misconfiguration
|
||||
follower_portal = test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).sudo()
|
||||
follower_portal.write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
|
||||
follower_portal.flush()
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_note').id,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
|
||||
# ensure filtering on subtype: should exclude Portal as it does not follow comment anymore
|
||||
follower_portal.write({'subtype_ids': [(3, self.env.ref('mail.mt_comment').id)]})
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
|
||||
# check without subtype
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records[0], 'comment', False,
|
||||
pids=(self.common_partner + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records[0], self.common_partner + self.partner_admin)
|
||||
|
||||
# multi mode
|
||||
test_records[1].message_subscribe(self.partner_portal.ids)
|
||||
test_records[0:4].message_subscribe(self.common_partner.ids)
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records, 'comment', self.env.ref('mail.mt_comment').id,
|
||||
pids=self.partner_admin.ids
|
||||
)
|
||||
# 0: portal is follower but does not follow comment + common partner (+ admin as pid)
|
||||
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[0:1].ids)
|
||||
self.assertRecipientsData(recipients_data_1, test_records[0:1], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
# 1: portal is follower with comment + common partner (+ admin as pid)
|
||||
recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[1:2].ids)
|
||||
self.assertRecipientsData(recipients_data_1, test_records[1:2], self.env.user.partner_id + self.common_partner + self.partner_portal + self.partner_admin)
|
||||
# 2-3: common partner (+ admin as pid)
|
||||
recipients_data_2 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[2:4].ids)
|
||||
self.assertRecipientsData(recipients_data_2, test_records[2:4], self.env.user.partner_id + self.common_partner + self.partner_admin)
|
||||
# 4+: env user partner (+ admin as pid)
|
||||
recipients_data_3 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[4:].ids)
|
||||
self.assertRecipientsData(recipients_data_3, test_records[4:], self.env.user.partner_id + self.partner_admin)
|
||||
|
||||
# multi mode, pids only
|
||||
recipients_data = self.env['mail.followers']._get_recipient_data(
|
||||
test_records, 'comment', False,
|
||||
pids=(self.env.user.partner_id + self.partner_admin).ids
|
||||
)
|
||||
self.assertRecipientsData(recipients_data, test_records, self.env.user.partner_id + self.partner_admin)
|
||||
|
||||
@@ -8,7 +8,7 @@ from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('mail_template')
|
||||
@tagged('mail_template', 'multi_lang')
|
||||
class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
@@ -39,81 +39,22 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
cls.email_2 = 'test2@example.com'
|
||||
cls.email_3 = cls.partner_1.email
|
||||
|
||||
# activate translations
|
||||
cls.env['res.lang']._activate_lang('es_ES')
|
||||
cls.env.ref('base.module_base')._update_translations(['es_ES'])
|
||||
|
||||
# create a complete test template
|
||||
cls.test_template = cls._create_template('mail.test.lang', {
|
||||
'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])],
|
||||
'body_html': '<p>English Body for <t t-out="object.name"/></p>',
|
||||
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
|
||||
'lang': '{{ object.customer_id.lang or object.lang }}',
|
||||
'email_to': '%s, %s' % (cls.email_1, cls.email_2),
|
||||
'email_cc': '%s' % cls.email_3,
|
||||
'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id),
|
||||
'subject': 'English for {{ object.name }}',
|
||||
'subject': 'EnglishSubject for {{ object.name }}',
|
||||
})
|
||||
|
||||
# Make sure Spanish translations have not been altered
|
||||
description_translations = cls.env['ir.translation'].search([
|
||||
('module', '=', 'test_mail'),
|
||||
('src', '=', cls.test_record._description),
|
||||
('lang', '=', 'es_ES')
|
||||
])
|
||||
if description_translations:
|
||||
description_translations.update({'value': 'Spanish description'})
|
||||
else:
|
||||
description_translations.create({
|
||||
'type': 'model',
|
||||
'name': 'ir.model,name',
|
||||
'module': 'test_mail',
|
||||
'lang': 'es_ES',
|
||||
'res_id': cls.env['ir.model']._get_id('mail.test.lang'),
|
||||
'src': cls.test_record._description,
|
||||
'value': 'Spanish description',
|
||||
'state': 'translated',
|
||||
})
|
||||
|
||||
cls.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'mail.template,subject',
|
||||
'module': 'mail',
|
||||
'lang': 'es_ES',
|
||||
'res_id': cls.test_template.id,
|
||||
'value': 'Spanish for {{ object.name }}',
|
||||
'state': 'translated',
|
||||
})
|
||||
cls.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'mail.template,body_html',
|
||||
'module': 'mail',
|
||||
'lang': 'es_ES',
|
||||
'res_id': cls.test_template.id,
|
||||
'value': '<p>Spanish Body for <t t-out="object.name" /></p>',
|
||||
'state': 'translated',
|
||||
})
|
||||
view = cls.env['ir.ui.view'].create({
|
||||
'name': 'test_layout',
|
||||
'key': 'test_layout',
|
||||
'type': 'qweb',
|
||||
'arch_db': '<body><t t-out="message.body"/> English Layout <t t-esc="model_description"/></body>'
|
||||
})
|
||||
cls.env['ir.model.data'].create({
|
||||
'name': 'test_layout',
|
||||
'module': 'test_mail',
|
||||
'model': 'ir.ui.view',
|
||||
'res_id': view.id
|
||||
})
|
||||
cls.env['ir.translation'].create({
|
||||
'type': 'model_terms',
|
||||
'name': 'ir.ui.view,arch_db',
|
||||
'module': 'test_mail',
|
||||
'lang': 'es_ES',
|
||||
'res_id': view.id,
|
||||
'src': 'English Layout',
|
||||
'value': 'Spanish Layout',
|
||||
'state': 'translated',
|
||||
})
|
||||
# activate translations
|
||||
cls._activate_multi_lang(
|
||||
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
|
||||
test_record=cls.test_record, test_template=cls.test_template
|
||||
)
|
||||
|
||||
# admin should receive emails
|
||||
cls.user_admin.write({'notification_type': 'email'})
|
||||
@@ -127,7 +68,7 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
self.assertEqual(mail.email_cc, self.test_template.email_cc)
|
||||
self.assertEqual(mail.email_to, self.test_template.email_to)
|
||||
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
|
||||
self.assertEqual(mail.subject, 'English for %s' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'EnglishSubject for %s' % self.test_record.name)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_translation_lang(self):
|
||||
@@ -137,11 +78,11 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='test_mail.test_layout')
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(mail.body_html,
|
||||
'<body><p>Spanish Body for %s</p> Spanish Layout Spanish description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'Spanish for %s' % self.test_record.name)
|
||||
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_template_translation_partner_lang(self):
|
||||
@@ -156,11 +97,11 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
|
||||
})
|
||||
test_template = self.env['mail.template'].browse(self.test_template.ids)
|
||||
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='test_mail.test_layout')
|
||||
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
|
||||
mail = self.env['mail.mail'].sudo().browse(mail_id)
|
||||
self.assertEqual(mail.body_html,
|
||||
'<body><p>Spanish Body for %s</p> Spanish Layout Spanish description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'Spanish for %s' % self.test_record.name)
|
||||
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish description</body>' % self.test_record.name)
|
||||
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
|
||||
|
||||
def test_template_add_context_action(self):
|
||||
self.test_template.create_action()
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestMailTemplateTools(TestMailTemplate):
|
||||
'resource_ref': test_record,
|
||||
'lang': 'es_ES',
|
||||
})
|
||||
self.assertEqual(preview.body_html, '<p>Spanish Body for %s</p>' % test_record.name)
|
||||
self.assertEqual(preview.body_html, '<p>SpanishBody for %s</p>' % test_record.name)
|
||||
|
||||
preview.write({'lang': 'en_US'})
|
||||
self.assertEqual(preview.body_html, '<p>English Body for %s</p>' % test_record.name)
|
||||
self.assertEqual(preview.body_html, '<p>EnglishBody for %s</p>' % test_record.name)
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestMailResend(TestMailCommon):
|
||||
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
|
||||
|
||||
#Two users
|
||||
cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_public', name='Employee 1', notification_type='email', email='e1') # invalid email
|
||||
cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_user', name='Employee 1', notification_type='email', email='e1') # invalid email
|
||||
cls.user2 = mail_new_test_user(cls.env, login='e2', groups='base.group_portal', name='Employee 2', notification_type='email', email='e2@example.com')
|
||||
#Two partner
|
||||
cls.partner1 = cls.env['res.partner'].with_context(cls._test_context).create({
|
||||
|
||||
@@ -53,9 +53,10 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
for email_xmlid in ['mail.message_notification_email',
|
||||
'mail.mail_notification_light',
|
||||
'mail.mail_notification_paynow']:
|
||||
test_message.sudo().notification_ids.unlink() # otherwise partner/message constraint fails
|
||||
test_message.write({'email_layout_xmlid': email_xmlid})
|
||||
with self.mock_mail_gateway():
|
||||
test_record._notify_record_by_email(
|
||||
test_record._notify_thread_by_email(
|
||||
test_message,
|
||||
recipients_data,
|
||||
force_send=False
|
||||
@@ -71,7 +72,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
self.assertTrue(user_email)
|
||||
|
||||
@users('employee')
|
||||
def test_notify_mail_add_signature(self):
|
||||
def test_notify_by_mail_add_signature(self):
|
||||
self.test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({
|
||||
'name': 'Test',
|
||||
'email_from': 'ignasse@example.com'
|
||||
@@ -84,19 +85,31 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
self.assertIn("record.user_id.sudo().signature", template.arch)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=True, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow")
|
||||
self.test_track.message_post(
|
||||
body="Test body",
|
||||
email_add_signature=True,
|
||||
email_layout_xmlid="mail.mail_notification_paynow",
|
||||
mail_auto_delete=False,
|
||||
partner_ids=[self.partner_1.id, self.partner_2.id],
|
||||
)
|
||||
found_mail = self._new_mails
|
||||
self.assertIn(signature, found_mail.body_html)
|
||||
self.assertEqual(found_mail.body_html.count(signature), 1)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=False, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow")
|
||||
self.test_track.message_post(
|
||||
body="Test body",
|
||||
email_add_signature=False,
|
||||
email_layout_xmlid="mail.mail_notification_paynow",
|
||||
mail_auto_delete=False,
|
||||
partner_ids=[self.partner_1.id, self.partner_2.id]
|
||||
)
|
||||
found_mail = self._new_mails
|
||||
self.assertNotIn(signature, found_mail.body_html)
|
||||
self.assertEqual(found_mail.body_html.count(signature), 0)
|
||||
|
||||
@users('employee')
|
||||
def test_notify_prepare_template_context_company_value(self):
|
||||
def test_notify_by_email_prepare_rendering_contextt(self):
|
||||
""" Verify that the template context company value is right
|
||||
after switching the env company or if a company_id is set
|
||||
on mail record.
|
||||
@@ -113,7 +126,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
# self.env.company.id = Main Company AND test_record.company_id = False
|
||||
self.assertEqual(self.env.company.id, main_company.id)
|
||||
self.assertEqual(test_record.company_id.id, False)
|
||||
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
|
||||
template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
|
||||
self.assertEqual(template_values.get('company').id, self.env.company.id)
|
||||
|
||||
# self.env.company.id = Other Company AND test_record.company_id = False
|
||||
@@ -121,7 +134,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
|
||||
self.assertEqual(self.env.company.id, other_company.id)
|
||||
self.assertEqual(test_record.company_id.id, False)
|
||||
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
|
||||
template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
|
||||
self.assertEqual(template_values.get('company').id, self.env.company.id)
|
||||
|
||||
# self.env.company.id = Other Company AND test_record.company_id = Main Company
|
||||
@@ -129,7 +142,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
|
||||
self.assertEqual(self.env.company.id, other_company.id)
|
||||
self.assertEqual(test_record.company_id.id, main_company.id)
|
||||
template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
|
||||
template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
|
||||
self.assertEqual(template_values.get('company').id, main_company.id)
|
||||
|
||||
def test_notify_recipients_internals(self):
|
||||
@@ -147,7 +160,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
|
||||
'auth_login': 'auth_login_val',
|
||||
}
|
||||
notify_msg_vals = dict(msg_vals, **link_vals)
|
||||
classify_res = self.env[self.test_record._name]._notify_classify_recipients(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
|
||||
classify_res = self.env[self.test_record._name]._notify_get_recipients_classify(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
|
||||
# find back information for each recipients
|
||||
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
|
||||
emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
|
||||
@@ -528,3 +541,133 @@ class TestMessagePostGlobal(TestMailCommon, TestRecipients):
|
||||
{'body': 'test'}
|
||||
)
|
||||
self.assertTrue(isinstance(message_id, int))
|
||||
|
||||
|
||||
@tagged('mail_post', 'multi_lang')
|
||||
class TestMessagePostLang(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMessagePostLang, cls).setUpClass()
|
||||
|
||||
cls.test_records = cls.env['mail.test.lang'].create([
|
||||
{'customer_id': False,
|
||||
'email_from': 'test.record.1@test.customer.com',
|
||||
'lang': 'es_ES',
|
||||
'name': 'TestRecord1',
|
||||
},
|
||||
{'customer_id': cls.partner_2.id,
|
||||
'email_from': 'valid.other@gmail.com',
|
||||
'name': 'TestRecord2',
|
||||
},
|
||||
])
|
||||
|
||||
cls.test_template = cls.env['mail.template'].create({
|
||||
'auto_delete': True,
|
||||
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
|
||||
'email_from': '{{ user.email_formatted }}',
|
||||
'email_to': '{{ (object.email_from if not object.customer_id else "") }}',
|
||||
'lang': '{{ object.customer_id.lang or object.lang }}',
|
||||
'model_id': cls.env['ir.model']._get('mail.test.lang').id,
|
||||
'name': 'TestTemplate',
|
||||
'partner_to': '{{ object.customer_id.id if object.customer_id else "" }}',
|
||||
'subject': 'EnglishSubject for {{ object.name }}',
|
||||
})
|
||||
cls.user_employee.write({ # add group to create contacts, necessary for templates
|
||||
'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
|
||||
})
|
||||
|
||||
cls._activate_multi_company()
|
||||
cls._activate_multi_lang(test_record=cls.test_records[0], test_template=cls.test_template)
|
||||
|
||||
cls.partner_2.write({'lang': 'es_ES'})
|
||||
|
||||
@users('employee')
|
||||
def test_composer_lang_template(self):
|
||||
test_records = self.test_records.with_user(self.env.user)
|
||||
test_template = self.test_template.with_user(self.env.user)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_records.message_post_with_template(
|
||||
test_template.id,
|
||||
composition_mode='mass_mail',
|
||||
# email_layout_xmlid='mail.test_layout', Not supported
|
||||
message_type='comment',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
|
||||
record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
|
||||
self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
|
||||
|
||||
for record, customer in zip(test_records, record0_customer + self.partner_2):
|
||||
customer_email = self._find_sent_mail_wemail(customer.email_formatted)
|
||||
self.assertTrue(customer_email)
|
||||
body = customer_email['body']
|
||||
# check content
|
||||
# self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
|
||||
self.assertIn('EnglishBody for %s' % record.name, body, 'Fixme: this should be translated')
|
||||
# check subject
|
||||
# self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
|
||||
self.assertEqual('EnglishSubject for %s' % record.name, customer_email['subject'], 'Fixme: this should be translated')
|
||||
|
||||
@users('employee')
|
||||
def test_layout_email_lang_context(self):
|
||||
test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES')
|
||||
test_records[1].message_subscribe(self.partner_2.ids)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
test_records[1].message_post(
|
||||
body='<p>Hello</p>',
|
||||
email_layout_xmlid='mail.test_layout',
|
||||
message_type='comment',
|
||||
subject='Subject',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
customer_email = self._find_sent_mail_wemail(self.partner_2.email_formatted)
|
||||
self.assertTrue(customer_email)
|
||||
body = customer_email['body']
|
||||
# check notification layout translation
|
||||
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
|
||||
self.assertNotIn('English Layout for', body)
|
||||
self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
|
||||
self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
|
||||
self.assertNotIn('View %s' % test_records[1]._description, body)
|
||||
self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
|
||||
self.assertNotIn('TestStuff', body)
|
||||
# check content
|
||||
self.assertIn('Hello', body, 'Body of posted message should be present')
|
||||
|
||||
@users('employee')
|
||||
def test_layout_email_lang_template(self):
|
||||
test_records = self.test_records.with_user(self.env.user)
|
||||
test_template = self.test_template.with_user(self.env.user)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
for test_record in test_records:
|
||||
test_record.message_post_with_template(
|
||||
test_template.id,
|
||||
email_layout_xmlid='mail.test_layout',
|
||||
message_type='comment',
|
||||
subtype_id=self.env.ref('mail.mt_comment').id,
|
||||
)
|
||||
|
||||
record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
|
||||
self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
|
||||
|
||||
for record, customer in zip(test_records, record0_customer + self.partner_2):
|
||||
customer_email = self._find_sent_mail_wemail(customer.email_formatted)
|
||||
self.assertTrue(customer_email)
|
||||
body = customer_email['body']
|
||||
# check notification layout translation
|
||||
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
|
||||
self.assertNotIn('English Layout for', body)
|
||||
self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
|
||||
# self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
|
||||
self.assertIn('View %s' % test_records[1]._description, body, 'Fixme: this should be translated')
|
||||
# self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
|
||||
self.assertIn('TestStuff', body, 'Fixme: groups-based action names should be translated')
|
||||
# check content
|
||||
self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
|
||||
# check subject
|
||||
self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
|
||||
|
||||
@@ -349,8 +349,8 @@ class TestTrackingInternals(TestMailCommon):
|
||||
self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
|
||||
self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values")
|
||||
|
||||
msg_emp = self.record._notify_prepare_template_context(self.record.message_ids, {})
|
||||
msg_sudo = self.record.sudo()._notify_prepare_template_context(self.record.message_ids, {})
|
||||
msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
|
||||
msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
|
||||
self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
|
||||
self.assertTrue(msg_sudo.get('tracking_values'), "should have protected tracking values")
|
||||
|
||||
|
||||
@@ -587,7 +587,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
record = self.container.with_user(self.env.user)
|
||||
template_id = self.env.ref('test_mail.mail_test_container_tpl').id
|
||||
|
||||
with self.assertQueryCount(__system__=72, employee=73):
|
||||
with self.assertQueryCount(__system__=72, employee=73): # about 20 queries per additional customer group
|
||||
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
|
||||
|
||||
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
|
||||
@@ -1018,7 +1018,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance):
|
||||
parent_id=False,
|
||||
attachments=attachements,
|
||||
attachment_ids=attachement_ids,
|
||||
add_sign=True,
|
||||
email_add_signature=True,
|
||||
model_description=False,
|
||||
mail_auto_delete=True
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestSMSPost(TestMailFullCommon, TestMailFullRecipients):
|
||||
|
||||
with self.with_user('employee'), self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
test_record._notify_record_by_sms(messages, [{'id': self.partner_1.id, 'notif': 'sms'}], check_existing=True)
|
||||
test_record._notify_thread_by_sms(messages, [{'id': self.partner_1.id, 'notif': 'sms'}], check_existing=True)
|
||||
self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages)
|
||||
|
||||
def test_message_sms_internals_sms_numbers(self):
|
||||
|
||||
@@ -258,13 +258,13 @@ class BlogPost(models.Model):
|
||||
default = dict(default or {}, name=name)
|
||||
return super(BlogPost, self).copy_data(default)
|
||||
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the post on website
|
||||
directly if user is an employee or if the post is published. """
|
||||
self.ensure_one()
|
||||
user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
|
||||
if user.share and not self.sudo().website_published:
|
||||
return super(BlogPost, self).get_access_action(access_uid)
|
||||
user = self.env['res.users'].sudo().browse(access_uid) if access_uid else self.env.user
|
||||
if not force_website and user.share and not self.sudo().website_published:
|
||||
return super(BlogPost, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.website_url,
|
||||
@@ -273,23 +273,25 @@ class BlogPost(models.Model):
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Add access button to everyone if the document is published. """
|
||||
groups = super(BlogPost, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(BlogPost, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
|
||||
if self.website_published:
|
||||
for group_name, group_method, group_data in groups:
|
||||
for _group_name, _group_method, group_data in groups:
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
|
||||
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if msg_vals is None:
|
||||
msg_vals = {}
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(BlogPost, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
return super(BlogPost, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
def _default_website_meta(self):
|
||||
res = super(BlogPost, self)._default_website_meta()
|
||||
|
||||
@@ -283,9 +283,9 @@ class CrmLead(models.Model):
|
||||
# DO NOT FORWARD PORT IN MASTER
|
||||
# instead, crm.lead should implement portal.mixin
|
||||
#
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the online document for
|
||||
portal users or if force_website=True in the context. """
|
||||
portal users or if force_website=True. """
|
||||
self.ensure_one()
|
||||
|
||||
user, record = self.env.user, self
|
||||
@@ -294,10 +294,10 @@ class CrmLead(models.Model):
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule("read")
|
||||
except AccessError:
|
||||
return super(CrmLead, self).get_access_action(access_uid)
|
||||
return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
user = self.env['res.users'].sudo().browse(access_uid)
|
||||
if user.share or force_website:
|
||||
record = self.with_user(user)
|
||||
if user.share or self.env.context.get('force_website'):
|
||||
try:
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule('read')
|
||||
@@ -308,4 +308,4 @@ class CrmLead(models.Model):
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/my/opportunity/%s' % record.id,
|
||||
}
|
||||
return super(CrmLead, self).get_access_action(access_uid)
|
||||
return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
|
||||
@@ -155,6 +155,6 @@ class TestPartnerLeadPortal(TestCrmCommon):
|
||||
self.assertEqual(opportunity.partner_assigned_id, self.user_portal.partner_id, 'Assigned Partner of created opportunity is the (portal) creator.')
|
||||
|
||||
def test_portal_mixin_url(self):
|
||||
record_action = self.lead_portal.get_access_action(self.user_portal.id)
|
||||
record_action = self.lead_portal._get_access_action(access_uid=self.user_portal.id)
|
||||
self.assertEqual(record_action['url'], '/my/opportunity/%s' % self.lead_portal.id)
|
||||
self.assertEqual(record_action['type'], 'ir.actions.act_url')
|
||||
|
||||
@@ -911,9 +911,11 @@ class Post(models.Model):
|
||||
self.ensure_one()
|
||||
return sql.increment_field_skiplock(self, 'views')
|
||||
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the post on the website directly """
|
||||
self.ensure_one()
|
||||
if not force_website and not self.state == 'active':
|
||||
return super(Post, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/forum/%s/%s' % (self.forum_id.id, self.id),
|
||||
@@ -922,12 +924,12 @@ class Post(models.Model):
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_recipients_get_groups(self, msg_vals=None):
|
||||
""" Add access button to everyone if the document is active. """
|
||||
groups = super(Post, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(Post, self)._notify_recipients_get_groups(msg_vals=msg_vals)
|
||||
|
||||
if self.state == 'active':
|
||||
for group_name, group_method, group_data in groups:
|
||||
for _group_name, _group_method, group_data in groups:
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
@@ -954,13 +956,15 @@ class Post(models.Model):
|
||||
kwargs['record_name'] = self.parent_id.name
|
||||
return super(Post, self).message_post(message_type=message_type, **kwargs)
|
||||
|
||||
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if msg_vals is None:
|
||||
msg_vals = {}
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(Post, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
return super(Post, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
def _compute_website_url(self):
|
||||
return '/forum/{forum}/{post}{anchor}'.format(
|
||||
|
||||
@@ -8,30 +8,12 @@
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted or '') }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 0px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
<!-- HEADER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr><td valign="middle">
|
||||
<t t-set="company" t-value="object.company_id or object.user_id.company_id or user.company_id"/>
|
||||
<span style="font-size: 10px;">Your Cart</span><br/>
|
||||
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">S00060</span>
|
||||
</td><td valign="middle" align="right">
|
||||
<img t-attf-src="/logo.png?company={{ company.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="company.name"/>
|
||||
</td></tr>
|
||||
<tr><td colspan="2" style="text-align:center;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- CONTENT -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 0px 0px 0px; border-collapse:separate;">
|
||||
<tr><td valign="top" style="font-size: 13px;">
|
||||
<h1 style="color:#A9A9A9;">THERE'S SOMETHING IN YOUR CART.</h1>
|
||||
Would you like to complete your purchase?<br/><br/>
|
||||
@@ -61,42 +43,13 @@
|
||||
Resume order
|
||||
</a>
|
||||
</div>
|
||||
<t t-set="company" t-value="object.company_id or object.user_id.company_id or user.company_id"/>
|
||||
<div style="text-align: center;"><strong>Thank you for shopping with <t t-out="company.name or ''">My Company (San Francisco)</t>!</strong></div>
|
||||
</td></tr>
|
||||
<tr><td style="text-align:center;">
|
||||
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- FOOTER -->
|
||||
<tr>
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||
<tr><td valign="middle" align="left" t-out="company.name or ''">My Company (San Francisco)</td></tr>
|
||||
<tr><td valign="middle" align="left" style="opacity: 0.7;">
|
||||
<t t-out="company.phone or ''">+1 650-123-4567</t>
|
||||
<t t-if="company.email">
|
||||
| <a t-attf-href="'mailto:%s' % {{ company.email }}" style="text-decoration:none; color: #454748;" t-out="company.email or ''">info@yourcompany.com</a>
|
||||
</t>
|
||||
<t t-if="company.website">
|
||||
| <a t-attf-href="'%s' % {{ company.website }}" style="text-decoration:none; color: #454748;" t-out="company.website or ''">http://www.example.com</a>
|
||||
</t>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- POWERED BY -->
|
||||
<tr><td align="center" style="min-width: 590px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
|
||||
<tr><td style="text-align: center; font-size: 13px;">
|
||||
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&utm_medium=website" style="color: #875A7B;">Odoo</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
|
||||
@@ -300,6 +300,7 @@ class SaleOrder(models.Model):
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_composition_mode': 'mass_mail' if len(self.ids) > 1 else 'comment',
|
||||
'default_email_layout_xmlid': 'mail.mail_notification_paynow',
|
||||
'default_res_id': self.ids[0],
|
||||
'default_model': 'sale.order',
|
||||
'default_use_template': bool(template_id),
|
||||
@@ -335,6 +336,19 @@ class SaleOrder(models.Model):
|
||||
sent_orders |= order
|
||||
sent_orders.write({'cart_recovery_email_sent': True})
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
""" In case of cart recovery email, update link to redirect directly
|
||||
to the cart (like ``mail_template_sale_cart_recovery`` template). """
|
||||
groups = super(SaleOrder, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
|
||||
customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
|
||||
if customer_portal_group:
|
||||
access_opt = customer_portal_group[2].setdefault('button_access', {})
|
||||
if self._context.get('website_sale_send_recovery_email'):
|
||||
access_opt['title'] = _('Resume Order')
|
||||
access_opt['url'] = '%s/shop/cart?access_token=%s' % (self.get_base_url(), self.access_token)
|
||||
return groups
|
||||
|
||||
def action_confirm(self):
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
for order in self:
|
||||
|
||||
@@ -26,6 +26,8 @@ class TestWebsiteSaleMail(HttpCase):
|
||||
|
||||
# we override unlink because we don't want the email to be auto deleted
|
||||
MailMail = odoo.addons.mail.models.mail_mail.MailMail
|
||||
# as we check some link content, avoid mobile doing its link management
|
||||
self.env['ir.config_parameter'].sudo().set_param('mail_mobile.disable_redirect_firebase_dynamic_link', True)
|
||||
|
||||
with patch.object(MailMail, 'unlink', lambda self: None):
|
||||
self.start_tour("/", 'shop_mail', login="admin")
|
||||
|
||||
@@ -649,10 +649,10 @@ class Slide(models.Model):
|
||||
raise AccessError(_('Not enough karma to comment'))
|
||||
return super(Slide, self).message_post(message_type=message_type, **kwargs)
|
||||
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to website if it is published. """
|
||||
self.ensure_one()
|
||||
if self.website_published:
|
||||
if force_website or self.website_published:
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '%s' % self.website_url,
|
||||
@@ -660,14 +660,14 @@ class Slide(models.Model):
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
return super(Slide, self).get_access_action(access_uid)
|
||||
return super(Slide, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Add access button to everyone if the document is active. """
|
||||
groups = super(Slide, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
groups = super(Slide, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
|
||||
if self.website_published:
|
||||
for group_name, group_method, group_data in groups:
|
||||
for _group_name, _group_method, group_data in groups:
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
|
||||
@@ -89,7 +89,7 @@ class SlideChannelInvite(models.TransientModel):
|
||||
except ValueError:
|
||||
_logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
|
||||
else:
|
||||
# could be great to use _notify_prepare_template_context someday
|
||||
# could be great to use ``_notify_by_email_prepare_rendering_context`` someday
|
||||
template_ctx = {
|
||||
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.channel_id.name)),
|
||||
'model_description': self.env['ir.model']._get('slide.channel').display_name,
|
||||
|
||||
@@ -14,6 +14,7 @@ import time
|
||||
from collections import defaultdict
|
||||
from hashlib import sha256
|
||||
from itertools import chain, repeat
|
||||
from markupsafe import Markup
|
||||
|
||||
import decorator
|
||||
import pytz
|
||||
@@ -28,7 +29,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
from odoo.service.db import check_super
|
||||
from odoo.tools import partition, collections, frozendict, lazy_property
|
||||
from odoo.tools import is_html_empty, partition, collections, frozendict, lazy_property
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -285,7 +286,7 @@ class Users(models.Model):
|
||||
help="Specify a value only when creating a user or if you're "\
|
||||
"changing the user's password, otherwise leave empty. After "\
|
||||
"a change of password, the user has to login again.")
|
||||
signature = fields.Html(string="Email Signature", default="")
|
||||
signature = fields.Html(string="Email Signature", compute='_compute_signature', readonly=False, store=True)
|
||||
active = fields.Boolean(default=True)
|
||||
active_partner = fields.Boolean(related='partner_id.active', readonly=True, string="Partner is Active")
|
||||
action_id = fields.Many2one('ir.actions.actions', string='Home Action',
|
||||
@@ -401,6 +402,11 @@ class Users(models.Model):
|
||||
else:
|
||||
user.password = user.new_password
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_signature(self):
|
||||
for user in self.filtered(lambda user: user.name and is_html_empty(user.signature)):
|
||||
user.signature = Markup('<p>--<br />%s</p>') % user['name']
|
||||
|
||||
@api.depends('groups_id')
|
||||
def _compute_share(self):
|
||||
user_group_id = self.env['ir.model.data']._xmlid_to_res_id('base.group_user')
|
||||
|
||||
@@ -503,7 +503,7 @@ class TestCustomFields(common.TransactionCase):
|
||||
|
||||
# create a non-computed field, and assert how many queries it takes
|
||||
model_id = self.env['ir.model']._get_id('res.partner')
|
||||
query_count = 44
|
||||
query_count = 42
|
||||
with self.assertQueryCount(query_count):
|
||||
self.env.registry.clear_caches()
|
||||
self.env['ir.model.fields'].create({
|
||||
|
||||
+8
-4
@@ -1767,15 +1767,19 @@ class BaseModel(metaclass=MetaModel):
|
||||
'context': dict(self._context),
|
||||
}
|
||||
|
||||
def get_access_action(self, access_uid=None):
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Return an action to open the document. This method is meant to be
|
||||
overridden in addons that want to give specific access to the document.
|
||||
By default, it opens the formview of the document.
|
||||
|
||||
An optional access_uid holds the user that will access the document
|
||||
that could be different from the current user.
|
||||
:param integer access_uid: optional access_uid being the user that
|
||||
accesses the document. May be different from the current user as we
|
||||
may compute an access for someone else.
|
||||
:param integer force_website: force frontend redirection if available
|
||||
on self. Used in overrides, notably with portal / website addons.
|
||||
"""
|
||||
return self[0].get_formview_action(access_uid=access_uid)
|
||||
self.ensure_one()
|
||||
return self.get_formview_action(access_uid=access_uid)
|
||||
|
||||
@api.model
|
||||
def search_count(self, args):
|
||||
|
||||
Reference in New Issue
Block a user