mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 18:52:02 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
],
|
||||
)
|
||||
)
|
||||
|
|
|
|||
467
odoo-bringout-oca-ocb-mail/mail/wizard/mail_activity_schedule.py
Normal file
467
odoo-bringout-oca-ocb-mail/mail/wizard/mail_activity_schedule.py
Normal 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))
|
||||
|
|
@ -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")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
|
|
@ -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 & 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue