19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -5,7 +5,8 @@ from . import base_module_uninstall
from . import base_partner_merge_automatic_wizard
from . import mail_blacklist_remove
from . import mail_compose_message
from . import mail_resend_message
from . import mail_activity_schedule
from . import mail_activity_schedule_summary
from . import mail_template_preview
from . import mail_template_reset
from . import mail_wizard_invite
from . import mail_followers_edit

View file

@ -1,12 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _
from odoo import models
class MergePartnerAutomatic(models.TransientModel):
class BasePartnerMergeAutomaticWizard(models.TransientModel):
_inherit = 'base.partner.merge.automatic.wizard'
def _log_merge_operation(self, src_partners, dst_partner):
super(MergePartnerAutomatic, self)._log_merge_operation(src_partners, dst_partner)
dst_partner.message_post(body='%s %s' % (_("Merged with the following partners:"), ", ".join('%s <%s> (ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners)))
super()._log_merge_operation(src_partners, dst_partner)
dst_partner.message_post(
body=self.env._(
"Merged with the following partners: %s",
[
self.env._("%(partner)s <%(email)s> (ID %(id)s)", partner=p.name, email=p.email or "n/a", id=p.id)
for p in src_partners
],
)
)

View file

@ -0,0 +1,467 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.addons.mail.tools.parser import parse_res_ids
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.fields import Domain
from odoo.tools import html2plaintext
from odoo.tools.misc import format_date
_logger = logging.getLogger(__name__)
class MailActivitySchedule(models.TransientModel):
_name = 'mail.activity.schedule'
_description = 'Activity schedule plan Wizard'
_batch_size = 500
@api.model
def default_get(self, fields):
res = super().default_get(fields)
context = self.env.context
active_res_ids = parse_res_ids(context.get('active_ids'), self.env)
if 'res_ids' in fields:
if active_res_ids and len(active_res_ids) <= self._batch_size:
res['res_ids'] = f"{context['active_ids']}"
elif not active_res_ids and context.get('active_id'):
res['res_ids'] = f"{[context['active_id']]}"
res_model = context.get('active_model') or context.get('params', {}).get('active_model', False)
if 'res_model' in fields:
res['res_model'] = res_model
return res
res_model_id = fields.Many2one(
'ir.model', string="Applies to",
compute="_compute_res_model_id", compute_sudo=True,
ondelete="cascade", precompute=True, readonly=False, required=False, store=True)
res_model = fields.Char("Model", readonly=False, required=False)
res_ids = fields.Text(
'Document IDs', compute='_compute_res_ids',
readonly=False, store=True, precompute=True)
is_batch_mode = fields.Boolean('Use in batch', compute='_compute_is_batch_mode')
company_id = fields.Many2one(
'res.company', 'Company',
compute='_compute_company_id', required=False)
# usage
error = fields.Html(compute='_compute_error')
has_error = fields.Boolean(compute='_compute_error')
warning = fields.Html(compute='_compute_error')
has_warning = fields.Boolean(compute='_compute_error')
# plan-based
plan_available_ids = fields.Many2many('mail.activity.plan', compute='_compute_plan_available_ids',
store=True, compute_sudo=True)
plan_id = fields.Many2one('mail.activity.plan', domain="[('id', 'in', plan_available_ids)]",
compute='_compute_plan_id', store=True, readonly=False)
plan_has_user_on_demand = fields.Boolean(related="plan_id.has_user_on_demand")
plan_schedule_line_ids = fields.One2many('mail.activity.schedule.line', 'activity_schedule_id',
string='Schedule Lines', compute='_compute_plan_schedule_line_ids')
plan_on_demand_user_id = fields.Many2one(
'res.users', 'Assigned To',
help='Choose assignation for activities with on demand assignation.',
default=lambda self: self.env.user)
plan_date = fields.Date(
'Plan Date', compute='_compute_plan_date',
store=True, readonly=False)
# activity-based
activity_type_id = fields.Many2one(
'mail.activity.type', string='Activity Type',
compute='_compute_activity_type_id', store=True, readonly=False,
domain="['|', ('res_model', '=', False), ('res_model', '=', res_model)]", ondelete='set null')
activity_category = fields.Selection(related='activity_type_id.category', readonly=True)
date_deadline = fields.Date(
'Due Date', compute="_compute_date_deadline",
readonly=False, store=True)
summary = fields.Char(
'Summary', compute="_compute_summary",
readonly=False, store=True)
note = fields.Html(
'Note', compute="_compute_note",
readonly=False, store=True, sanitize_style=True)
activity_user_id = fields.Many2one(
'res.users', 'Assigned to', compute='_compute_activity_user_id',
readonly=False, store=True)
chaining_type = fields.Selection(related='activity_type_id.chaining_type', readonly=True)
@api.depends('res_model')
def _compute_res_model_id(self):
self.filtered(lambda a: not a.res_model).res_model_id = False
for scheduler in self.filtered('res_model'):
scheduler.res_model_id = self.env['ir.model']._get_id(scheduler.res_model)
@api.depends_context('active_ids')
def _compute_res_ids(self):
context = self.env.context
for scheduler in self.filtered(lambda scheduler: not scheduler.res_ids):
active_res_ids = parse_res_ids(context.get('active_ids'), self.env)
if active_res_ids and len(active_res_ids) <= self._batch_size:
scheduler.res_ids = f"{context['active_ids']}"
elif not active_res_ids and context.get('active_id'):
scheduler.res_ids = f"{[context['active_id']]}"
@api.depends('res_model_id', 'res_ids')
def _compute_company_id(self):
self.filtered(lambda a: not a.res_model).company_id = False
for scheduler in self.filtered('res_model'):
applied_on = scheduler._get_applied_on_records()
scheduler.company_id = (applied_on and 'company_id' in applied_on[0]._fields and applied_on[0].company_id
) or self.env.company
@api.depends('company_id', 'res_model_id', 'res_ids',
'plan_id', 'plan_on_demand_user_id', 'plan_available_ids', # plan specific
'activity_type_id', 'activity_user_id') # activity specific
def _compute_error(self):
for scheduler in self:
errors = set()
warnings = set()
if scheduler.res_model:
applied_on = scheduler._get_applied_on_records()
if applied_on and ('company_id' in scheduler.env[applied_on._name]._fields and
len(applied_on.mapped('company_id')) > 1):
errors.add(_('The records must belong to the same company.'))
if scheduler.plan_id:
errors |= set(scheduler._check_plan_templates_error(applied_on))
warnings |= set(scheduler._check_plan_templates_warning(applied_on))
if not scheduler.res_ids:
errors.add(_("Can't launch a plan without a record."))
if not scheduler.res_ids and not scheduler.activity_user_id:
errors.add(_("Can't schedule activities without either a record or a user."))
if errors:
error_header = (
_('The plan "%(plan_name)s" cannot be launched:', plan_name=scheduler.plan_id.name) if scheduler.plan_id
else _('The activity cannot be launched:')
)
error_body = Markup('<ul>%s</ul>') % (
Markup().join(Markup('<li>%s</li>') % error for error in errors)
)
scheduler.error = f'{error_header}{error_body}'
scheduler.has_error = True
scheduler.has_warning = False
else:
scheduler.error = False
scheduler.has_error = False
if warnings:
warning_header = (
_('The plan "%(plan_name)s" can be launched, with these additional effects:', plan_name=scheduler.plan_id.name) if scheduler.plan_id
else _('The activity can be launched, with these additional effects:')
)
warning_body = Markup('<ul>%s</ul>') % (
Markup().join(Markup('<li>%s</li>') % warning for warning in warnings)
)
scheduler.warning = f'{warning_header}{warning_body}'
scheduler.has_warning = True
else:
scheduler.warning = False
scheduler.has_warning = False
@api.depends('res_ids')
def _compute_is_batch_mode(self):
for scheduler in self:
scheduler.is_batch_mode = len(scheduler._evaluate_res_ids()) > 1
@api.depends('company_id', 'res_model')
def _compute_plan_available_ids(self):
for scheduler in self:
scheduler.plan_available_ids = self.env['mail.activity.plan'].search(scheduler._get_plan_available_base_domain())
@api.depends_context('plan_mode')
@api.depends('plan_available_ids')
def _compute_plan_id(self):
for scheduler in self:
if self.env.context.get('plan_mode'):
scheduler.plan_id = scheduler.env['mail.activity.plan'].search(
[('id', 'in', scheduler.plan_available_ids.ids)], order='id', limit=1)
else:
scheduler.plan_id = False
@api.onchange('plan_id')
def _onchange_plan_id(self):
""" Reset UX """
if self.plan_id:
self.activity_type_id = False
@api.depends('res_model', 'res_ids')
def _compute_plan_date(self):
self.plan_date = fields.Date.context_today(self)
@api.depends('plan_date', 'plan_id', 'plan_on_demand_user_id', 'res_model', 'res_ids')
def _compute_plan_schedule_line_ids(self):
self.plan_schedule_line_ids = False
for scheduler in self:
schedule_line_values_list = []
for template in scheduler.plan_id.template_ids:
schedule_line_values = {
'line_description': template.summary or template.activity_type_id.name,
}
# try to determine responsible user, light re-coding of '_determine_responsible' but
# we don't always have a target record here
responsible_user = False
res_ids = scheduler._evaluate_res_ids()
if template.responsible_id:
responsible_user = template.responsible_id
elif template.responsible_type == 'on_demand':
responsible_user = scheduler.plan_on_demand_user_id
elif scheduler.res_model and res_ids and len(res_ids) == 1:
record = self.env[scheduler.res_model].browse(res_ids)
if record.exists():
responsible_user = template._determine_responsible(
scheduler.plan_on_demand_user_id,
record,
)['responsible']
if responsible_user:
schedule_line_values['responsible_user_id'] = responsible_user.id
activity_date_deadline = False
if scheduler.plan_date:
activity_date_deadline = template._get_date_deadline(scheduler.plan_date)
schedule_line_values['line_date_deadline'] = activity_date_deadline
# append main line before handling next activities
schedule_line_values_list.append(schedule_line_values)
activity_type = template.activity_type_id
if activity_type.triggered_next_type_id:
next_activity = activity_type.triggered_next_type_id
schedule_line_values = {
'line_description': next_activity.summary or next_activity.name,
'responsible_user_id': next_activity.default_user_id.id or False
}
if activity_date_deadline:
schedule_line_values['line_date_deadline'] = next_activity.with_context(
activity_previous_deadline=activity_date_deadline
)._get_date_deadline()
schedule_line_values_list.append(schedule_line_values)
elif activity_type.suggested_next_type_ids:
for suggested in activity_type.suggested_next_type_ids:
schedule_line_values = {
'line_description': suggested.summary or suggested.name,
'responsible_user_id': suggested.default_user_id.id or False,
}
if activity_date_deadline:
schedule_line_values['line_date_deadline'] = suggested.with_context(
activity_previous_deadline=activity_date_deadline
)._get_date_deadline()
schedule_line_values_list.append(schedule_line_values)
scheduler.plan_schedule_line_ids = [(5,)] + [(0, 0, values) for values in schedule_line_values_list]
@api.depends('res_model')
def _compute_activity_type_id(self):
for scheduler in self:
if not scheduler.activity_type_id or (
scheduler.activity_type_id.res_model and scheduler.res_model and scheduler.activity_type_id.res_model != scheduler.res_model
):
scheduler.activity_type_id = scheduler.env['mail.activity']._default_activity_type_for_model(scheduler.res_model)
@api.onchange('activity_type_id')
def _onchange_activity_type_id(self):
""" Reset UX """
if self.activity_type_id:
self.plan_id = False
@api.depends('activity_type_id')
def _compute_date_deadline(self):
for scheduler in self:
if scheduler.activity_type_id:
scheduler.date_deadline = scheduler.activity_type_id._get_date_deadline()
elif not scheduler.date_deadline:
scheduler.date_deadline = fields.Date.context_today(scheduler)
@api.depends('activity_type_id')
def _compute_summary(self):
for scheduler in self:
scheduler.summary = scheduler.activity_type_id.summary
@api.depends('activity_type_id')
def _compute_note(self):
for scheduler in self:
scheduler.note = scheduler.activity_type_id.default_note
@api.depends('activity_type_id', 'res_model')
def _compute_activity_user_id(self):
for scheduler in self:
if scheduler.activity_type_id.default_user_id:
scheduler.activity_user_id = scheduler.activity_type_id.default_user_id
else:
scheduler.activity_user_id = self.env.user
# Any writable fields that can change error computed field
@api.constrains('res_model_id', 'res_ids', # records (-> responsible)
'plan_id', 'plan_on_demand_user_id', # plan specific
'activity_type_id', 'activity_user_id') # activity specific
def _check_consistency(self):
for scheduler in self.filtered('error'):
raise ValidationError(html2plaintext(scheduler.error))
@api.constrains('res_ids')
def _check_res_ids(self):
""" Check res_ids is a valid list of integers (or Falsy). """
for scheduler in self:
scheduler._evaluate_res_ids()
@api.readonly
@api.model
def get_model_options(self):
""" Return a list of valid models for a user to define an activity on. """
functional_models = [
model.model
for model in self.env['ir.model'].sudo().search(
['&', ('is_mail_activity', '=', True), ('transient', '=', False)]
)
if model.has_access('read')
]
return functional_models
# ------------------------------------------------------------
# PLAN-BASED SCHEDULING API
# ------------------------------------------------------------
def action_schedule_plan(self):
if not self.res_model:
raise ValueError(_('Plan-based scheduling are available only on documents.'))
applied_on = self._get_applied_on_records()
for record in applied_on:
body = _('The plan "%(plan_name)s" has been started', plan_name=self.plan_id.name)
activity_descriptions = []
for template in self._plan_filter_activity_templates_to_schedule():
if template.responsible_type == 'on_demand':
responsible = self.plan_on_demand_user_id
else:
responsible = template._determine_responsible(self.plan_on_demand_user_id, record)['responsible']
date_deadline = template._get_date_deadline(self.plan_date)
record.activity_schedule(
activity_type_id=template.activity_type_id.id,
automated=False,
summary=template.summary,
note=template.note,
user_id=responsible.id,
date_deadline=date_deadline
)
activity_descriptions.append(
_('%(activity)s, assigned to %(name)s, due on the %(deadline)s',
activity=template.summary or template.activity_type_id.name,
name=responsible.name, deadline=format_date(self.env, date_deadline)))
if activity_descriptions:
body += Markup('<ul>%s</ul>') % (
Markup().join(Markup('<li>%s</li>') % description for description in activity_descriptions)
)
record.message_post(body=body)
if len(applied_on) == 1:
return {'type': 'ir.actions.client', 'tag': 'soft_reload'}
return {
'type': 'ir.actions.act_window',
'res_model': self.res_model,
'name': _('Launch Plans'),
'view_mode': 'list,form',
'target': 'current',
'domain': [('id', 'in', applied_on.ids)],
}
def _check_plan_templates_error(self, applied_on):
self.ensure_one()
return filter(
None, [
activity_template._determine_responsible(self.plan_on_demand_user_id, record)['error']
for activity_template in self.plan_id.template_ids
for record in applied_on
]
)
def _check_plan_templates_warning(self, applied_on):
self.ensure_one()
return filter(
None, [
activity_template._determine_responsible(self.plan_on_demand_user_id, record)['warning']
for activity_template in self.plan_id.template_ids
for record in applied_on
]
)
# ------------------------------------------------------------
# ACTIVITY-BASED SCHEDULING API
# ------------------------------------------------------------
def action_schedule_activities(self):
self._action_schedule_activities()
def action_schedule_activities_done(self):
self._action_schedule_activities().action_done()
def _action_schedule_activities(self):
if not self.res_model:
return self._action_schedule_activities_personal()
return self._get_applied_on_records().activity_schedule(
activity_type_id=self.activity_type_id.id,
automated=False,
summary=self.summary,
note=self.note,
user_id=self.activity_user_id.id,
date_deadline=self.date_deadline
)
def _action_schedule_activities_personal(self):
if not self.activity_user_id:
raise ValueError(_('Scheduling personal activities requires an assigned user.'))
return self.env['mail.activity'].create({
'activity_type_id': self.activity_type_id.id,
'automated': False,
'date_deadline': self.date_deadline,
'note': self.note,
'res_id': False,
'res_model_id': False,
'summary': self.summary,
'user_id': self.activity_user_id.id,
})
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _evaluate_res_ids(self):
""" Parse composer res_ids, which can be: an already valid list or
tuple (generally in code), a list or tuple as a string (coming from
actions). Void strings / missing values are evaluated as an empty list.
:return: a list of IDs (empty list in case of falsy strings)"""
self.ensure_one()
return parse_res_ids(self.res_ids, self.env) or []
def _get_applied_on_records(self):
if not self.res_model:
return None
return self.env[self.res_model].browse(self._evaluate_res_ids())
def _get_plan_available_base_domain(self):
self.ensure_one()
return Domain.AND([
['|', ('company_id', '=', False), ('company_id', '=', self.company_id.id)],
['|', ('res_model', '=', False), ('res_model', '=', self.res_model)],
[('template_ids', '!=', False)], # exclude plan without activities
])
def _plan_filter_activity_templates_to_schedule(self):
return self.plan_id.template_ids
@api.onchange('activity_user_id', 'activity_type_id')
def _onchange_activity_user_id(self):
if self.activity_category != "upload_file":
return
activity_user = self.activity_user_id
model = self.res_model
if model and activity_user:
try:
thread = self.with_user(activity_user).env[model].browse(self._evaluate_res_ids())
thread.check_access(thread._mail_get_operation_for_mail_message_operation('create')[thread])
except AccessError:
raise UserError(_("Selected user '%(user)s' cannot upload documents on model '%(model)s'",
model=model,
user=activity_user.display_name))

View file

@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class MailActivityScheduleSummary(models.TransientModel):
_name = 'mail.activity.schedule.line'
_description = 'Mail Activity Schedule Line'
_order = 'line_date_deadline asc, id asc'
_rec_name = 'activity_schedule_id'
activity_schedule_id = fields.Many2one('mail.activity.schedule', string="Activity Schedule",
required=True, ondelete='cascade')
line_description = fields.Char("Line Description")
line_date_deadline = fields.Date("Date Deadline")
responsible_user_id = fields.Many2one('res.users', string="Responsible User")

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="mail_activity_schedule_view_form" model="ir.ui.view">
<field name="name">Activity schedule</field>
<field name="model">mail.activity.schedule</field>
<field name="arch" type="xml">
<form>
<field name="activity_category" invisible="1"/>
<field name="chaining_type" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="has_error" invisible="1"/>
<field name="has_warning" invisible="1"/>
<field name="plan_has_user_on_demand" invisible="1"/>
<field name="res_ids" invisible="1"/>
<field name="plan_available_ids" invisible="1"/>
<sheet class="o_mail_activity_schedule_wizard">
<field name="plan_id"
options="{'no_open': True, 'no_create': True}"
widget="selection_badge"
invisible="not plan_available_ids"
nolabel="1"/>
<br/>
<field name="activity_type_id"
required="not plan_id"
widget="selection_badge_icons"
iconField="icon"
nolabel="1"/>
<group invisible="not plan_id">
<group>
<field name="plan_date" placeholder="Default deadline for the activities..." string="Due Date"/>
<field name="plan_on_demand_user_id" widget="many2one_avatar_user"
invisible="not plan_has_user_on_demand" string="Responsible"/>
</group>
<group>
<label for="plan_schedule_line_ids" string="Plan Summary" class="o_form_label mb-n2" colspan="2"/>
<field name="plan_schedule_line_ids" nolabel="1" class="text-muted mt-n2 small" colspan="2">
<list edit="0" no_open="1" class="o_mail_activity_schedule_summary">
<field name="responsible_user_id" nolabel="1" widget="many2one_avatar_user" width="22px"/>
<field name="line_description" nolabel="1"/>
<field name="line_date_deadline" nolabel="1"/>
</list>
</field>
</group>
</group>
<group invisible="not activity_type_id">
<group name="summary_group" colspan="2">
<label for="summary" class="o_form_label fs-3"/>
<field string="Summary" name="summary" placeholder="e.g. Discuss Proposal" class="fs-3 w-100" nolabel="1"/>
</group>
<group>
<field name="date_deadline" string="Due Date"/>
<field name="activity_user_id" widget="many2one_avatar_user" placeholder="Unassigned"/>
<field name="res_model" widget="activity_model_selector" string="Link to"
invisible="id or context.get('active_model')" required="activity_user_id != context.get('uid')"/>
</group>
<field name="note" class="oe-bordered-editor embedded-editor-height-4" placeholder="Log a note..." widget="html_mail" />
</group>
<div role="alert" class="alert alert-danger mb8" invisible="not has_error">
<field name="error"/>
</div>
<div role="alert" class="alert alert-warning mb8" invisible="not has_warning">
<field name="warning"/>
</div>
</sheet>
<footer invisible="plan_id">
<button name="action_schedule_activities" string="Save" type="object" class="btn-primary"
invisible="has_error" data-hotkey="q"/>
<button name="action_schedule_activities_done" string="Mark Done" type="object"
invisible="has_error or chaining_type == 'trigger'"
class="btn-secondary" data-hotkey="w"
context="{'mail_activity_quick_update': True}"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
<footer invisible="not plan_id">
<button name="action_schedule_plan" string="Schedule" type="object" class="btn-primary"
invisible="has_error" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
from markupsafe import Markup
from odoo import fields, models, _
class MailBlacklistRemove(models.TransientModel):
@ -11,4 +12,11 @@ class MailBlacklistRemove(models.TransientModel):
reason = fields.Char(name="Reason")
def action_unblacklist_apply(self):
return self.env['mail.blacklist'].action_remove_with_reason(self.email, self.reason)
if self.reason:
message = Markup('<p>%s</p>') % _("Unblock Reason: %(reason)s", reason=self.reason)
else:
message = None
return self.env['mail.blacklist']._remove(
self.email,
message=message,
)

View file

@ -7,11 +7,11 @@
<form string="mail_blacklist_removal">
<group class="oe_title">
<field name="email" string="Email Address"/>
<field name="reason" string="Reason"/>
<field name="reason" string="Reason" placeholder='e.g "Asked to receive our next newsletters"'/>
</group>
<footer>
<button name="action_unblacklist_apply" string="Confirm" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button name="action_unblacklist_apply" string="Remove address from blacklist" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>

File diff suppressed because it is too large Load diff

View file

@ -4,76 +4,120 @@
<record model="ir.ui.view" id="email_compose_message_wizard_form">
<field name="name">mail.compose.message.form</field>
<field name="model">mail.compose.message</field>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="group_ids" eval="[Command.link(ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Compose Email" class="pt-0 pb-0 o_mail_composer_form">
<form string="Compose Email" class="pt-0 pb-0 o_mail_composer_form" js_class="mail_composer_form" disable_autofocus="1">
<group>
<!-- truly invisible fields for control and options -->
<field name="author_id" invisible="1"/>
<field name="auto_delete" invisible="1"/>
<field name="auto_delete_message" invisible="1"/>
<field name="auto_delete_keep_log" invisible="1"/>
<field name="composition_batch" invisible="1"/>
<field name="composition_comment_option" invisible="1"/>
<field name="composition_mode" invisible="1"/>
<field name="email_layout_xmlid" invisible="1"/>
<field name="is_log" invisible="1"/>
<field name="force_send" invisible="1"/>
<field name="lang" invisible="1"/>
<field name="mail_server_id" invisible="1"/>
<field name="model" invisible="1"/>
<field name="model_is_thread" invisible="1"/>
<field name="notified_bcc_contains_share" invisible="1"/>
<field name="notify_author" invisible="1"/>
<field name="notify_author_mention" invisible="1"/>
<field name="notify_skip_followers" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="record_name" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="partner_ids_all_have_email" invisible="1"/>
<field name="record_alias_domain_id" invisible="1"/>
<field name="record_company_id" invisible="1"/>
<field name="render_model" invisible="1"/>
<field name="res_domain" invisible="1"/>
<field name="res_domain_user_id" invisible="1"/>
<field name="res_ids" invisible="1"/>
<field name="scheduled_date" invisible="1"/>
<field name="subtype_id" invisible="1"/>
<field name="subtype_is_log" invisible="1"/>
<field name="use_exclusion_list" invisible="1"/>
<!-- visible wizard -->
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<label for="partner_ids" string="Recipients" attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}"/>
<div groups="base.group_user" attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}">
<span name="document_followers_text" attrs="{'invisible':['|', ('model', '=', False), ('composition_mode', '=', 'mass_mail')]}">Followers of the document and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
invisible="composition_mode != 'mass_mail'"/>
<label for="partner_ids" string="To" invisible="composition_mode != 'comment' or subtype_is_log"/>
<div groups="base.group_user" invisible="composition_mode != 'comment' or subtype_is_log" class="d-flex gap-3">
<field name="partner_ids" widget="many2many_tags_email" placeholder="Followers only" class="w-auto flex-grow-1"
invisible="composition_comment_option == 'forward' or (composition_comment_option != 'reply_all' and not context.get('clicked_on_full_composer', False))"
context="{'form_view_ref': 'base.view_partner_simple_form'}"
options="{'edit_tags': True}"/>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add recipients..." class="w-auto flex-grow-1"
required="composition_comment_option == 'forward' or (composition_comment_option != 'reply_all' and not context.get('clicked_on_full_composer', False) and composition_mode == 'comment' and not notified_bcc_contains_share)"
invisible="composition_comment_option != 'forward' and (context.get('clicked_on_full_composer', False) or composition_comment_option == 'reply_all' )"
options="{'edit_tags': True}"
context="{'force_email': True, 'show_email': True, 'form_view_ref': 'base.view_partner_simple_form', 'forward_mode': True}"/>
</div>
<field name="subject" placeholder="Welcome to MyCompany!" required="True"/>
<!-- mass post -->
<field name="notify"
attrs="{'invisible':[('composition_mode', '!=', 'mass_post')]}"/>
<field name="subject" placeholder="e.g. Welcome to MyCompany!" required="True"/>
<field name="reply_to" placeholder="e.g. info@company.com"
invisible="reply_to_mode == 'update'"
required="reply_to_mode != 'update'"/>
</group>
<field name="can_edit_body" invisible="1"/>
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
<field name="body" class="oe-bordered-editor" placeholder="Write your message here..." options="{'style-inline': true}" attrs="{'readonly': [('can_edit_body', '=', False)]}" force_save="1"/>
<group col="4">
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2"/>
<field name="template_id" string="Load template" options="{'no_create': True}"
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
</group>
<div invisible="composition_mode == 'mass_mail'">
<field name="body" widget="html_composer_message" class="oe-bordered-editor"
placeholder="Write your message here..." readonly="not can_edit_body" force_save="1"
options="{'dynamic_placeholder': true, 'dynamic_placeholder_model_reference_field': 'render_model'}"/>
<field name="attachment_ids" widget="mail_composer_attachment_list"/>
</div>
<notebook attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
<page string="Content">
<notebook invisible="composition_mode != 'mass_mail'">
<page string="Content" name="page_content">
<div>
<field name="body" class="oe-bordered-editor" placeholder="Write your message here..." options="{'style-inline': true}" attrs="{'readonly': [('can_edit_body', '=', False)]}" force_save="1"/>
<group col="4">
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2"/>
<field name="template_id" string="Load template" options="{'no_create': True}"
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
</group>
<field name="body" widget="html_composer_message" class="oe-bordered-editor"
placeholder="Write your message here..." readonly="not can_edit_body" force_save="1"
options="{'dynamic_placeholder': true, 'dynamic_placeholder_model_reference_field': 'render_model'}"/>
<field name="attachment_ids" widget="mail_composer_attachment_list"/>
</div>
</page>
<page string="Settings">
<page string="Settings" name="page_settings">
<!-- mass mailing -->
<field name="reply_to_force_new" invisible="1"/>
<field name="reply_to_mode" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}" widget="radio"/>
<group>
<field name="reply_to" string="Reply-to Address" placeholder='e.g: "info@mycompany.odoo.com"'
attrs="{'invisible':['|', ('reply_to_mode', '=', 'update'), ('composition_mode', '!=', 'mass_mail')],
'required':[('reply_to_mode', '!=', 'update'), ('composition_mode', '=', 'mass_mail')]}"/>
</group>
<field name="reply_to_mode" invisible="composition_mode != 'mass_mail'" widget="radio"/>
<field name="use_exclusion_list" invisible="1"/>
</page>
</notebook>
<footer>
<button string="Send" attrs="{'invisible': [('is_log', '=', True)]}" name="action_send_mail" type="object" class="btn-primary o_mail_send" data-hotkey="q"/>
<button string="Log" attrs="{'invisible': [('is_log', '=', False)]}" name="action_send_mail" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
<button icon="fa-lg fa-save" type="object"
name="action_save_as_template" string="Save as new template"
attrs="{'invisible': [('can_edit_body', '=', False)]}"
class="float-end btn-secondary" help="Save as a new template" data-hotkey="w"/>
<button string="Send" name="action_send_mail"
type="object" class="btn-primary o_mail_send" data-hotkey="q"
invisible="(subtype_is_log or composition_mode == 'comment' and not composition_batch and scheduled_date) or not partner_ids_all_have_email"/>
<button string="Log" name="action_send_mail"
type="object" class="btn-primary" data-hotkey="q"
invisible="(not subtype_is_log or composition_mode == 'comment' and not composition_batch and scheduled_date) or not partner_ids_all_have_email"/>
<button string="Schedule" name="action_schedule_message" type="object" class="btn-primary" data-hotkey="q"
invisible="(composition_mode != 'comment' or composition_batch or not scheduled_date) or not partner_ids_all_have_email"/>
<button string="Send" name="action_send_mail" disabled="1"
type="object" class="btn-primary o_mail_send" data-hotkey="q"
invisible="(subtype_is_log or composition_mode == 'comment' and not composition_batch and scheduled_date) or partner_ids_all_have_email"/>
<button string="Log" name="action_send_mail" disabled="1"
type="object" class="btn-primary" data-hotkey="q"
invisible="(not subtype_is_log or composition_mode == 'comment' and not composition_batch and scheduled_date) or partner_ids_all_have_email"/>
<button string="Schedule" name="action_schedule_message" type="object" class="btn-primary" data-hotkey="q" disabled="1"
invisible="(composition_mode != 'comment' or composition_batch or not scheduled_date) or partner_ids_all_have_email"/>
<button string="Discard" class="btn-secondary w-auto" special="cancel" data-hotkey="x" />
<field name="attachment_ids" widget="mail_composer_attachment_selector" invisible="not can_edit_body"/>
<field name="template_id" widget="mail_composer_template_selector"/>
<field name="scheduled_date" widget="text_scheduled_date" invisible="composition_batch or composition_mode != 'comment'"/>
</footer>
</form>
</field>
</record>
<record id="mail_compose_message_view_form_template_save" model="ir.ui.view">
<field name="name">mail.compose.message.view.form.template.save</field>
<field name="model">mail.compose.message</field>
<field name="arch" type="xml">
<form js_class="mail_composer_save_template_form" string="Templates">
<group>
<field name="template_name" placeholder="e.g: Send order confirmation" required="1"/>
<field name="model" invisible="1"/>
</group>
<footer>
<button name="create_mail_template" type="object" class="btn btn-primary" string="Save Template"/>
<button class="btn btn-secondary" string="Discard" special="cancel"/>
</footer>
</form>
</field>
@ -83,7 +127,6 @@
<field name="name">Compose Email</field>
<field name="res_model">mail.compose.message</field>
<field name="binding_model_id" ref="mail.model_mail_compose_message"/>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>

View file

@ -0,0 +1,80 @@
from odoo import fields, models
from odoo.exceptions import UserError
from odoo.addons.mail.tools.parser import parse_res_ids
class MailFollowersEdit(models.TransientModel):
"""Wizard to edit partners (or channels) to add/remove them to/from followers list."""
_name = 'mail.followers.edit'
_description = "Followers edit wizard"
res_model = fields.Char(
"Related Document Model", required=True, help="Model of the followed resource"
)
res_ids = fields.Char("Related Document IDs", help="Ids of the followed resources")
operation = fields.Selection(
[
("add", "Add"),
("remove", "Remove"),
],
string="Operation",
required=True,
default="add",
)
partner_ids = fields.Many2many("res.partner", required=True, string="Followers")
message = fields.Html("Message")
notify = fields.Boolean("Notify Recipients", default=False)
def edit_followers(self):
for wizard in self:
res_ids = parse_res_ids(wizard.res_ids, self.env)
documents = self.env[wizard.res_model].browse(res_ids)
if not documents:
raise UserError(self.env._("No documents found for the selected records."))
if wizard.operation == "remove":
documents.message_unsubscribe(partner_ids=wizard.partner_ids.ids)
else:
if not self.env.user.email:
raise UserError(
self.env._(
"Unable to post message, please configure the sender's email address."
)
)
documents.message_subscribe(partner_ids=wizard.partner_ids.ids)
if wizard.notify:
model_name = self.env["ir.model"]._get(wizard.res_model).display_name
message_values = wizard._prepare_message_values(documents, model_name)
message_values["partner_ids"] = wizard.partner_ids.ids
documents[0].message_notify(**message_values)
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"type": "success",
"message": self.env._("Followers updated") if len(wizard) > 1 else (
self.env._("Followers added") if wizard.operation == "add" else self.env._("Followers removed")
),
"sticky": False,
"next": {"type": "ir.actions.act_window_close"},
},
}
def _prepare_message_values(self, documents, model_name):
return {
"body": (len(documents) > 1 and (", ".join(documents.mapped('display_name')) + "\n") or "") + (self.message or ""),
"email_add_signature": False,
"email_from": self.env.user.email_formatted,
"email_layout_xmlid": len(documents) > 1 and "mail.mail_notification_multi_invite" or "mail.mail_notification_invite",
"model": self.res_model,
"reply_to": self.env.user.email_formatted,
"reply_to_force_new": True,
"subject": len(documents) > 1 and self.env._(
"Invitation to follow %(document_model)s.",
document_model=model_name,
) or self.env._(
"Invitation to follow %(document_model)s: %(document_name)s",
document_model=model_name,
document_name=documents.display_name,
)
}

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- wizard view -->
<record model="ir.ui.view" id="mail_followers_edit_form">
<field name="name">mail.followers.edit.form</field>
<field name="model">mail.followers.edit</field>
<field name="arch" type="xml">
<form string="Add/Remove Followers">
<group>
<field name="res_model" invisible="1"/>
<field name="res_ids" invisible="1"/>
<field name="operation" widget="radio" options="{'horizontal': true}"/>
<field name="partner_ids" widget="many2many_tags_email"
placeholder="Add contacts" options="{'warn_future': True, 'edit_tags': True}"
context="{'show_email': True, 'form_view_ref': 'base.view_partner_simple_form', 'force_email': True}"/>
<field name="notify" options="{'autosave': False}" invisible="operation == 'remove'" widget="boolean_toggle"/>
</group>
<field name="message"
invisible="not notify or operation == 'remove'"
placeholder="Extra Comments ..."
widget="html_mail"
options="{'no-attachment': true}"
class="o_mail_extra_comments border p-1 ps-1 pe-5"/>
<footer>
<button string="Update Followers" name="edit_followers" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
</field>
</record>
<record id="mail_followers_list_edit_form" model="ir.ui.view">
<field name="name">mail.followers.list.edit.form</field>
<field name="model">mail.followers.edit</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="mail_followers_edit_form"/>
<field name="arch" type="xml">
<data>
<field name="operation" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<button name="edit_followers" position="attributes">
<attribute name="string">Add Followers</attribute>
</button>
</data>
</field>
</record>
</data>
</odoo>

