[REM] mail, sms, snailmail: discard notifications without confirmation

Skip the confirmation step when wanting to get rid of a notification
for emails, sms or letters that were not sent.

Technical:
- All 3 wizards to confirm the discarding of the notification have been removed
(the ones for mail, sms and letter) have been removed
- The method to discard notification has been centralized in MailThread
(notify_cancel_by_type in addons/mail/models/mail_thread.py)
- This centralization has been done as well on the client code as the same
server code is called whether it is a mail, a sms or a letter
(in addons/mail/static/src/models/notification_group/notification_group.js)
- Existing front and back test have been adapted
- A test of notify_cancel_by_type for mail has been added
(addons/test_mail/tests/test_mail_management.py)
- add missing author_id in mail_notification demo data

Task-2752190

closes odoo/odoo#84452

Related: odoo/upgrade#3246
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Pierre-Yves Dufays
2022-04-01 17:56:20 +02:00
parent e82e12e3a4
commit eca543ef40
31 changed files with 226 additions and 345 deletions
-1
View File
@@ -13,7 +13,6 @@
'data/mail_groups.xml',
'wizard/mail_blacklist_remove_views.xml',
'wizard/mail_compose_message_views.xml',
'wizard/mail_resend_cancel_views.xml',
'wizard/mail_resend_message_views.xml',
'wizard/mail_template_preview_views.xml',
'wizard/mail_wizard_invite_views.xml',
+42
View File
@@ -2105,6 +2105,48 @@ class MailThread(models.AbstractModel):
# NOTIFICATION API
# ------------------------------------------------------
def _notify_cancel_by_type_generic(self, notification_type):
""" Standard implementation for canceling notifications by type that cancels notifications
* in 'bounce' and 'exception' status
* of the current user
* of the given type
* for mail_message related to the model implemented by this class
It also sends bus notifications to update status of notifications in the web client.
"""
author_id = self.env.user.partner_id.id
self._cr.execute("""
SELECT notif.id, msg.id
FROM mail_notification notif
JOIN mail_message msg ON notif.mail_message_id = msg.id
WHERE notif.notification_type = %(notification_type)s
AND notif.author_id = %(author_id)s
AND notif.notification_status IN ('bounce', 'exception')
AND msg.model = %(model_name)s
""", {'model_name': self._name, 'author_id': author_id, 'notification_type': notification_type})
records = self._cr.fetchall()
if records:
notif_ids, msg_ids = zip(*records)
msg_ids = list(set(msg_ids))
if notif_ids:
self.env['mail.notification'].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
if msg_ids:
self.env['mail.message'].browse(msg_ids)._notify_message_notification_update()
return True
@api.model
def notify_cancel_by_type(self, notification_type):
""" Subclasses must call this method and then
* either call the standard implementation _notify_cancel_by_type_generic
* or implements their own logic
"""
if not self.env.user.has_group('base.group_user'):
raise exceptions.AccessError(_("Access Denied"))
self.check_access_rights('read')
if notification_type == 'email':
self._notify_cancel_by_type_generic('email')
return True
def _notify_thread(self, message, msg_vals=False, **kwargs):
""" Main notification method. This method basically does two things
-1
View File
@@ -45,7 +45,6 @@ access_mail_blacklist_system,access_mail_blacklist_system,model_mail_blacklist,b
access_mail_wizard_invite,access.mail.wizard.invite,model_mail_wizard_invite,base.group_user,1,1,1,0
access_mail_compose_message,access.mail.compose.message,model_mail_compose_message,base.group_user,1,1,1,0
access_mail_compose_message_portal,access.mail.compose.message.portal,model_mail_compose_message,base.group_portal,1,1,1,0
access_mail_resend_cancel,access.mail.resend.cancel,model_mail_resend_cancel,base.group_user,1,1,1,0
access_mail_resend_message,access.mail.resend.message,model_mail_resend_message,base.group_user,1,1,1,0
access_mail_resend_partner,access.mail.resend.partner,model_mail_resend_partner,base.group_user,1,1,1,0
access_mail_template_preview,access.mail.template.preview,model_mail_template_preview,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
45 access_mail_wizard_invite access.mail.wizard.invite model_mail_wizard_invite base.group_user 1 1 1 0
46 access_mail_compose_message access.mail.compose.message model_mail_compose_message base.group_user 1 1 1 0
47 access_mail_compose_message_portal access.mail.compose.message.portal model_mail_compose_message base.group_portal 1 1 1 0
access_mail_resend_cancel access.mail.resend.cancel model_mail_resend_cancel base.group_user 1 1 1 0
48 access_mail_resend_message access.mail.resend.message model_mail_resend_message base.group_user 1 1 1 0
49 access_mail_resend_partner access.mail.resend.partner model_mail_resend_partner base.group_user 1 1 1 0
50 access_mail_template_preview access.mail.template.preview model_mail_template_preview base.group_user 1 1 1 0
@@ -63,10 +63,7 @@ export class NotificationGroup extends Component {
* @param {MouseEvent} ev
*/
_onClickMarkAsRead(ev) {
this.notificationGroupView.notificationGroup.openCancelAction();
if (!this.messaging.device.isMobile) {
this.messaging.messagingMenu.close();
}
this.notificationGroupView.notificationGroup.notifyCancel();
}
}
@@ -10,20 +10,13 @@ registerModel({
identifyingFields: ['res_model', 'res_id', 'notification_type'],
recordMethods: {
/**
* Opens the view that allows to cancel all notifications of the group.
* Cancel notifications of the group.
*/
openCancelAction() {
if (this.notification_type !== 'email') {
return;
}
this.env.bus.trigger('do-action', {
action: 'mail.mail_resend_cancel_action',
options: {
additional_context: {
default_model: this.res_model,
unread_counter: this.notifications.length,
},
},
notifyCancel() {
this.env.services.rpc({
model: this.res_model,
method: 'notify_cancel_by_type',
kwargs: { notification_type: this.notification_type },
});
},
/**
@@ -301,6 +301,9 @@ MockServer.include({
delete kwargs.context;
return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
}
if (args.method === 'notify_cancel_by_type') {
return this._mockMailThreadNotifyCancelByType(args.model, args.kwargs.notification_type);
}
return this._super(route, args);
},
/**
@@ -2364,4 +2367,31 @@ MockServer.include({
starred_counter: this._getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length,
};
},
/**
* Simulate the `notify_cancel_by_type` on `mail.thread` .
* Note that this method is overridden by snailmail module but not simulated here.
*/
_mockMailThreadNotifyCancelByType(model, notificationType) {
// Query matching notifications
const notifications = this._getRecords('mail.notification', [
['notification_type', '=', notificationType],
['notification_status', 'in', ['bounce', 'exception']],
]).filter(notification => {
const message = this._getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
return message.model === model && message.author_id === this.currentPartnerId;
});
// Update notification status
this._mockWrite('mail.notification', [
notifications.map(notification => notification.id),
{ notification_status: 'canceled' },
]);
// Send bus notifications to update status of notifications in the web client
this._widget.call('bus_service', 'trigger', 'notification', [{
payload: {
elements: this._mockMailMessage_MessageNotificationFormat(
notifications.map(notification => notification.mail_message_id)),
},
type: 'mail.message/notification_update',
}]);
},
});
@@ -86,7 +86,7 @@ QUnit.test('notification group basic layout', async function (assert) {
});
QUnit.test('mark as read', async function (assert) {
assert.expect(6);
assert.expect(2);
const pyEnv = await startServer();
const mailChannelId1 = pyEnv['mail.channel'].create();
@@ -100,28 +100,9 @@ QUnit.test('mark as read', async function (assert) {
pyEnv['mail.notification'].create({
mail_message_id: mailMessageId1, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'email', // expected failure type for email message
notification_type: 'email',
});
const bus = new Bus();
bus.on('do-action', null, payload => {
assert.step('do_action');
assert.strictEqual(
payload.action,
'mail.mail_resend_cancel_action',
"action should be the one to cancel email"
);
assert.strictEqual(
payload.options.additional_context.default_model,
'mail.channel',
"action should have the group model as default_model"
);
assert.strictEqual(
payload.options.additional_context.unread_counter,
1,
"action should have the group notification length as unread_counter"
);
});
const { createNotificationListComponent } = await start({ env: { bus } });
const { createNotificationListComponent } = await start();
await createNotificationListComponent();
assert.containsOnce(
document.body,
@@ -129,10 +110,13 @@ QUnit.test('mark as read', async function (assert) {
"should have 1 mark as read button"
);
document.querySelector('.o_NotificationGroup_markAsRead').click();
assert.verifySteps(
['do_action'],
"should do an action to display the cancel email dialog"
await afterNextRender(() => {
document.querySelector('.o_NotificationGroup_markAsRead').click();
});
assert.containsNone(
document.body,
'.o_NotificationGroup',
"should have no notification group"
);
});
-1
View File
@@ -5,7 +5,6 @@ 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_cancel
from . import mail_resend_message
from . import mail_template_preview
from . import mail_wizard_invite
-38
View File
@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class MailResendCancel(models.TransientModel):
_name = 'mail.resend.cancel'
_description = 'Dismiss notification for resend by model'
model = fields.Char(string='Model')
help_message = fields.Char(string='Help message', compute='_compute_help_message')
@api.depends('model')
def _compute_help_message(self):
for wizard in self:
wizard.help_message = _("Are you sure you want to discard %s mail delivery failures? You won't be able to re-send these mails later!") % (wizard._context.get('unread_counter'))
def cancel_resend_action(self):
author_id = self.env.user.partner_id.id
for wizard in self:
self._cr.execute("""
SELECT notif.id, mes.id
FROM mail_notification notif
JOIN mail_message mes
ON notif.mail_message_id = mes.id
WHERE notif.notification_type = 'email'
AND notif.author_id = %(author_id)s
AND notif.notification_status IN ('bounce', 'exception')
AND mes.model = %(model_name)s
""", {'model_name': wizard.model, 'author_id': author_id})
res = self._cr.fetchall()
notif_ids = [row[0] for row in res]
messages_ids = list(set([row[1] for row in res]))
if notif_ids:
self.env["mail.notification"].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
self.env["mail.message"].browse(messages_ids)._notify_message_notification_update()
return {'type': 'ir.actions.act_window_close'}
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="mail_resend_cancel_view_form" model="ir.ui.view">
<field name="name">mail.resend.cancel.view.form</field>
<field name="model">mail.resend.cancel</field>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Cancel notification in failure">
<field name="model" invisible='1'/>
<field name="help_message"/>
<p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message.</p>
<img src="/mail/static/img/red_envelope.png" alt="Envelope Example"/>
<footer>
<button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="mail_resend_cancel_action" model="ir.actions.act_window">
<field name="name">Discard mail delivery failures</field>
<field name="res_model">mail.resend.cancel</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data></odoo>
+1 -2
View File
@@ -3,7 +3,7 @@
{
'name': 'SMS gateway',
'version': '2.2',
'version': '2.3',
'category': 'Hidden/Tools',
'summary': 'SMS Text Messaging',
'description': """
@@ -20,7 +20,6 @@ The service is provided by the In App Purchase Odoo platform.
],
'data': [
'data/ir_cron_data.xml',
'wizard/sms_cancel_views.xml',
'wizard/sms_composer_views.xml',
'wizard/sms_template_preview_views.xml',
'wizard/sms_resend_views.xml',
+4
View File
@@ -19,6 +19,7 @@
<field name="date" eval="(DateTime.today() - timedelta(days=4)).strftime('%Y-%m-%d %H:%M:00')"/>
</record>
<record id="message_demo_partner_1_2_notif_0" model="mail.notification">
<field name="author_id" ref="base.partner_admin"/>
<field name="mail_message_id" ref="message_demo_partner_1_1"/>
<field name="res_partner_id" ref="base.res_partner_address_28"/>
<field name="notification_type">email</field>
@@ -45,6 +46,7 @@
<field name="date" eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:00')"/>
</record>
<record id="message_demo_partner_1_3_notif_0" model="mail.notification">
<field name="author_id" ref="base.partner_admin"/>
<field name="mail_message_id" ref="message_demo_partner_1_3"/>
<field name="res_partner_id" ref="base.res_partner_address_28"/>
<field name="notification_type">sms</field>
@@ -61,6 +63,7 @@
<field name="date" eval="(DateTime.today() - timedelta(days=1,hours=22)).strftime('%Y-%m-%d %H:%M:00')"/>
</record>
<record id="message_demo_partner_1_4_notif_0" model="mail.notification">
<field name="author_id" ref="base.partner_admin"/>
<field name="mail_message_id" ref="message_demo_partner_1_4"/>
<field name="res_partner_id" ref="base.res_partner_address_28"/>
<field name="notification_type">sms</field>
@@ -76,6 +79,7 @@
<field name="date" eval="(DateTime.today() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:00')"/>
</record>
<record id="message_demo_partner_1_5_notif_0" model="mail.notification">
<field name="author_id" ref="base.partner_admin"/>
<field name="mail_message_id" ref="message_demo_partner_1_5"/>
<field name="res_partner_id" ref="base.res_partner_address_16"/>
<field name="notification_type">sms</field>
+8
View File
@@ -362,3 +362,11 @@ class MailThread(models.AbstractModel):
sms_all.filtered(lambda sms: sms.state == 'outgoing').send(auto_commit=False, raise_exception=False)
return True
@api.model
def notify_cancel_by_type(self, notification_type):
super().notify_cancel_by_type(notification_type)
if notification_type == 'sms':
# TDE CHECK: delete pending SMS
self._notify_cancel_by_type_generic('sms')
return True
-1
View File
@@ -4,7 +4,6 @@ access_sms_sms_system,access.sms.sms.system,model_sms_sms,base.group_system,1,1,
access_sms_template_all,access.sms.template.all,model_sms_template,,0,0,0,0
access_sms_template_user,access.sms.template.user,model_sms_template,base.group_user,1,0,0,0
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,1
access_sms_cancel,access.sms.cancel,model_sms_cancel,base.group_user,1,1,1,0
access_sms_composer,access.sms.composer,model_sms_composer,base.group_user,1,1,1,0
access_sms_resend_recipient,access.sms.resend.recipient,model_sms_resend_recipient,base.group_user,1,1,1,0
access_sms_resend,access.sms.resend,model_sms_resend,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_sms_template_all access.sms.template.all model_sms_template 0 0 0 0
5 access_sms_template_user access.sms.template.user model_sms_template base.group_user 1 0 0 0
6 access_sms_template_system access.sms.template.system model_sms_template base.group_system 1 1 1 1
access_sms_cancel access.sms.cancel model_sms_cancel base.group_user 1 1 1 0
7 access_sms_composer access.sms.composer model_sms_composer base.group_user 1 1 1 0
8 access_sms_resend_recipient access.sms.resend.recipient model_sms_resend_recipient base.group_user 1 1 1 0
9 access_sms_resend access.sms.resend model_sms_resend base.group_user 1 1 1 0
@@ -5,23 +5,6 @@ import { patchRecordMethods } from '@mail/model/model_core';
import '@mail/models/notification_group';
patchRecordMethods('NotificationGroup', {
/**
* @override
*/
openCancelAction() {
if (this.notification_type !== 'sms') {
return this._super(...arguments);
}
this.env.bus.trigger('do-action', {
action: 'sms.sms_cancel_action',
options: {
additional_context: {
default_model: this.res_model,
unread_counter: this.notifications.length,
},
},
});
},
/**
* @override
*/
@@ -14,15 +14,16 @@ QUnit.module('notification_list_notification_group_tests.js', {
});
QUnit.test('mark as read', async function (assert) {
assert.expect(6);
assert.expect(2);
this.data['mail.message'].records.push(
// message that is expected to have a failure
{
id: 11, // random unique id, will be used to link failure to message
message_type: 'sms', // message must be sms (goal of the test)
model: 'mail.channel', // expected value to link message to channel
res_id: 31, // id of a random channel
author_id: this.data.currentPartnerId,
id: 11,
message_type: 'sms',
model: 'mail.channel',
res_id: 31,
}
);
this.data['mail.notification'].records.push(
@@ -30,42 +31,24 @@ QUnit.test('mark as read', async function (assert) {
{
mail_message_id: 11, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'sms', // expected failure type for sms message
notification_type: 'sms',
}
);
const bus = new Bus();
bus.on('do-action', null, payload => {
assert.step('do_action');
assert.strictEqual(
payload.action,
'sms.sms_cancel_action',
"action should be the one to cancel sms"
);
assert.strictEqual(
payload.options.additional_context.default_model,
'mail.channel',
"action should have the group model as default_model"
);
assert.strictEqual(
payload.options.additional_context.unread_counter,
1,
"action should have the group notification length as unread_counter"
);
});
const { createNotificationListComponent } = await start({ data: this.data, env: { bus } });
const { afterNextRender, createNotificationListComponent } = await start({ data: this.data });
await createNotificationListComponent();
assert.containsOnce(
document.body,
'.o_NotificationGroup_markAsRead',
"should have 1 mark as read button"
);
document.querySelector('.o_NotificationGroup_markAsRead').click();
assert.verifySteps(
['do_action'],
"should do an action to display the cancel sms dialog"
await afterNextRender(() => {
document.querySelector('.o_NotificationGroup_markAsRead').click();
});
assert.containsNone(
document.body,
'.o_NotificationGroup',
"should have no notification group"
);
});
-1
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from . import sms_cancel
from . import sms_composer
from . import sms_resend
from . import sms_template_preview
-38
View File
@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class SMSCancel(models.TransientModel):
_name = 'sms.cancel'
_description = 'Dismiss notification for resend by model'
model = fields.Char(string='Model', required=True)
help_message = fields.Char(string='Help message', compute='_compute_help_message')
@api.depends('model')
def _compute_help_message(self):
for wizard in self:
wizard.help_message = _("Are you sure you want to discard %s SMS delivery failures? You won't be able to re-send these SMS later!") % (wizard._context.get('unread_counter'))
def action_cancel(self):
# TDE CHECK: delete pending SMS
author_id = self.env.user.partner_id.id
for wizard in self:
self._cr.execute("""
SELECT notif.id, msg.id
FROM mail_notification notif
JOIN mail_message msg
ON notif.mail_message_id = msg.id
WHERE notif.notification_type = 'sms' IS TRUE AND notif.notification_status IN ('bounce', 'exception')
AND msg.model = %s
AND msg.author_id = %s """, (wizard.model, author_id))
res = self._cr.fetchall()
notif_ids = [row[0] for row in res]
message_ids = list(set([row[1] for row in res]))
if notif_ids:
self.env['mail.notification'].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
if message_ids:
self.env['mail.message'].browse(message_ids)._notify_message_notification_update()
return {'type': 'ir.actions.act_window_close'}
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="sms_cancel" model="ir.ui.view">
<field name="name">sms.cancel.form</field>
<field name="model">sms.cancel</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Cancel notification in failure">
<field name="model" invisible='1'/>
<field name="help_message"/>
<p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red icon next to each message.</p>
<footer>
<button string="Discard delivery failures" name="action_cancel" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="sms_cancel_action" model="ir.actions.act_window">
<field name="name">Discard SMS delivery failures</field>
<field name="res_model">sms.cancel</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data></odoo>
+1 -2
View File
@@ -6,7 +6,7 @@ Allows users to send documents by post
=====================================================
""",
'category': 'Hidden/Tools',
'version': '0.3',
'version': '0.4',
'depends': [
'iap_mail',
'mail'
@@ -16,7 +16,6 @@ Allows users to send documents by post
'views/report_assets.xml',
'views/snailmail_views.xml',
'wizard/snailmail_confirm_views.xml',
'wizard/snailmail_letter_cancel_views.xml',
'wizard/snailmail_letter_format_error_views.xml',
'wizard/snailmail_letter_missing_required_fields_views.xml',
'security/ir.model.access.csv',
+1
View File
@@ -8,3 +8,4 @@ from . import res_company
from . import res_config_settings
from . import res_partner
from . import snailmail_letter
from . import mail_thread
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import api
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
def _notify_cancel_snail(self):
author_id = self.env.user.id
letters = self.env['snailmail.letter'].search([
('state', 'not in', ['sent', 'canceled', 'pending']),
('user_id', '=', author_id),
('model', '=', self._name)
])
letters.cancel()
@api.model
def notify_cancel_by_type(self, notification_type):
super().notify_cancel_by_type(notification_type)
if notification_type == 'snail':
self._notify_cancel_snail()
return True
@@ -1,6 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_snailmail_letter_user,snailmail.letter.user,model_snailmail_letter,base.group_user,1,1,1,0
access_snailmail_letter_system,snailmail.letter.system,model_snailmail_letter,base.group_system,1,1,1,1
access_snailmail_letter_cancel,access.snailmail.letter.cancel,model_snailmail_letter_cancel,base.group_user,1,1,1,0
access_snailmail_letter_format_error,access.snailmail.letter.format.error,model_snailmail_letter_format_error,base.group_user,1,1,1,0
access_snailmail_letter_missing_required_fields,access.snailmail.letter.missing.required.fields,model_snailmail_letter_missing_required_fields,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_snailmail_letter_user snailmail.letter.user model_snailmail_letter base.group_user 1 1 1 0
3 access_snailmail_letter_system snailmail.letter.system model_snailmail_letter base.group_system 1 1 1 1
access_snailmail_letter_cancel access.snailmail.letter.cancel model_snailmail_letter_cancel base.group_user 1 1 1 0
4 access_snailmail_letter_format_error access.snailmail.letter.format.error model_snailmail_letter_format_error base.group_user 1 1 1 0
5 access_snailmail_letter_missing_required_fields access.snailmail.letter.missing.required.fields model_snailmail_letter_missing_required_fields base.group_user 1 1 1 0
@@ -5,23 +5,6 @@ import { patchRecordMethods } from '@mail/model/model_core';
import '@mail/models/notification_group';
patchRecordMethods('NotificationGroup', {
/**
* @override
*/
openCancelAction() {
if (this.notification_type !== 'snail') {
return this._super(...arguments);
}
this.env.bus.trigger('do-action', {
action: 'snailmail.snailmail_letter_cancel_action',
options: {
additional_context: {
default_model: this.res_model,
unread_counter: this.notifications.length,
},
},
});
},
/**
* @override
*/
@@ -13,53 +13,41 @@ QUnit.module('notification_list_notification_group_tests.js', {
});
QUnit.test('mark as read', async function (assert) {
assert.expect(6);
assert.expect(2);
// Note: The server code is too complex to be rewritten in javascript.
// Actually, the server rely on the model "snailmail.letter" to identify the notification to cancel
// The following code simulates the cancel of the notification without using "snailmail.letter" model
// message that is expected to have a failure
this.data['mail.message'].records.push({
id: 11, // random unique id, will be used to link failure to message
message_type: 'snailmail', // message must be snailmail (goal of the test)
model: 'mail.channel', // expected value to link message to channel
res_id: 31, // id of a random channel
author_id: this.data.currentPartnerId,
id: 11,
message_type: 'snailmail',
model: 'mail.channel',
res_id: 31,
});
// failure that is expected to be used in the test
this.data['mail.notification'].records.push({
mail_message_id: 11, // id of the related message
notification_status: 'exception', // necessary value to have a failure
notification_type: 'snail', // expected failure type for snailmail message
notification_type: 'snail',
});
const bus = new Bus();
bus.on('do-action', null, payload => {
assert.step('do_action');
assert.strictEqual(
payload.action,
'snailmail.snailmail_letter_cancel_action',
"action should be the one to cancel letter"
);
assert.strictEqual(
payload.options.additional_context.default_model,
'mail.channel',
"action should have the group model as default_model"
);
assert.strictEqual(
payload.options.additional_context.unread_counter,
1,
"action should have the group notification length as unread_counter"
);
});
const { createNotificationListComponent } = await start({ data: this.data, env: { bus } });
const { afterNextRender, createNotificationListComponent } = await start({ data: this.data });
await createNotificationListComponent();
assert.containsOnce(
document.body,
'.o_NotificationGroup_markAsRead',
"should have 1 mark as read button"
);
document.querySelector('.o_NotificationGroup_markAsRead').click();
assert.verifySteps(
['do_action'],
"should do an action to display the cancel letter dialog"
await afterNextRender(() => {
document.querySelector('.o_NotificationGroup_markAsRead').click();
});
assert.containsNone(
document.body,
'.o_NotificationGroup',
"should have no notification group"
);
});
-1
View File
@@ -1,4 +1,3 @@
from . import snailmail_confirm
from . import snailmail_letter_cancel
from . import snailmail_letter_format_error
from . import snailmail_letter_missing_required_fields
@@ -1,26 +0,0 @@
from odoo import _, api, fields, models
class SnailmailLetterCancel(models.TransientModel):
_name = 'snailmail.letter.cancel'
_description = 'Dismiss notification for resend by model'
model = fields.Char(string='Model')
help_message = fields.Char(string='Help message', compute='_compute_help_message')
@api.depends('model')
def _compute_help_message(self):
for wizard in self:
wizard.help_message = _("Are you sure you want to discard %s snailmail delivery failures? You won't be able to re-send these letters later!") % (wizard._context.get('unread_counter'))
def cancel_resend_action(self):
author_id = self.env.user.id
for wizard in self:
letters = self.env['snailmail.letter'].search([
('state', 'not in', ['sent', 'canceled', 'pending']),
('user_id', '=', author_id),
('model', '=', wizard.model)
])
for letter in letters:
letter.cancel()
return {'type': 'ir.actions.act_window_close'}
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="snailmail_letter_cancel" model="ir.ui.view">
<field name="name">snailmail.letter.cancel.form</field>
<field name="model">snailmail.letter.cancel</field>
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
<field name="arch" type="xml">
<form string="Cancel notification in failure">
<field name="model" invisible='1'/>
<field name="help_message"/>
<p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red paper-plane next to each message.</p>
<footer>
<button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="snailmail_letter_cancel_action" model="ir.actions.act_window">
<field name="name">Discard snailmail delivery failures</field>
<field name="res_model">snailmail.letter.cancel</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>
+1
View File
@@ -18,3 +18,4 @@ from . import test_message_post
from . import test_message_track
from . import test_performance
from . import test_ui
from . import test_mail_management
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
from odoo.tests import tagged
@tagged('mail_management')
class TestMailManagement(TestMailCommon, TestRecipients):
@classmethod
def setUpClass(cls):
super(TestMailManagement, cls).setUpClass()
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test'})
cls._reset_mail_context(cls.test_record)
cls.msg = cls.test_record.message_post(body='TEST BODY', author_id=cls.partner_employee.id)
cls.notif_p1 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_1.id,
'notification_type': 'email',
'notification_status': 'exception',
'failure_type': 'mail_smtp',
})
cls.notif_p2 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_2.id,
'notification_type': 'email',
'notification_status': 'bounce',
'failure_type': 'unknown',
})
cls.partner_3 = cls.env['res.partner'].create({
'name': 'Partner3',
'email': 'partner3@example.com',
})
cls.notif_p3 = cls.env['mail.notification'].create({
'author_id': cls.msg.author_id.id,
'mail_message_id': cls.msg.id,
'res_partner_id': cls.partner_3.id,
'notification_type': 'email',
'notification_status': 'sent',
'failure_type': None,
})
def test_mail_notify_cancel(self):
self._reset_bus()
self.test_record.with_user(self.user_employee).notify_cancel_by_type('email')
self.assertEqual((self.notif_p1 | self.notif_p2 | self.notif_p3).mapped('notification_status'),
['canceled', 'canceled', 'sent'])
self.assertMessageBusNotifications(self.msg)
@@ -58,6 +58,15 @@ class TestSMSActionsCommon(TestMailFullCommon, TestMailFullRecipients):
@tagged('sms_management')
class TestSMSActions(TestSMSActionsCommon):
def test_sms_notify_cancel(self):
self._reset_bus()
with self.with_user('employee'):
self.test_record.with_user(self.env.user).notify_cancel_by_type('sms')
self.assertEqual((self.notif_p1 | self.notif_p2).mapped('notification_status'), ['canceled', 'canceled'])
self.assertMessageBusNotifications(self.msg)
def test_sms_set_cancel(self):
self._reset_bus()
self.sms_p1.action_set_canceled()
@@ -188,14 +197,3 @@ class TestSMSWizards(TestSMSActionsCommon):
self.assertSMSNotification([{'partner': self.partner_1, 'state': 'sent'}], 'TEST BODY', self.msg, check_sms=True)
self.assertSMSNotification([{'partner': self.partner_2, 'state': 'canceled', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'}], 'TEST BODY', self.msg, check_sms=False)
self.assertMessageBusNotifications(self.msg)
def test_sms_cancel(self):
self._reset_bus()
with self.mockSMSGateway(), self.with_user('employee'):
wizard = self.env['sms.cancel'].with_context(default_model=self.msg.model).create({})
wizard.action_cancel()
self.assertEqual((self.notif_p1 | self.notif_p2).mapped('notification_status'), ['canceled', 'canceled'])
self.assertMessageBusNotifications(self.msg)