[IMP] mail: revamp the activity type form view

* Reorganize and reword some of the fields for the activity form type view

* The fields `force_next` from `mail.activity.type` and its related
  field from `mail.activity` are removed.
  Instead, a new field `chaining_type`, which is a `selection` will
  improve the readability of the activity_type form. (It is made more
  obvious that the user has to choose between 2 modes :
    - 'Trigger Next Activity': used when the user wants to specify the type of the
      next activity, which will be triggered once the current activity is done
    - 'Suggest Next Activity': used when the user wants to recommend the
      next activity for the user to schedule once the current activity is done

* To be consistent with this change :
    - The field `default_next_type_id` is renamed `triggered_next_type_id`
    - The field `next_type_ids` is renamed `suggested_next_type_ids`

* The field `default_description` is renamed `default_note` to better match the
  `note` field from `mail.activity`

* About the specific case of activity_type.category = 'upload_file' :
    An activity which has this type's category is automatically marked as done as soon as
    the file is uploaded. This prevents the user from choosing a "next activity type".

    As such, an activity_type with this category can only make use of the
    chaining_type = "trigger", to be part of an automated process.

    An activity_type with this chaining_type should have the
    triggered_next_type_id set (usually required in the Form).
    But, since it does not make sense to set suggested_next_type_ids in this case :
      - chaining_type will stay hidden in the Form
      - triggered_next_type_id will always be shown and is not marked as required in the Form
      - if triggered_next_type_id is not set, chaining_type will be "suggest"

Task ID : 2410217
PR : https://github.com/odoo/odoo/pull/63370
UPGRADE : https://github.com/odoo/upgrade/pull/2167
This commit is contained in:
Damien Abeloos
2021-03-03 12:12:52 +00:00
committed by Damien (abd)
parent 62ae7896f5
commit 8aa1d6ff02
11 changed files with 91 additions and 57 deletions
@@ -16,7 +16,7 @@
<attribute name="attrs">{'invisible': ['|', ('activity_category', '=', 'meeting'), ('id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//button[@name='action_done']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('activity_category', '=', 'meeting'), ('force_next', '=', True)]}</attribute>
<attribute name="attrs">{'invisible': ['|', ('activity_category', '=', 'meeting'), ('chaining_type', '=', 'trigger')]}</attribute>
</xpath>
<xpath expr="//button[@special='cancel']" position="attributes">
<attribute name="attrs">{'invisible': [('activity_category', '=', 'meeting')]}</attribute>
+1 -1
View File
@@ -2,7 +2,7 @@
{
'name': 'Discuss',
'version': '1.2',
'version': '1.3',
'category': 'Productivity/Discuss',
'sequence': 145,
'summary': 'Chat, mail gateway and private channels',
+50 -18
View File
@@ -40,7 +40,7 @@ class MailActivityType(models.Model):
active = fields.Boolean(default=True)
create_uid = fields.Many2one('res.users', index=True)
delay_count = fields.Integer(
'Scheduled Date', default=0,
'Schedule', default=0,
help='Number of days/week/month before executing the action. It allows to plan the action deadline.')
delay_unit = fields.Selection([
('days', 'days'),
@@ -48,7 +48,7 @@ class MailActivityType(models.Model):
('months', 'months')], string="Delay units", help="Unit of delay", required=True, default='days')
delay_label = fields.Char(compute='_compute_delay_label')
delay_from = fields.Selection([
('current_date', 'after validation date'),
('current_date', 'after completion date'),
('previous_activity', 'after previous activity deadline')], string="Delay Type", help="Type of delay", required=True, default='previous_activity')
icon = fields.Char('Icon', help="Font awesome icon e.g. fa-tasks")
decoration_type = fields.Selection([
@@ -60,24 +60,30 @@ class MailActivityType(models.Model):
domain=['&', ('is_mail_thread', '=', True), ('transient', '=', False)],
help='Specify a model if the activity should be specific to a model'
' and not available when managing activities for other models.')
default_next_type_id = fields.Many2one('mail.activity.type', 'Default Next Activity',
domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict')
force_next = fields.Boolean("Trigger Next Activity", default=False)
next_type_ids = fields.Many2many(
'mail.activity.type', 'mail_activity_rel', 'activity_id', 'recommended_id',
triggered_next_type_id = fields.Many2one(
'mail.activity.type', string='Trigger', compute='_compute_triggered_next_type_id',
inverse='_inverse_triggered_next_type_id', store=True, readonly=False,
domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict',
help="Automatically schedule this activity once the current one is marked as done.")
chaining_type = fields.Selection([
('suggest', 'Suggest Next Activity'), ('trigger', 'Trigger Next Activity')
], string="Chaining Type", required=True, default="suggest")
suggested_next_type_ids = fields.Many2many(
'mail.activity.type', 'mail_activity_rel', 'activity_id', 'recommended_id', string='Suggest',
domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]",
string='Recommended Next Activities')
compute='_compute_suggested_next_type_ids', inverse='_inverse_suggested_next_type_ids', store=True, readonly=False,
help="Suggest these activities once the current one is marked as done.")
previous_type_ids = fields.Many2many(
'mail.activity.type', 'mail_activity_rel', 'recommended_id', 'activity_id',
domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]",
string='Preceding Activities')
category = fields.Selection([
('default', 'None'), ('upload_file', 'Upload Document')
], default='default', string='Action to Perform',
], default='default', string='Action',
help='Actions may trigger specific behavior like opening calendar view or automatically mark as done when a document is uploaded')
mail_template_ids = fields.Many2many('mail.template', string='Email templates')
default_user_id = fields.Many2one("res.users", string="Default User")
default_description = fields.Html(string="Default Description", translate=True)
default_note = fields.Html(string="Default Note", translate=True)
#Fields for display purpose only
initial_res_model_id = fields.Many2one('ir.model', 'Initial model', compute="_compute_initial_res_model_id", store=False,
@@ -101,6 +107,32 @@ class MailActivityType(models.Model):
unit = selection_description_values[activity_type.delay_unit]
activity_type.delay_label = '%s %s' % (activity_type.delay_count, unit)
@api.depends('chaining_type')
def _compute_suggested_next_type_ids(self):
"""suggested_next_type_ids and triggered_next_type_id should be mutually exclusive"""
for activity_type in self:
if activity_type.chaining_type == 'trigger':
activity_type.suggested_next_type_ids = False
def _inverse_suggested_next_type_ids(self):
for activity_type in self:
if activity_type.suggested_next_type_ids:
activity_type.chaining_type = 'suggest'
@api.depends('chaining_type')
def _compute_triggered_next_type_id(self):
"""suggested_next_type_ids and triggered_next_type_id should be mutually exclusive"""
for activity_type in self:
if activity_type.chaining_type == 'suggest':
activity_type.triggered_next_type_id = False
def _inverse_triggered_next_type_id(self):
for activity_type in self:
if activity_type.triggered_next_type_id:
activity_type.chaining_type = 'trigger'
else:
activity_type.chaining_type = 'suggest'
class MailActivity(models.Model):
""" An actual activity to perform. Activities are linked to
@@ -179,20 +211,20 @@ class MailActivity(models.Model):
compute='_compute_has_recommended_activities',
help='Technical field for UX purpose')
mail_template_ids = fields.Many2many(related='activity_type_id.mail_template_ids', readonly=True)
force_next = fields.Boolean(related='activity_type_id.force_next', readonly=True)
chaining_type = fields.Selection(related='activity_type_id.chaining_type', readonly=True)
# access
can_write = fields.Boolean(compute='_compute_can_write', help='Technical field to hide buttons if the current user has no access.')
@api.onchange('previous_activity_type_id')
def _compute_has_recommended_activities(self):
for record in self:
record.has_recommended_activities = bool(record.previous_activity_type_id.next_type_ids)
record.has_recommended_activities = bool(record.previous_activity_type_id.suggested_next_type_ids)
@api.onchange('previous_activity_type_id')
def _onchange_previous_activity_type_id(self):
for record in self:
if record.previous_activity_type_id.default_next_type_id:
record.activity_type_id = record.previous_activity_type_id.default_next_type_id
if record.previous_activity_type_id.triggered_next_type_id:
record.activity_type_id = record.previous_activity_type_id.triggered_next_type_id
@api.depends('res_model', 'res_id')
def _compute_res_name(self):
@@ -237,8 +269,8 @@ class MailActivity(models.Model):
self.summary = self.activity_type_id.summary
self.date_deadline = self._calculate_date_deadline(self.activity_type_id)
self.user_id = self.activity_type_id.default_user_id or self.env.user
if self.activity_type_id.default_description:
self.note = self.activity_type_id.default_description
if self.activity_type_id.default_note:
self.note = self.activity_type_id.default_note
def _calculate_date_deadline(self, activity_type):
# Date.context_today is correct because date_deadline is a Date and is meant to be
@@ -521,7 +553,7 @@ class MailActivity(models.Model):
for activity in self:
# extract value to generate next activities
if activity.force_next:
if activity.chaining_type == 'trigger':
Activity = self.env['mail.activity'].with_context(activity_previous_deadline=activity.date_deadline) # context key is required in the onchange to set deadline
vals = Activity.default_get(Activity.fields_get())
@@ -929,7 +961,7 @@ class MailActivityMixin(models.AbstractModel):
'activity_type_id': activity_type and activity_type.id,
'summary': summary or activity_type.summary,
'automated': True,
'note': note or activity_type.default_description,
'note': note or activity_type.default_note,
'date_deadline': date_deadline,
'res_model_id': model_id,
'res_id': record.id,
@@ -7,7 +7,7 @@
<textarea class="form-control o_ActivityMarkDonePopover_feedback" rows="3" placeholder="Write Feedback" t-on-blur="_onBlur" t-ref="feedbackTextarea"/>
<div class="o_ActivityMarkDonePopover_buttons">
<button type="button" class="o_ActivityMarkDonePopover_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="_onClickDoneAndScheduleNext" t-esc="DONE_AND_SCHEDULE_NEXT"/>
<t t-if="!activity.force_next">
<t t-if="activity.chaining_type === 'suggest'">
<button type="button" class="o_ActivityMarkDonePopover_doneButton btn btn-sm btn-primary" t-on-click="_onClickDone">
Done
</button>
@@ -93,7 +93,7 @@ QUnit.test('activity with force next mark done popover simplest layout', async f
const activity = this.env.models['mail.activity'].create({
canWrite: true,
category: 'not_upload_file',
force_next: true,
chaining_type: 'trigger',
id: 12,
thread: [['insert', { id: 42, model: 'res.partner' }]],
});
+3 -3
View File
@@ -339,7 +339,7 @@ var BasicActivity = AbstractField.extend({
var $markDoneBtn = $(ev.currentTarget);
var activityID = $markDoneBtn.data('activity-id');
var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false;
var forceNextActivity = $markDoneBtn.data('force-next-activity');
var chainingTypeActivity = $markDoneBtn.data('chaining-type-activity');
if ($markDoneBtn.data('toggle') === 'collapse') {
var $actLi = $markDoneBtn.parents('.o_log_activity');
@@ -348,7 +348,7 @@ var BasicActivity = AbstractField.extend({
if (!$panel.data('bs.collapse')) {
var $form = $(QWeb.render('mail.activity_feedback_form', {
previous_activity_type_id: previousActivityTypeID,
force_next: forceNextActivity
chaining_type: chainingTypeActivity
}));
$panel.append($form);
self._onMarkActivityDoneActions($markDoneBtn, $form, activityID);
@@ -388,7 +388,7 @@ var BasicActivity = AbstractField.extend({
content: function () {
var $popover = $(QWeb.render('mail.activity_feedback_form', {
previous_activity_type_id: previousActivityTypeID,
force_next: forceNextActivity
chaining_type: chainingTypeActivity
}));
self._onMarkActivityDoneActions($markDoneBtn, $popover, activityID);
return $popover;
@@ -49,8 +49,8 @@ function factory(dependencies) {
if ('date_deadline' in data) {
data2.dateDeadline = data.date_deadline;
}
if ('force_next' in data) {
data2.force_next = data.force_next;
if ('chaining_type' in data) {
data2.chaining_type = data.chaining_type;
}
if ('icon' in data) {
data2.icon = data.icon;
@@ -289,8 +289,8 @@ function factory(dependencies) {
* In all other cases, this field value should not be trusted.
*/
feedbackBackup: attr(),
force_next: attr({
default: false,
chaining_type: attr({
default: 'suggest',
}),
icon: attr(),
id: attr({
+3 -3
View File
@@ -84,11 +84,11 @@
</t>
<t t-name="mail.activity_thread_message_tools">
<div t-if="activity.can_write" class="o_thread_message_tools btn-group">
<span t-if="activity.activity_category !== 'upload_file'" class="o_mark_as_done" data-toggle="popover" t-att-data-activity-id="activity.id" t-att-data-force-next-activity="activity.force_next" t-att-data-previous-activity-type-id="activity.activity_type_id[0]">
<span t-if="activity.activity_category !== 'upload_file'" class="o_mark_as_done" data-toggle="popover" t-att-data-activity-id="activity.id" t-att-data-chaining-type-activity="activity.chaining_type" t-att-data-previous-activity-type-id="activity.activity_type_id[0]">
<a role="button" href="#" class="btn btn-link btn-success text-muted o_activity_link mr8">
<i class="fa fa-check"/> Mark Done </a>
</span>
<span t-if="activity.activity_category === 'upload_file'" class="o_mark_as_done_upload_file" t-att-data-activity-id="activity.id" t-att-data-force-next-activity="activity.force_next" t-att-data-previous-activity-type-id="activity.activity_type_id[0]" t-att-data-fileupload-id="activity.fileuploadID">
<span t-if="activity.activity_category === 'upload_file'" class="o_mark_as_done_upload_file" t-att-data-activity-id="activity.id" t-att-data-chaining-type-activity="activity.chaining_type" t-att-data-previous-activity-type-id="activity.activity_type_id[0]" t-att-data-fileupload-id="activity.fileuploadID">
<a role="button" href="#" class="btn btn-link btn-success text-muted o_activity_link mr8">
<i class="fa fa-upload"/> Upload Document </a>
</span>
@@ -112,7 +112,7 @@
<div>
<textarea class="form-control" rows="3" id="activity_feedback" placeholder="Write Feedback"/>
<div class="mt8">
<t t-if="!force_next">
<t t-if="chaining_type === 'suggest'">
<button type="button" class="btn btn-sm btn-primary o_activity_popover_done_next" t-att-data-previous-activity-type-id="previous_activity_type_id">
Done &amp; Schedule Next</button>
<button type="button" class="btn btn-sm btn-primary o_activity_popover_done">
@@ -39,7 +39,7 @@
</li>
<t t-foreach="logs" t-as="log">
<t t-set="edit_class" t-value="'o_edit_activity'"/>
<t t-if="log.force_next">
<t t-if="log.chaining_type === 'trigger'">
<t t-set="edit_class" t-value=""/>
</t>
<t class="activities_list_group_item">
@@ -61,10 +61,10 @@
<t t-name="mail.activities-list-group-item">
<li t-attf-class="list-group-item o_log_activity d-flex #{log_last ? 'border-bottom' : ''}" role="menuitem">
<div t-attf-class="o_activity_title o_log_activity #{edit_class}" t-att-data-activity-id="log.id">
<div t-attf-class="o_activity_title_entry o_mail_activity {{! log.force_next ? 'align-items-center' : 'mb-1'}}">
<div t-attf-class="o_activity_title_entry o_mail_activity {{ log.chaining_type === 'suggest' ? 'align-items-center' : 'mb-1'}}">
<span t-attf-class="fa #{log.icon ? log.icon : 'fa-bell' } fa-fw mr-2 text-center text-muted" role="img" aria-label="Log" title="Log"/>
<strong class="text-dark o_activity_summary"><t t-esc="log.title_action or log.summary or log.activity_type_id[1]"/></strong>
<button t-if="! log.force_next and log.can_write" class="btn btn-sm btn-link py-0 o_edit_button"><i class="fa fa-pencil"/></button>
<button t-if="log.chaining_type === 'suggest' and log.can_write" class="btn btn-sm btn-link py-0 o_edit_button"><i class="fa fa-pencil"/></button>
</div>
<div class="o_activity_title_entry mt-1" t-if="log.state != 'today'">
<span class="fa fa-clock-o fa-fw mr-2 text-center text-muted" role="img" aria-label="Deadline" title="Deadline"/>
@@ -72,7 +72,7 @@
<small t-att-title="log.date_deadline"><t t-esc="log.label_delay" /></small>
</div>
<t t-if="log.mail_template_ids">
<div t-foreach="log.mail_template_ids" t-as="mail_template" class="o_activity_title_entry mt-2" t-att-data-activity-id="log.id" t-att-data-force-next-activity="log.force_next" t-att-data-previous-activity-type-id="log.activity_type_id[0]">
<div t-foreach="log.mail_template_ids" t-as="mail_template" class="o_activity_title_entry mt-2" t-att-data-activity-id="log.id" t-att-data-chaining-type-activity="log.chaining_type" t-att-data-previous-activity-type-id="log.activity_type_id[0]">
<i class="fa fa-envelope-o fa-fw mr-2 text-center text-muted" aria-label="Mail" title="Mail" role="img"></i>
<small>
<div class="mb-1" t-esc="mail_template.name + ':'"/>
@@ -85,7 +85,7 @@
</div>
<div t-if="log.can_write" class="flex-grow-1 text-right">
<t t-if="log.activity_category === 'upload_file'">
<a t-att-data-force-next-activity="log.force_next"
<a t-att-data-chaining-type-activity="log.chaining_type"
t-att-data-previous-activity-type-id="log.activity_type_id[0]"
t-att-data-activity-id="log.id"
class="o_mark_as_done_upload_file o_activity_link o_activity_link_kanban fa fa-upload"
@@ -100,7 +100,7 @@
</span>
</t>
<t t-else="">
<a t-att-data-force-next-activity="log.force_next"
<a t-att-data-chaining-type-activity="log.chaining_type"
t-att-data-previous-activity-type-id="log.activity_type_id[0]"
t-att-data-activity-id="log.id"
t-attf-href="#o_mark_done_form{{log.id}}"
@@ -40,6 +40,7 @@ class MockModels {
activity_category: { string: "Category", type: 'selection', selection: [['default', 'Other'], ['upload_file', 'Upload File']] },
activity_type_id: { string: "Activity type", type: "many2one", relation: "mail.activity.type" },
can_write: { string: "Can write", type: "boolean" },
chaining_type: { string: 'Chaining Type', type: 'selection', selection: [['suggest', 'Suggest Next Activity'], ['trigger', 'Trigger Next Activity']], default: 'suggest' },
create_uid: { string: "Created By", type: "many2one", relation: 'res.users' },
display_name: { string: "Display name", type: "char" },
date_deadline: { string: "Due Date", type: "date", default() { return moment().format('YYYY-MM-DD'); } },
@@ -54,6 +55,7 @@ class MockModels {
},
'mail.activity.type': {
fields: {
chaining_type: { string: 'Chaining Type', type: 'selection', selection: [['suggest', 'Suggest Next Activity'], ['trigger', 'Trigger Next Activity']], default: 'suggest' },
category: { string: 'Category', type: 'selection', selection: [['default', 'Other'], ['upload_file', 'Upload File']] },
decoration_type: { string: "Decoration Type", type: "selection", selection: [['warning', 'Alert'], ['danger', 'Error']] },
icon: { string: 'icon', type: "char" },
+19 -19
View File
@@ -8,11 +8,11 @@
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Follow-up Call"/></h1>
<label for="name" class="oe_edit_only"/>
<h1><field name="name" placeholder="e.g. Schedule a meeting"/></h1>
</div>
<group>
<group name="activity_details">
<group name="activity_details" string="Activity Settings">
<field name="active" invisible="1"/>
<field name="category"/>
<field name="default_user_id" options="{'no_create': True, 'no_edit': True}" domain="[('share', '=', False)]"/>
@@ -23,11 +23,13 @@
<field name="icon" groups="base.group_no_one"/>
<field name="decoration_type" groups="base.group_no_one"/>
</group>
<group name="activity_planning">
<field name="force_next"/>
<field name="default_next_type_id" attrs="{'required': [('force_next', '=', True)]}" context="{'default_res_model_id': res_model_id}"/>
<field name="next_type_ids" widget="many2many_tags"
attrs="{'invisible': ['|', ('force_next', '=', True), ('category', '=', 'upload_file')]}" context="{'default_res_model_id': res_model_id}"/>
<group name="activity_planning" string="Next Activity">
<field name="chaining_type" attrs="{'invisible': [('category', '=', 'upload_file')]}"/>
<field name="triggered_next_type_id" options="{'no_open': True}" context="{'default_res_model_id': res_model_id}"
attrs="{'required': ['&amp;', ('chaining_type', '=', 'trigger'), ('category', '!=', 'upload_file')],
'invisible': ['&amp;', ('chaining_type', '=', 'suggest'), ('category', '!=', 'upload_file')]}"/>
<field name="suggested_next_type_ids" widget="many2many_tags" context="{'default_res_model_id': res_model_id}"
attrs="{'invisible': ['|', ('chaining_type', '=', 'trigger'), ('category', '=', 'upload_file')]}"/>
<field name="mail_template_ids" widget="many2many_tags"
domain="[('model_id', '=', res_model_id)]"
attrs="{'invisible': [('res_model_id', '=', False)]}"
@@ -35,17 +37,15 @@
<label for="delay_count"/>
<div>
<div class="o_row">
<field name="delay_count"/>
<field name="delay_unit"/>
<field class="col-1 pl-0 pr-0" name="delay_count"/>
<field class="col-3 pl-1 pr-1" name="delay_unit"/>
<field name="delay_from"/>
</div>
<span class="oe_read_only">&amp;nbsp;</span>
<field name="delay_from"/>
</div>
</group>
</group>
<group>
<field name="default_description"/>
</group>
<label for="default_note"/>
<field name="default_note"/>
<p class="alert alert-info" role="alert" attrs="{'invisible': [('res_model_change', '=', False)]}">Modifying the model can have an impact on existing activities using this activity type, be careful.</p>
</sheet>
</form>
@@ -104,7 +104,7 @@
<field name="res_model" invisible="1"/>
<field name="res_model_id" invisible="1"/>
<field name="res_id" invisible="1" widget="integer"/>
<field name="force_next" invisible="1"/>
<field name="chaining_type" invisible="1"/>
<field name="previous_activity_type_id"/>
<field name="has_recommended_activities"/>
</group>
@@ -133,13 +133,13 @@
attrs="{'invisible': [('id', '!=', False)]}"/>
<button string="Save" name="action_close_dialog" type="object" class="btn-primary"
attrs="{'invisible': [('id', '=', False)]}"/>
<button attrs="{'invisible': [('force_next', '=', True)]}" string="Mark as Done" name="action_done"
<button attrs="{'invisible': [('chaining_type', '=', 'trigger')]}" string="Mark as Done" name="action_done"
type="object" class="btn-secondary"
context="{'mail_activity_quick_update': True}"/>
<button attrs="{'invisible': [('force_next', '=', True)]}" string="Done &amp; Schedule Next" name="action_done_schedule_next"
<button attrs="{'invisible': [('chaining_type', '=', 'trigger')]}" string="Done &amp; Schedule Next" name="action_done_schedule_next"
type="object" class="btn-secondary"
context="{'mail_activity_quick_update': True}"/>
<button attrs="{'invisible': [('force_next', '=', False)]}" string="Done &amp; Launch Next" name="action_done_schedule_next"
<button attrs="{'invisible': [('chaining_type', '=', 'suggest')]}" string="Done &amp; Launch Next" name="action_done_schedule_next"
type="object" class="btn-secondary"
context="{'mail_activity_quick_update': True}"/>
<button string="Discard" class="btn-secondary" special="cancel" />