View file

@ -1,102 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
class MailResendMessage(models.TransientModel):
_name = 'mail.resend.message'
_description = 'Email resend wizard'
mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True)
partner_ids = fields.One2many('mail.resend.partner', 'resend_wizard_id', string='Recipients')
notification_ids = fields.Many2many('mail.notification', string='Notifications', readonly=True)
can_cancel = fields.Boolean(compute='_compute_can_cancel')
can_resend = fields.Boolean(compute='_compute_can_resend')
partner_readonly = fields.Boolean(compute='_compute_partner_readonly')
@api.depends("partner_ids")
def _compute_can_cancel(self):
self.can_cancel = self.partner_ids.filtered(lambda p: not p.resend)
@api.depends('partner_ids.resend')
def _compute_can_resend(self):
self.can_resend = any([partner.resend for partner in self.partner_ids])
def _compute_partner_readonly(self):
self.partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
@api.model
def default_get(self, fields):
rec = super(MailResendMessage, self).default_get(fields)
message_id = self._context.get('mail_message_to_resend')
if message_id:
mail_message_id = self.env['mail.message'].browse(message_id)
notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce'))
partner_ids = [Command.create({
"partner_id": notif.res_partner_id.id,
"name": notif.res_partner_id.name,
"email": notif.res_partner_id.email,
"resend": True,
"message": notif.format_failure_reason(),
}) for notif in notification_ids]
has_user = any(notif.res_partner_id.user_ids for notif in notification_ids)
if has_user:
partner_readonly = not self.env['res.users'].check_access_rights('write', raise_exception=False)
else:
partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
rec['partner_readonly'] = partner_readonly
rec['notification_ids'] = [Command.set(notification_ids.ids)]
rec['mail_message_id'] = mail_message_id.id
rec['partner_ids'] = partner_ids
else:
raise UserError(_('No message_id found in context'))
return rec
def resend_mail_action(self):
""" Process the wizard content and proceed with sending the related
email(s), rendering any template patterns on the fly if needed. """
for wizard in self:
"If a partner disappeared from partner list, we cancel the notification"
to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.res_partner_id in to_cancel and notif.notification_status in ('exception', 'bounce'))
notif_to_cancel.sudo().write({'notification_status': 'canceled'})
if to_send:
message = wizard.mail_message_id
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
email_partners_data = []
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_thread_by_email(
message, email_partners_data,
resend_existing=True,
send_after_commit=False
)
self.mail_message_id._notify_message_notification_update()
return {'type': 'ir.actions.act_window_close'}
def cancel_mail_action(self):
for wizard in self:
for notif in wizard.notification_ids:
notif.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce')).sudo().write({'notification_status': 'canceled'})
wizard.mail_message_id._notify_message_notification_update()
return {'type': 'ir.actions.act_window_close'}
class PartnerResend(models.TransientModel):
_name = 'mail.resend.partner'
_description = 'Partner with additional information for mail resend'
partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete='cascade')
name = fields.Char(related='partner_id.name', string='Recipient Name', related_sudo=False, readonly=False)
email = fields.Char(related='partner_id.email', string='Email Address', related_sudo=False, readonly=False)
resend = fields.Boolean(string='Try Again', default=True)
resend_wizard_id = fields.Many2one('mail.resend.message', string="Resend wizard")
message = fields.Char(string='Error message')

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="mail_resend_message_view_form" model="ir.ui.view">
<field name="name">mail.resend.message.view.form</field>
<field name="model">mail.resend.message</field>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Edit Partners">
<field name="mail_message_id" invisible="1"/>
<field name="notification_ids" invisible="1"/>
<field name="can_resend" invisible="1"/>
<field name="partner_readonly" invisible="1"/>
<field name="partner_ids">
<tree string="Recipient" editable="top" create="0" delete="0">
<field name="name" readonly="1"/>
<field name="email" attrs="{'readonly': [('parent.partner_readonly', '=', True)]}"/>
<field name="message" readonly="1" class="text-wrap"/>
<field name="partner_id" invisible="1"/>
<field name="resend" widget="boolean_toggle"/>
</tree>
</field>
<footer>
<button string="Send &amp; close" name="resend_mail_action" type="object" class="btn-primary o_mail_send"
attrs="{'invisible': [('can_resend', '=', False)]}" data-hotkey="q"/>
<button string="Ignore all" name="cancel_mail_action" type="object" class="btn-primary"
attrs="{'invisible': [('can_resend', '=', True)]}" data-hotkey="w"/>
<button string="Ignore all" name="cancel_mail_action" type="object" class="btn-secondary"
attrs="{'invisible': [('can_resend', '=', False)]}" data-hotkey="w"/>
<button string="Close" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="mail_resend_message_action" model="ir.actions.act_window">
<field name="name">Sending Failures</field>
<field name="res_model">mail.resend.message</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>

