[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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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',
|
||||
}]);
|
||||
},
|
||||
});
|
||||
|
||||
+10
-26
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
*/
|
||||
|
||||
+15
-32
@@ -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,6 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sms_cancel
|
||||
from . import sms_composer
|
||||
from . import sms_resend
|
||||
from . import sms_template_preview
|
||||
|
||||
@@ -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'}
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
*/
|
||||
|
||||
+19
-31
@@ -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,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>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user