[IMP] mail: allow users to manage own templates

Before this commit, all mail templates were shared, which was cluttering the UI
for everyone.

Now, each user can have their own templates that they can edit and save. Access
is done through the mail composer wizard, where users can only access their own
templates and templates that don't belong to anyone.

Some groups are considered as admins and can access all templates in
Settings/Technical/Email/Email Templates:

- Sales Admin
- Project Admins
- Helpdesk Admins
- Accountants
- Event Admins
- Recruitment Admins

Task-2504439

Part-of: odoo/odoo#126049
This commit is contained in:
miad-odoo
2023-09-29 13:11:50 +00:00
parent cdbd8d038d
commit 39dd2fc2ab
10 changed files with 169 additions and 74 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
<record id="group_event_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="base.module_category_marketing_events"/>
<field name="implied_ids" eval="[(4, ref('group_event_user'))]"/>
<field name="implied_ids" eval="[(4, ref('group_event_user')), (4, ref('mail.group_mail_template_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
@@ -28,7 +28,7 @@
<record id="group_hr_recruitment_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="base.module_category_human_resources_recruitment"/>
<field name="implied_ids" eval="[(4, ref('group_hr_recruitment_user'))]"/>
<field name="implied_ids" eval="[(4, ref('group_hr_recruitment_user')), (4, ref('mail.group_mail_template_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
+29 -1
View File
@@ -18,7 +18,7 @@ class MailTemplate(models.Model):
_name = "mail.template"
_inherit = ['mail.render.mixin', 'template.reset.mixin']
_description = 'Email Templates'
_order = 'name'
_order = 'user_id,name,id'
_unrestricted_rendering = True
@@ -46,6 +46,7 @@ class MailTemplate(models.Model):
email_from = fields.Char('From',
help="Sender address (placeholders may be used here). If not set, the default "
"value will be the author's email alias if configured, or email address.")
user_id = fields.Many2one('res.users', string='User', domain="[('share', '=', False)]", help='The template belongs to this user')
# recipients
use_default_to = fields.Boolean(
'Default recipients',
@@ -88,6 +89,7 @@ class MailTemplate(models.Model):
# access
can_write = fields.Boolean(compute='_compute_can_write',
help='The current user can edit the template.')
is_template_editor = fields.Boolean(compute="_compute_is_template_editor")
# Overrides of mail.render.mixin
@api.depends('model')
@@ -101,6 +103,10 @@ class MailTemplate(models.Model):
for template in self:
template.can_write = template in writable_templates
@api.depends_context('uid')
def _compute_is_template_editor(self):
self.is_template_editor = self.user_has_groups('mail.group_mail_template_editor')
@api.depends('active', 'description')
def _compute_template_category(self):
""" Base templates (or master templates) are active templates having
@@ -166,10 +172,32 @@ class MailTemplate(models.Model):
self._fix_attachment_ownership()
return True
def open_delete_confirmation_modal(self):
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_id': self.id,
'res_model': self._name,
'target': 'new',
'view_id': self.env.ref('mail.mail_template_view_form_confirm_delete').id,
'context': {'dialog_size': 'medium'},
'name': _('Confirmation'),
}
def unlink(self):
self.unlink_action()
return super(MailTemplate, self).unlink()
def cancel_unlink(self):
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_id': self.id,
'res_model': self._name,
'target': 'new',
'context': {'dialog_size': 'large'},
}
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
default = dict(default or {},
@@ -48,6 +48,7 @@
}
.o_mail_composer_form .o_form_renderer {
padding-bottom: 0px;
.oe-bordered-editor[name=body] .o_readonly {
border: 1px solid $o-gray-300;
padding: 4px;
+56 -37
View File
@@ -7,42 +7,35 @@
<field name="arch" type="xml">
<form string="Templates">
<header>
<field name="ref_ir_act_window" invisible="1"/>
<field name="template_fs" invisible="1"/>
<field name="is_template_editor" invisible="1"/>
<button string="Preview"
name="%(mail_template_preview_action)d" type="action"
class="btn-primary" target="new"/>
<button string="Reset Template"
name="%(mail_template_reset_action)d" type="action"
groups="mail.group_mail_template_editor"
invisible="not template_fs"/>
<button string="Delete"
name="open_delete_confirmation_modal" type="object"
class="btn btn-secondary"
invisible="(user_id != uid and not is_template_editor) or template_fs"
help="Permanently delete this template"/>
<button string="Add Context Action"
class="btn btn-secondary"
name="create_action" type="object"
groups="base.group_no_one"
invisible="ref_ir_act_window"
help="Display an option on related documents to open a composition wizard with this template"/>
<button string="Remove Context Action"
class="btn btn-secondary"
name="unlink_action" type="object"
groups="base.group_no_one"
invisible="not ref_ir_act_window"
help="Remove the contextual action to use this template on related documents"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<field name="ref_ir_act_window" invisible="1"/>
<button class="oe_stat_button"
groups="base.group_no_one"
name="create_action" type="object"
invisible="ref_ir_act_window" icon="fa-plus"
help="Display an option on related documents to open a composition wizard with this template">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Add</span>
<span class="o_stat_text">Context Action</span>
</div>
</button>
<button name="unlink_action" type="object"
groups="base.group_no_one"
class="oe_stat_button" icon="fa-minus"
invisible="not ref_ir_act_window"
help="Remove the contextual action to use this template on related documents" widget="statinfo">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Remove</span>
<span class="o_stat_text">Context Action</span>
</div>
</button>
<button class="oe_stat_button" name="%(mail_template_preview_action)d" icon="fa-search-plus"
type="action" target="new">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Preview</span>
</div>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" class="w-100"
@@ -52,7 +45,6 @@
<field name="subject" options="{'dynamic_placeholder': true}"
placeholder='e.g. "Welcome to MyCompany" or "Nice to meet you, {{ object.name }}"'/>
<field name="model" invisible="1"/>
<field name="description"/>
</group>
</div>
<notebook>
@@ -80,13 +72,24 @@
</group>
</page>
<page string="Settings" name="email_settings">
<group>
<field name="lang" placeholder="{{ object.partner_id.lang }}"/>
<field name="mail_server_id"/>
<field name="auto_delete"/>
<field name="report_template_ids" domain="[('model','=',model)]"
widget="many2many_tags"
options="{'no_create': True}"/>
<group col="2">
<group>
<field name="lang" placeholder="{{ object.partner_id.lang }}"/>
<field name="mail_server_id"/>
<field name="auto_delete"/>
<field name="report_template_ids" domain="[('model','=',model)]"
widget="many2many_tags"
options="{'no_create': True}"/>
</group>
<group>
<field name="user_id" widget="many2one_avatar_user"
readonly="not is_template_editor"
invisible="not is_template_editor"
placeholder="If not set, shared with all users."
help="If set, will restrict the template to this specific user.
If not set, shared with all users."/>
<field name="description"/>
</group>
</group>
</page>
</notebook>
@@ -95,6 +98,20 @@
</field>
</record>
<record id="mail_template_view_form_confirm_delete" model="ir.ui.view">
<field name="name">mail.template.view.form.confirm.delete</field>
<field name="model">mail.template</field>
<field name="arch" type="xml">
<form string="Confirmation">
<p>Are you sure you want to delete this Mail Template?</p>
<footer>
<button name="unlink" type="object" string="Delete" class="btn btn-primary"/>
<button name="cancel_unlink" type="object" string="Cancel" class="btn btn-secondary"/>
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="email_template_tree">
<field name="name">email.template.tree</field>
<field name="model">mail.template</field>
@@ -103,6 +120,7 @@
<field name="mail_server_id" column_invisible="True"/>
<field name="name"/>
<field name="model_id" groups="base.group_no_one"/>
<field name="user_id" optional="show" widget="many2one_avatar_user"/>
<field name="description"/>
<field name="subject" optional="hidden"/>
<field name="email_from" optional="hidden"/>
@@ -120,6 +138,7 @@
<field name="name" filter_domain="['|', '|', ('name','ilike',self), ('subject','ilike',self), ('email_to','ilike',self)]" string="Templates"/>
<field name="lang"/>
<field name="model_id"/>
<filter name="my_templates" string="My Templates" domain="[('user_id', '=', uid)]"/>
<filter name="base_templates" string="Base Templates" domain="[('template_category', '=', 'base_template')]"/>
<filter name="custom_templates" string="Custom Templates" domain="[('template_category', '=', 'custom_template')]"/>
<group expand="0" string="Group by...">
+51 -25
View File
@@ -83,7 +83,10 @@ class MailComposer(models.TransientModel):
compute='_compute_body', readonly=False, store=True)
parent_id = fields.Many2one(
'mail.message', 'Parent Message', ondelete='set null')
template_id = fields.Many2one('mail.template', 'Use template', domain="[('model', '=', model)]")
template_id = fields.Many2one(
'mail.template', 'Use template',
domain="[('model', '=', model), '|', ('user_id','=', False), ('user_id', '=', uid)]"
)
attachment_ids = fields.Many2many(
'ir.attachment', 'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', string='Attachments',
@@ -700,32 +703,55 @@ class MailComposer(models.TransientModel):
return mails_sudo
def action_save_as_template(self):
""" hit save as template button: current form value will be a new
template attached to the current document. """
for record in self:
model = self.env['ir.model']._get(record.model or 'mail.message')
model_name = model.name or ''
template_name = "%s: %s" % (model_name, tools.ustr(record.subject))
values = {
'name': template_name,
'subject': record.subject or False,
'body_html': record.body or False,
'model_id': model.id or False,
'use_default_to': True,
}
template = self.env['mail.template'].create(values)
def open_template_creation_wizard(self):
""" hit save as template button: opens a wizard that prompts for the template's subject.
`create_mail_template` is called when saving the new wizard. """
if record.attachment_ids:
attachments = self.env['ir.attachment'].sudo().browse(record.attachment_ids.ids).filtered(
lambda a: a.res_model == 'mail.compose.message' and a.create_uid.id == self._uid)
if attachments:
attachments.write({'res_model': template._name, 'res_id': template.id})
template.attachment_ids |= record.attachment_ids
self.ensure_one()
saved_subject = self.subject
self.subject = False
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'view_id': self.env.ref('mail.mail_compose_message_view_form_template_save').id,
'name': _('Create a new Mail Template'),
'res_model': 'mail.compose.message',
'context': {'dialog_size': 'medium', 'mail_composer_saved_subject': saved_subject},
'target': 'new',
'res_id': self.id,
}
# generate the saved template
record.write({'template_id': template.id})
return _reopen(self, record.id, record.model, context=self.env.context)
def create_mail_template(self):
""" creates a mail template with the information form the current mail composer """
self.ensure_one()
model_id = self.env['ir.model']._get_id(self.model)
values = {
'name': self.subject,
'subject': self.subject,
'body_html': self.body,
'model_id': model_id,
'use_default_to': True,
'user_id': self.env.uid,
}
template = self.env['mail.template'].create(values)
if self.attachment_ids:
attachments = self.env['ir.attachment'].sudo().browse(self.attachment_ids.ids).filtered(
lambda a: a.res_model == 'mail.compose.message' and a.create_uid.id == self._uid)
if attachments:
attachments.write({'res_model': template._name, 'res_id': template.id})
template.attachment_ids = self.attachment_ids
# generate the saved template
self.write({'template_id': template.id})
return _reopen(self, self.id, self.model, context={**self.env.context, 'dialog_size': 'large'})
def cancel_save_template(self):
""" Restore old subject when canceling the 'save as template' action
as it was erased to let user give a more custom input. """
self.ensure_one()
self.subject = self.env.context.get('mail_composer_saved_subject')
return _reopen(self, self.id, self.model, context={**self.env.context, 'dialog_size': 'large'})
# ------------------------------------------------------------
# RENDERING / VALUES GENERATION
@@ -46,7 +46,9 @@
<field name="body" class="oe-bordered-editor" placeholder="Write your message here..." options="{'style-inline': true}" readonly="not can_edit_body" force_save="1"/>
<group>
<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}"
</group>
<group>
<field name="template_id" string="Load template" options="{'no_create': True}" class="w-50"
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
</group>
</div>
@@ -56,7 +58,9 @@
<field name="body" class="oe-bordered-editor" placeholder="Write your message here..." options="{'style-inline': true}" readonly="not can_edit_body" force_save="1"/>
<group>
<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}"
</group>
<group>
<field name="template_id" string="Load template" options="{'no_create': True}" class="w-50"
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
</group>
</div>
@@ -80,10 +84,26 @@
type="object" class="btn-primary" data-hotkey="q"
invisible="not subtype_is_log"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" />
<button icon="fa-lg fa-save" type="object"
name="action_save_as_template" string="Save as new template"
<button icon="fa-cloud-upload" type="object"
name="open_template_creation_wizard" string="Save Template"
invisible="not can_edit_body"
class="float-end btn-secondary" help="Save as a new template" data-hotkey="w"/>
class="float-end btn-secondary" data-hotkey="w" help="Save as a new template"/>
</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 string="Templates">
<group>
<field name="subject" placeholder="e.g. &quot;Welcome to MyCompany&quot; or &quot;Nice to meet you, {{ object.name }}&quot;"/>
</group>
<footer>
<button name="create_mail_template" type="object" class="btn btn-primary" string="Save"/>
<button name="cancel_save_template" type="object" class="btn btn-secondary" string="Cancel"/>
</footer>
</form>
</field>
+1 -1
View File
@@ -15,7 +15,7 @@
<record id="group_project_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="base.module_category_services_project"/>
<field name="implied_ids" eval="[(4, ref('group_project_user'))]"/>
<field name="implied_ids" eval="[(4, ref('group_project_user')), (4, ref('mail.group_mail_template_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
@@ -23,7 +23,8 @@
<field name="name">Administrator</field>
<field name="comment">the user will have an access to the sales configuration as well as statistic reports.</field>
<field name="category_id" ref="base.module_category_sales_sales"/>
<field name="implied_ids" eval="[(4, ref('group_sale_salesman_all_leads'))]"/>
<field name="implied_ids" eval="[(4, ref('group_sale_salesman_all_leads')),
(4, ref('mail.group_mail_template_editor'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
+2 -2
View File
@@ -1176,14 +1176,14 @@ class TestComposerInternals(TestMailComposer):
).create({
'subject': 'Template Subject',
'body': '<p>Template Body</p>',
}).action_save_as_template()
}).create_mail_template()
# Test: email_template subject, body_html, model
template = self.env['mail.template'].search([
('model', '=', self.test_record._name),
('subject', '=', 'Template Subject')
], limit=1)
self.assertEqual(template.name, "%s: %s" % (self.env['ir.model']._get(self.test_record._name).name, 'Template Subject'))
self.assertEqual(template.name, 'Template Subject')
self.assertEqual(template.body_html, '<p>Template Body</p>', 'email_template incorrect body_html')
@users('erp_manager')