View file

@ -8,8 +8,17 @@ from odoo.exceptions import UserError
class MailTemplatePreview(models.TransientModel):
_name = 'mail.template.preview'
_description = 'Email Template Preview'
_MAIL_TEMPLATE_FIELDS = ['subject', 'body_html', 'email_from', 'email_to',
'email_cc', 'reply_to', 'scheduled_date', 'attachment_ids']
_MAIL_TEMPLATE_FIELDS = ['attachment_ids',
'body_html',
'subject',
'email_cc',
'email_from',
'email_to',
'partner_to',
'report_template_ids',
'reply_to',
'scheduled_date',
]
@api.model
def _selection_target_model(self):
@ -19,24 +28,19 @@ class MailTemplatePreview(models.TransientModel):
def _selection_languages(self):
return self.env['res.lang'].get_installed()
@api.model
def default_get(self, fields):
result = super(MailTemplatePreview, self).default_get(fields)
if not result.get('mail_template_id') or 'resource_ref' not in fields:
return result
mail_template = self.env['mail.template'].browse(result['mail_template_id']).sudo()
model = mail_template.model
res = self.env[model].search([], limit=1)
if res:
result['resource_ref'] = '%s,%s' % (model, res.id)
return result
mail_template_id = fields.Many2one('mail.template', string='Related Mail Template', required=True)
model_id = fields.Many2one('ir.model', string='Targeted model', related="mail_template_id.model_id")
resource_ref = fields.Reference(string='Record', selection='_selection_target_model')
resource_ref = fields.Reference(
string='Record',
compute='_compute_resource_ref',
compute_sudo=False, readonly=False,
selection='_selection_target_model',
store=True
)
lang = fields.Selection(_selection_languages, string='Template Preview Language')
no_record = fields.Boolean('No Record', compute='_compute_no_record')
error_msg = fields.Char('Error Message', readonly=True)
error_msg = fields.Char('Error Message', compute='_compute_mail_template_fields')
# Fields same than the mail.template model, computed with resource_ref and lang
subject = fields.Char('Subject', compute='_compute_mail_template_fields')
email_from = fields.Char('From', compute='_compute_mail_template_fields', help="Sender address")
@ -47,8 +51,10 @@ class MailTemplatePreview(models.TransientModel):
scheduled_date = fields.Char('Scheduled Date', compute='_compute_mail_template_fields',
help="The queue manager will send the email after the date")
body_html = fields.Html('Body', compute='_compute_mail_template_fields', sanitize=False)
attachment_ids = fields.Many2many('ir.attachment', 'Attachments', compute='_compute_mail_template_fields')
# Extra fields info generated by generate_email
attachment_ids = fields.Many2many('ir.attachment', string='Attachments', compute='_compute_mail_template_fields')
has_attachments = fields.Boolean(compute='_compute_has_attachments')
has_several_languages_installed = fields.Boolean(compute='_compute_has_several_languages_installed')
# Extra fields info generated by _generate_template
partner_ids = fields.Many2many('res.partner', string='Recipients', compute='_compute_mail_template_fields')
@api.depends('model_id')
@ -62,27 +68,50 @@ class MailTemplatePreview(models.TransientModel):
""" Preview the mail template (body, subject, ...) depending of the language and
the record reference, more precisely the record id for the defined model of the mail template.
If no record id is selectable/set, the inline_template placeholders won't be replace in the display information. """
copy_depends_values = {'lang': self.lang}
mail_template = self.mail_template_id.with_context(lang=self.lang)
try:
if not self.resource_ref:
self._set_mail_attributes()
for preview in self:
error_msg = False
mail_template = preview.mail_template_id.with_context(lang=preview.lang)
if not preview.resource_ref or not preview.resource_ref.id:
preview._set_mail_attributes()
preview.error_msg = False
else:
copy_depends_values['resource_ref'] = '%s,%s' % (self.resource_ref._name, self.resource_ref.id)
mail_values = mail_template.with_context(template_preview_lang=self.lang).generate_email(
self.resource_ref.id, self._MAIL_TEMPLATE_FIELDS + ['partner_to'])
self._set_mail_attributes(values=mail_values)
self.error_msg = False
except UserError as user_error:
self._set_mail_attributes()
self.error_msg = user_error.args[0]
finally:
# Avoid to be change by a cache invalidation (in generate_mail), e.g. Quotation / Order report
for key, value in copy_depends_values.items():
self[key] = value
try:
mail_values = mail_template.with_context(template_preview_lang=preview.lang)._generate_template(
[preview.resource_ref.id],
preview._MAIL_TEMPLATE_FIELDS
)[preview.resource_ref.id]
preview._set_mail_attributes(values=mail_values)
except (ValueError, UserError) as user_error:
preview._set_mail_attributes()
error_msg = user_error.args[0]
preview.error_msg = error_msg
@api.depends('attachment_ids')
def _compute_has_attachments(self):
for preview in self:
preview.has_attachments = bool(preview.attachment_ids)
@api.depends('lang')
def _compute_has_several_languages_installed(self):
for preview in self:
preview.has_several_languages_installed = bool(preview._fields['lang'].selection(preview))
@api.depends('mail_template_id')
def _compute_resource_ref(self):
to_reset = self.filtered(lambda p: not p.mail_template_id.model)
to_reset.resource_ref = False
for preview in (self - to_reset):
mail_template = preview.mail_template_id.sudo()
model = mail_template.model
res = self.env[model].search([], limit=1)
preview.resource_ref = f'{model},{res.id}' if res else False
def _set_mail_attributes(self, values=None):
for field in self._MAIL_TEMPLATE_FIELDS:
if field in ('partner_to', 'report_template_ids'):
# partner_to is used to generate partner_ids, handled here below
# report_template_ids generates attachments, no usage here
continue
field_value = values.get(field, False) if values else self.mail_template_id[field]
self[field] = field_value
self.partner_ids = values.get('partner_ids', False) if values else False

View file

@ -5,42 +5,40 @@
<field name="name">mail.template.preview.view.form</field>
<field name="model">mail.template.preview</field>
<field name="arch" type="xml">
<form string="Email Preview">
<h3>Preview of <field name="mail_template_id" readonly="1" nolabel="1" options="{'no_open' : True}"/></h3>
<div class="alert alert-danger" role="alert" attrs="{'invisible' : [('error_msg', '=', False)]}">
<field name="error_msg" widget="text"/>
</div>
<field name="no_record" invisible="1"/>
<div class="container">
<div class="row">
<span class="col-md-5 col-lg-4 col-sm-12 ps-0">Choose an example <field name="model_id" readonly="1"/> record:</span>
<div class="col-md-7 col-lg-6 col-sm-12 ps-0">
<field name="resource_ref" readonly="False" class="w-100"
options="{'hide_model': True, 'no_create': True, 'no_open': True}"
attrs="{'invisible': [('no_record', '=', True)]}"/>
<b attrs="{'invisible': [('no_record', '=', False)]}" class="text-warning">No record for this model</b>
</div>
<form string="Email Preview" class="o_mail_template_preview_form_view">
<sheet>
<div class="alert alert-danger" role="alert" invisible="not error_msg">
<field name="error_msg" widget="text"/>
</div>
<div class="row">
<span class="col-md-5 col-lg-4 col-sm-12 ps-0">Force a language: </span>
<div class="col-md-7 col-lg-6 col-sm-12 ps-0">
<field name="lang" placeholder="Select a language" class="w-100"/>
</div>
</div>
</div>
<group>
<field name="subject"/>
<field name="email_from" attrs="{'invisible':[('email_from','=', False)]}"/>
<field name="partner_ids" widget="many2many_tags" attrs="{'invisible':[('partner_ids', '=', [])]}"/>
<field name="email_to" attrs="{'invisible':[('email_to','=', False)]}"/>
<field name="email_cc" attrs="{'invisible':[('email_cc','=', False)]}"/>
<field name="reply_to" attrs="{'invisible':[('reply_to','=', False)]}"/>
<field name="scheduled_date" attrs="{'invisible':[('scheduled_date','=', False)]}"/>
</group>
<field name="body_html" widget="html" nolabel="1" options='{"safe": True}'/>
<field name="attachment_ids" widget="many2many_binary"/>
<field name="mail_template_id" invisible="1"/>
<field name="no_record" invisible="1"/>
<group>
<label for="resource_ref" string="Test Record"/>
<field name="resource_ref" nolabel="1"
options="{'hide_model': True, 'no_create': True, 'no_open': True}"
readonly="False"
invisible="no_record"/>
<b invisible="not no_record" class="text-warning">No record for this model</b>
<field name="has_several_languages_installed" invisible="1"/>
<field name="lang" invisible="not has_several_languages_installed" string="Force a Language" placeholder="Select a language"/>
<field name="subject"/>
<field name="email_from" invisible="not email_from"/>
<field name="partner_ids" widget="many2many_tags" invisible="not partner_ids"/>
<field name="email_to" invisible="not email_to"/>
<field name="email_cc" invisible="not email_cc"/>
<field name="reply_to" invisible="not reply_to"/>
<field name="scheduled_date" invisible="not scheduled_date"/>
<field name="body_html" widget="html" colspan="2" nolabel="1" options="{'safe': True}"/>
<field name="has_attachments" invisible="1"/>
<field name="attachment_ids" invisible="not has_attachments" widget="many2many_binary"/>
</group>
</sheet>
<footer>
<button string="Close" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Close" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -50,7 +48,6 @@
<field name="name">Template Preview</field>
<field name="res_model">mail.template.preview</field>
<field name="binding_model_id" eval="False"/>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="view_id" ref="mail_template_preview_view_form"/>
<field name="target">new</field>

View file

@ -23,7 +23,7 @@ class MailTemplateReset(models.TransientModel):
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _('Mail Templates have been reset'),
'message': _('The email template(s) have been restored to their original settings.'),
'next': next_action,
}
}

View file

@ -10,8 +10,8 @@
Are you sure you want to reset these email templates to their original configuration? Changes and translations will be lost.
</div>
<footer>
<button string="Proceed" class="btn btn-primary" type="object" name="reset_template" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
<button string="Reset Template" class="btn btn-primary" type="object" name="reset_template" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
@ -21,8 +21,7 @@
<field name="name">Reset Mail Template</field>
<field name="res_model">mail.template.reset</field>
<field name="binding_model_id" ref="mail.model_mail_template"/>
<field name="binding_view_types">list</field>
<field name="type">ir.actions.act_window</field>
<field name="binding_view_types">list,kanban</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{

View file

@ -1,93 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from lxml.html import builder as html
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class Invite(models.TransientModel):
""" Wizard to invite partners (or channels) and make them followers. """
_name = 'mail.wizard.invite'
_description = 'Invite wizard'
@api.model
def default_get(self, fields):
result = super(Invite, self).default_get(fields)
if 'message' not in fields:
return result
user_name = self.env.user.display_name
model = result.get('res_model')
res_id = result.get('res_id')
if model and res_id:
document = self.env['ir.model']._get(model).display_name
title = self.env[model].browse(res_id).display_name
msg_fmt = _('%(user_name)s invited you to follow %(document)s document: %(title)s')
else:
msg_fmt = _('%(user_name)s invited you to follow a new document.')
text = msg_fmt % locals()
message = html.DIV(
html.P(_('Hello,')),
html.P(text)
)
result['message'] = etree.tostring(message)
return result
res_model = fields.Char('Related Document Model', required=True, help='Model of the followed resource')
res_id = fields.Integer('Related Document ID', help='Id of the followed resource')
partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.",
domain=[('type', '!=', 'private')])
message = fields.Html('Message')
send_mail = fields.Boolean('Send Email', default=True, help="If checked, the partners will receive an email warning they have been added in the document's followers.")
def add_followers(self):
if not self.env.user.email:
raise UserError(_("Unable to post message, please configure the sender's email address."))
email_from = self.env.user.email_formatted
for wizard in self:
Model = self.env[wizard.res_model]
document = Model.browse(wizard.res_id)
# filter partner_ids to get the new followers, to avoid sending email to already following partners
new_partners = wizard.partner_ids - document.sudo().message_partner_ids
document.message_subscribe(partner_ids=new_partners.ids)
model_name = self.env['ir.model']._get(wizard.res_model).display_name
# send an email if option checked and if a message exists (do not send void emails)
if wizard.send_mail and wizard.message and not wizard.message == '<br>': # when deleting the message, cleditor keeps a <br>
message = self.env['mail.message'].create(
self._prepare_message_values(document, model_name, email_from)
)
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_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})
message.unlink()
return {'type': 'ir.actions.act_window_close'}
def _prepare_message_values(self, document, model_name, email_from):
return {
'subject': _('Invitation to follow %(document_model)s: %(document_name)s', document_model=model_name,
document_name=document.display_name),
'body': self.message,
'record_name': document.display_name,
'email_from': email_from,
'reply_to': email_from,
'model': self.res_model,
'res_id': self.res_id,
'reply_to_force_new': True,
'email_add_signature': True,
}

View file

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- wizard view -->
<record model="ir.ui.view" id="mail_wizard_invite_form">
<field name="name">Add Followers</field>
<field name="model">mail.wizard.invite</field>
<field name="arch" type="xml">
<form string="Add Followers">
<group>
<field name="res_model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="partner_ids" widget="many2many_tags_email"
placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
<field name="send_mail"/>
<field name="message" attrs="{'invisible': [('send_mail','!=',True)]}" options="{'style-inline': true, 'no-attachment': true}" class="test_message"/>
</group>
<footer>
<button string="Add Followers"
name="add_followers" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
</data>
</odoo>