summaryrefslogtreecommitdiff
path: root/addons/snailmail
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-04-17 09:53:05 +0000
committerSébastien Theys <seb@odoo.com>2020-04-17 12:17:57 +0000
commitdea52493b819939ae16391d8b19c2f404c7ec143 (patch)
treeda5729bab3ff9bc36825480f54c838b17fced580 /addons/snailmail
parent69b51ce05e855b2b95e75919ac97996a1ff5e136 (diff)
[IMP] mail, sms, snailmail, test_mail_full: adapt mail.notification
The purpose is to have more common code for failure and notifications, with less override in `sms` and `snailmail`. task-2176017 closes odoo/odoo#44170 Related: odoo/enterprise#9140 Related: odoo/upgrade#918 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/snailmail')
-rw-r--r--addons/snailmail/__manifest__.py2
-rw-r--r--addons/snailmail/models/__init__.py1
-rw-r--r--addons/snailmail/models/mail_message.py17
-rw-r--r--addons/snailmail/models/mail_notification.py18
-rw-r--r--addons/snailmail/models/snailmail_letter.py109
-rw-r--r--addons/snailmail/static/src/js/mail_failure.js26
-rw-r--r--addons/snailmail/static/src/js/message.js61
-rw-r--r--addons/snailmail/static/src/js/snailmail_notification_manager.js67
-rw-r--r--addons/snailmail/static/src/js/thread_widget.js130
-rw-r--r--addons/snailmail/static/src/scss/thread.scss17
-rw-r--r--addons/snailmail/static/src/xml/thread.xml68
-rw-r--r--addons/snailmail/static/tests/chatter_snailmail_tests.js212
-rw-r--r--addons/snailmail/views/assets.xml2
13 files changed, 323 insertions, 407 deletions
diff --git a/addons/snailmail/__manifest__.py b/addons/snailmail/__manifest__.py
index 00d597708fd..f9a0a4c121c 100644
--- a/addons/snailmail/__manifest__.py
+++ b/addons/snailmail/__manifest__.py
@@ -6,7 +6,7 @@ Allows users to send documents by post
=====================================================
""",
'category': 'Hidden/Tools',
- 'version': '0.1',
+ 'version': '0.2',
'depends': ['iap', 'mail'],
'data': [
'data/snailmail_data.xml',
diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py
index a05bee36be9..37c70029e12 100644
--- a/addons/snailmail/models/__init__.py
+++ b/addons/snailmail/models/__init__.py
@@ -3,6 +3,7 @@
from . import res_company
from . import res_partner
from . import res_config_settings
+from . import mail_notification
from . import snailmail_letter
from . import ir_actions_report
from . import ir_qweb_fields
diff --git a/addons/snailmail/models/mail_message.py b/addons/snailmail/models/mail_message.py
index 94284fd99a7..3607ff8bfca 100644
--- a/addons/snailmail/models/mail_message.py
+++ b/addons/snailmail/models/mail_message.py
@@ -1,44 +1,31 @@
from odoo import api, fields, models
+
class Message(models.Model):
_inherit = 'mail.message'
snailmail_error = fields.Boolean("Snailmail message in error", compute="_compute_snailmail_error", search="_search_snailmail_error")
- snailmail_status = fields.Char("Snailmail Status", compute="_compute_snailmail_error")
letter_ids = fields.One2many(comodel_name='snailmail.letter', inverse_name='message_id')
message_type = fields.Selection(selection_add=[
('snailmail', 'Snailmail')
], ondelete={'snailmail': lambda recs: recs.write({'message_type': 'email'})})
- def _get_message_format_fields(self):
- res = super(Message, self)._get_message_format_fields()
- res.append('snailmail_error')
- res.append('snailmail_status')
- return res
-
@api.depends('letter_ids', 'letter_ids.state')
def _compute_snailmail_error(self):
for message in self:
if message.message_type == 'snailmail' and message.letter_ids:
message.snailmail_error = message.letter_ids[0].state == 'error'
- message.snailmail_status = message.letter_ids[0].error_code if message.letter_ids[0].state == 'error' else message.letter_ids[0].state
else:
message.snailmail_error = False
- message.snailmail_status = ''
def _search_snailmail_error(self, operator, operand):
if operator == '=' and operand:
return ['&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)]
- return ['!', '&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)]
+ return ['!', '&', ('letter_ids.state', '=', 'error'), ('letter_ids.user_id', '=', self.env.user.id)]
def cancel_letter(self):
self.mapped('letter_ids').cancel()
def send_letter(self):
self.mapped('letter_ids')._snailmail_print()
-
- def message_fetch_failed(self):
- res = super(Message, self).message_fetch_failed()
- failed_letters = self.letter_ids.fetch_failed_letters()
- return res + failed_letters
diff --git a/addons/snailmail/models/mail_notification.py b/addons/snailmail/models/mail_notification.py
new file mode 100644
index 00000000000..a368c0a7783
--- /dev/null
+++ b/addons/snailmail/models/mail_notification.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class Notification(models.Model):
+ _inherit = 'mail.notification'
+
+ notification_type = fields.Selection(selection_add=[('snail', 'Snailmail')], ondelete={'snail': 'cascade'})
+ letter_id = fields.Many2one('snailmail.letter', string="Snailmail Letter", index=True, ondelete='cascade')
+ failure_type = fields.Selection(selection_add=[
+ ('sn_credit', "Snailmail Credit Error"),
+ ('sn_trial', "Snailmail Trial Error"),
+ ('sn_price', "Snailmail No Price Available"),
+ ('sn_fields', "Snailmail Missing Required Fields"),
+ ('sn_format', "Snailmail Format Error"),
+ ('sn_error', "Snailmail Unknown Error"),
+ ])
diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py
index b74e2a42abc..702f2d366ca 100644
--- a/addons/snailmail/models/snailmail_letter.py
+++ b/addons/snailmail/models/snailmail_letter.py
@@ -2,11 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import base64
-import datetime
-from odoo import fields, models, api, _, tools
+from odoo import fields, models, api, _
from odoo.addons.iap import jsonrpc
-from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com'
@@ -56,6 +54,7 @@ class SnailmailLetter(models.Model):
reference = fields.Char(string='Related Record', compute='_compute_reference', readonly=True, store=False)
message_id = fields.Many2one('mail.message', string="Snailmail Status Message")
+ notification_ids = fields.One2many('mail.notification', 'letter_id', "Notifications")
street = fields.Char('Street')
street2 = fields.Char('Street2')
@@ -83,6 +82,7 @@ class SnailmailLetter(models.Model):
body=_("Letter sent by post with Snailmail"),
message_type='snailmail'
)
+
partner_id = self.env['res.partner'].browse(vals['partner_id'])
vals.update({
'message_id': msg_id.id,
@@ -93,7 +93,18 @@ class SnailmailLetter(models.Model):
'state_id': partner_id.state_id.id,
'country_id': partner_id.country_id.id,
})
- return super(SnailmailLetter, self).create(vals)
+ letter = super(SnailmailLetter, self).create(vals)
+
+ self.env['mail.notification'].sudo().create({
+ 'mail_message_id': msg_id.id,
+ 'res_partner_id': partner_id.id,
+ 'notification_type': 'snail',
+ 'letter_id': letter.id,
+ 'is_read': True, # discard Inbox notification
+ 'notification_status': 'ready',
+ })
+
+ return letter
def _fetch_attachment(self):
"""
@@ -230,7 +241,7 @@ class SnailmailLetter(models.Model):
'info_msg': 'The attachment could not be generated.',
'state': 'error',
'error_code': 'ATTACHMENT_ERROR'
- })
+ })
continue
if letter.company_id.external_report_layout_id == self.env.ref('l10n_de.external_layout_din5008', False):
document.update({
@@ -269,6 +280,20 @@ class SnailmailLetter(models.Model):
return _('An unknown error happened. Please contact the support.')
return error
+ def _get_failure_type(self, error):
+ if error == 'CREDIT_ERROR':
+ return 'sn_credit'
+ if error == 'TRIAL_ERROR':
+ return 'sn_trial'
+ if error == 'NO_PRICE_AVAILABLE':
+ return 'sn_price'
+ if error == 'MISSING_REQUIRED_FIELDS':
+ return 'sn_fields'
+ if error == 'FORMAT_ERROR':
+ return 'sn_format'
+ else:
+ return 'sn_error'
+
def _snailmail_print(self, immediate=True):
valid_address_letters = self.filtered(lambda l: l._is_valid_address(l))
invalid_address_letters = self - valid_address_letters
@@ -278,13 +303,19 @@ class SnailmailLetter(models.Model):
self.env.cr.commit()
def _snailmail_print_invalid_address(self):
- for letter in self:
- letter.write({
- 'state': 'error',
- 'error_code': 'MISSING_REQUIRED_FIELDS',
- 'info_msg': _('The address of the recipient is not complete')
- })
- self.send_snailmail_update()
+ error = 'MISSING_REQUIRED_FIELDS'
+ error_message = _("The address of the recipient is not complete")
+ self.write({
+ 'state': 'error',
+ 'error_code': error,
+ 'info_msg': error_message,
+ })
+ self.notification_ids.sudo().write({
+ 'notification_status': 'exception',
+ 'failure_type': self._get_failure_type(error),
+ 'failure_reason': error_message,
+ })
+ self.message_id._notify_message_notification_update()
def _snailmail_print_valid_address(self):
"""
@@ -307,6 +338,11 @@ class SnailmailLetter(models.Model):
if doc.get('sent') and response['request_code'] == 200:
note = _('The document was correctly sent by post.<br>The tracking id is %s' % doc['send_id'])
letter_data = {'info_msg': note, 'state': 'sent', 'error_code': False}
+ notification_data = {
+ 'notification_status': 'sent',
+ 'failure_type': False,
+ 'failure_reason': False,
+ }
else:
error = doc['error'] if response['request_code'] == 200 else response['reason']
@@ -316,26 +352,26 @@ class SnailmailLetter(models.Model):
'state': 'error',
'error_code': error if error in ERROR_CODES else 'UNKNOWN_ERROR'
}
+ notification_data = {
+ 'notification_status': 'exception',
+ 'failure_type': self._get_failure_type(error),
+ 'failure_reason': note,
+ }
letter = self.browse(doc['letter_id'])
letter.write(letter_data)
- self.send_snailmail_update()
-
- def send_snailmail_update(self):
- notifications = []
- for letter in self:
- notifications.append([
- (self._cr.dbname, 'res.partner', letter.user_id.partner_id.id),
- {'type': 'snailmail_update', 'elements': letter._format_snailmail_failures()}
- ])
- self.env['bus.bus'].sendmany(notifications)
+ letter.notification_ids.sudo().write(notification_data)
+ self.message_id._notify_message_notification_update()
def snailmail_print(self):
self._snailmail_print()
def cancel(self):
self.write({'state': 'canceled', 'error_code': False})
- self.send_snailmail_update()
+ self.notification_ids.sudo().write({
+ 'notification_status': 'canceled',
+ })
+ self.message_id._notify_message_notification_update()
@api.model
def _snailmail_cron(self):
@@ -349,34 +385,7 @@ class SnailmailLetter(models.Model):
letters_send._snailmail_print()
@api.model
- def fetch_failed_letters(self):
- failed_letters = self.search([('state', '=', 'error'), ('user_id.id', '=', self.env.user.id), ('res_id', '!=', 0), ('model', '!=', False)])
- return failed_letters._format_snailmail_failures()
-
- @api.model
def _is_valid_address(self, record):
record.ensure_one()
required_keys = ['street', 'city', 'zip', 'country_id']
return all(record[key] for key in required_keys)
-
- def _format_snailmail_failures(self):
- """
- A shorter message to notify a failure update
- """
- failures_infos = []
- for letter in self:
- info = {
- 'message_id': letter.message_id.id,
- 'record_name': letter.message_id.record_name,
- 'model_name': self.env['ir.model']._get(letter.model).display_name,
- 'uuid': letter.message_id.message_id,
- 'res_id': letter.res_id,
- 'model': letter.model,
- 'last_message_date': letter.message_id.date,
- 'module_icon': '/snailmail/static/img/snailmail_failure.png',
- 'snailmail_status': letter.error_code if letter.state == 'error' else '',
- 'snailmail_error': letter.state == 'error',
- 'failure_type': 'snailmail',
- }
- failures_infos.append(info)
- return failures_infos
diff --git a/addons/snailmail/static/src/js/mail_failure.js b/addons/snailmail/static/src/js/mail_failure.js
index cb363051392..903633c42ff 100644
--- a/addons/snailmail/static/src/js/mail_failure.js
+++ b/addons/snailmail/static/src/js/mail_failure.js
@@ -16,14 +16,36 @@ MailFailure.include({
*/
getPreview: function () {
var preview = this._super.apply(this, arguments);
- if (this._failureType === 'snailmail') {
+ if (this._messageType === 'snailmail') {
_.extend(preview, {
- body: _t("An error occured when sending a letter with Snailmail"),
id: 'snailmail_failure',
});
}
return preview;
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getPreviewBody() {
+ if (this._messageType === 'snailmail') {
+ return _t("An error occured when sending a letter with Snailmail.");
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ _getPreviewImage() {
+ if (this._messageType === 'snailmail') {
+ return '/snailmail/static/img/snailmail_failure.png';
+ }
+ return this._super(...arguments);
+ },
});
});
diff --git a/addons/snailmail/static/src/js/message.js b/addons/snailmail/static/src/js/message.js
index 5549fe14261..624f2b4027f 100644
--- a/addons/snailmail/static/src/js/message.js
+++ b/addons/snailmail/static/src/js/message.js
@@ -10,76 +10,39 @@ Message.include({
//--------------------------------------------------------------------------
/**
- * Cancels the 'snailmail.letter' which has a message_id corresponding to
- * the ID of this message, then update the status of the message
- *
+ * Cancels the 'snailmail.letter' which has a message_id corresponding to
+ * the ID of this message.
+ *
* @returns {Deferred}
*/
- cancelLetter: function () {
- var self = this;
- var mailBus = this.call('mail_service', 'getMailBus');
+ cancelLetter() {
return this._rpc({
model: 'mail.message',
method: 'cancel_letter',
args: [[this.getID()]],
- }).then(function () {
- self.setSnailmailStatus('canceled');
- self.setSnailmailError(false);
- mailBus.trigger('update_message', self);
});
},
/**
- * @return {string}
- */
- getSnailmailStatus: function () {
- return this._snailmailStatus.toLowerCase();
- },
- /**
- * Is the sent snailmail letter in error
- *
- * @return {Boolean}
+ * @override
*/
- getSnailmailError: function () {
- return this._snailmailError;
+ getNotificationIcon() {
+ if (this.getType() === 'snail') {
+ return 'fa fa-paper-plane';
+ }
+ return this._super(...arguments);
},
/**
* Retries to send the 'snailmail.letter' corresponding to this message
- *
+ *
* @returns {Deferred}
*/
- resendLetter: function () {
+ resendLetter() {
return this._rpc({
model: 'mail.message',
method: 'send_letter',
args: [[this.getID()]],
});
},
- /**
- * @param {string} snailmailStatus
- */
- setSnailmailStatus: function (snailmailStatus) {
- this._snailmailStatus = snailmailStatus;
- },
- /**
- * @param {Boolean} snailmailError
- */
- setSnailmailError: function (snailmailError) {
- this._snailmailError = snailmailError;
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @private
- * @override
- */
- _setInitialData: function (data){
- this._super.apply(this, arguments);
- this._snailmailStatus = data.snailmail_status;
- this._snailmailError = data.snailmail_error;
- },
});
});
diff --git a/addons/snailmail/static/src/js/snailmail_notification_manager.js b/addons/snailmail/static/src/js/snailmail_notification_manager.js
deleted file mode 100644
index 4d33ba89127..00000000000
--- a/addons/snailmail/static/src/js/snailmail_notification_manager.js
+++ /dev/null
@@ -1,67 +0,0 @@
-odoo.define('snailmail.NotificationManager', function (require) {
-"use strict";
-
-var MailManager = require('mail.Manager');
-var MailFailure = require('mail.model.MailFailure');
-
-MailManager.include({
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * @override
- * @param {Object} data structure depending on the type
- * @param {integer} data.id
- */
- _handlePartnerNotification: function (data) {
- if (data.type === 'snailmail_update') {
- this._handleSnailmailUpdateNotification(data);
- } else {
- this._super.apply(this, arguments);
- }
- },
- /**
- * Updates message in thread and systray when there's an update in a snailmail letter
- *
- * @private
- * @param {Object} datas
- * @param {Object[]} datas.elements list of snailmail failure data
- * @param {string} datas.elements[].message_id ID of related message that
- * has a snailmail failure.
- * @param {boolean} datas.elements[].snailmail_error letter is in error
- * @param {string} datas.elements[].snailmail_status status of the letter
- */
- _handleSnailmailUpdateNotification: function (datas) {
- var self = this;
- _.each(datas.elements, function (data) {
- var isNewFailure = data.snailmail_error;
- var matchedFailure = _.find(self._mailFailures, function (failure) {
- return failure.getMessageID() === data.message_id;
- });
-
- if (matchedFailure) {
- var index = _.findIndex(self._mailFailures, matchedFailure);
- if (isNewFailure) {
- self._mailFailures[index] = new MailFailure(self, data);
- } else {
- self._mailFailures.splice(index, 1);
- }
- } else if (isNewFailure) {
- self._mailFailures.push(new MailFailure(self, data));
- }
- var message = _.find(self._messages, function (msg) {
- return msg.getID() === data.message_id;
- });
- if (message) {
- message.setSnailmailStatus(data.snailmail_status);
- message.setSnailmailError(data.snailmail_error);
- self._mailBus.trigger('update_message', message);
- }
- });
- this._mailBus.trigger('update_needaction', this.needactionCounter);
- },
-});
-
-});
diff --git a/addons/snailmail/static/src/js/thread_widget.js b/addons/snailmail/static/src/js/thread_widget.js
index 84cc2a3b2dc..84ab7f9a877 100644
--- a/addons/snailmail/static/src/js/thread_widget.js
+++ b/addons/snailmail/static/src/js/thread_widget.js
@@ -5,39 +5,17 @@ var ThreadWidget = require('mail.widget.Thread');
var Dialog = require('web.Dialog');
var core = require('web.core');
-var QWeb = core.qweb;
var _t = core._t;
ThreadWidget.include({
- dependencies: ['bus_service'],
- events: _.extend({}, ThreadWidget.prototype.events, {
- 'click .o_thread_message_snailmail_missing_required_fields': '_onClickMissingRequiredFields',
- 'click .o_thread_message_snailmail_credit_error': '_onClickCreditError',
- 'click .o_thread_message_snailmail_trial_error': '_onClickTrialError',
- 'click .o_thread_message_snailmail_no_price_available': '_onClickNoPriceAvailable',
- 'click .o_thread_message_snailmail_format_error': '_onClickFormatError',
- 'click .o_thread_message_snailmail_unknown_error': '_onClickUnknownError',
- }),
- /**
- * @override
- */
- init: function () {
- this._super.apply(this, arguments);
- this._enabledOptions = _.defaults(this._enabledOptions, {
- displaySnailmailIcons: true,
- });
- this._disabledOptions = _.defaults(this._disabledOptions, {
- displaySnailmailIcons: false,
- });
- },
+
/**
* @override
*/
render: function (thread, options) {
this._super.apply(this, arguments);
var messages = _.clone(thread.getMessages({ domain: options.domain || [] }));
- this._messages = messages;
- this._renderMessageSnailmailPopover();
+ this._messages = messages;
},
//--------------------------------------------------------------------------
@@ -46,8 +24,8 @@ ThreadWidget.include({
/**
* @private
- * @param {integer} messageID
- * @param {string} content
+ * @param {integer} messageID
+ * @param {string} content
*/
_openCreditErrorDialog: function (messageID, content) {
var message = _.find(this._messages, function (message) {
@@ -57,17 +35,25 @@ ThreadWidget.include({
size: 'medium',
title: _t("Failed letter"),
$content: $('<div>').html(content),
- buttons: [
- {text: _t("Re-send letter"), classes: 'btn-primary', close: true, click: _.bind(message.resendLetter, message)},
- {text: _t("Cancel letter"), close: true, click: _.bind(message.cancelLetter, message)},
- {text: _t("Close"), close: true},
- ],
+ buttons: [{
+ text: _t("Re-send letter"),
+ classes: 'btn-primary',
+ close: true,
+ click: (...args) => message.resendLetter(...args),
+ }, {
+ text: _t("Cancel letter"),
+ close: true,
+ click: (...args) => message.cancelLetter(...args),
+ }, {
+ text: _t("Close"),
+ close: true,
+ }],
}).open();
},
/**
* @private
- * @param {integer} messageID
- * @param {string} content
+ * @param {integer} messageID
+ * @param {string} content
*/
_openGenericErrorDialog: function (messageID, content) {
var message = _.find(this._messages, function (message) {
@@ -77,46 +63,17 @@ ThreadWidget.include({
size: 'medium',
title: _t("Failed letter"),
$content: $('<div>').html(content),
- buttons: [
- {text: _t("Cancel letter"), classes: 'btn-primary', close: true, click: _.bind(message.cancelLetter, message)},
- {text: _t("Close"), close: true},
- ],
+ buttons: [{
+ text: _t("Cancel letter"),
+ classes: 'btn-primary',
+ close: true,
+ click: (...args) => message.cancelLetter(...args),
+ }, {
+ text: _t("Close"),
+ close: true,
+ }],
}).open();
},
- /**
- * Render the popover when mouse-hovering on the mail icon of a message
- * in the thread. There is at most one such popover at any given time.
- *
- * @private
- * @param {mail.model.AbstractMessage[]} messages list of messages in the
- * rendered thread, for which popover on mouseover interaction is
- * permitted.
- */
- _renderMessageSnailmailPopover: function () {
- var self = this;
- if (this._messageSnailmailPopover) {
- this._messageSnailmailPopover.popover('hide');
- }
- if (!this.$('.o_thread_snailmail_tooltip').length) {
- return;
- }
- this._messageSnailmailPopover = this.$('.o_thread_snailmail_tooltip').popover({
- html: true,
- boundary: 'viewport',
- placement: 'auto',
- trigger: 'hover',
- offset: '0, 1',
- content: function () {
- var messageID = $(this).data('message-id');
- var message = _.find(self._messages, function (message) {
- return message.getID() === messageID;
- });
- return QWeb.render('snailmail.widget.Thread.Message.SnailmailTooltip', {
- status: message.getSnailmailStatus()
- });
- },
- });
- },
//--------------------------------------------------------------------------
// Handlers
@@ -158,6 +115,37 @@ ThreadWidget.include({
});
},
/**
+ * Discards the generic error handler since more specific handlers are
+ * defined here.
+ *
+ * @override
+ */
+ _onClickMessageNotificationError(ev) {
+ const $target = $(ev.currentTarget);
+ if ($target.data('message-type') === 'snailmail') {
+ if ($target.data('failure-type') === 'sn_fields') {
+ this._onClickMissingRequiredFields(ev);
+ }
+ if ($target.data('failure-type') === 'sn_credit') {
+ this._onClickCreditError(ev);
+ }
+ if ($target.data('failure-type') === 'sn_trial') {
+ this._onClickTrialError(ev);
+ }
+ if ($target.data('failure-type') === 'sn_price') {
+ this._onClickNoPriceAvailable(ev);
+ }
+ if ($target.data('failure-type') === 'sn_format') {
+ this._onClickFormatError(ev);
+ }
+ if ($target.data('failure-type') === 'sn_error') {
+ this._onClickUnknownError(ev);
+ }
+ } else {
+ this._super(...arguments);
+ }
+ },
+ /**
* @private
* @param {MouseEvent} event
*/
diff --git a/addons/snailmail/static/src/scss/thread.scss b/addons/snailmail/static/src/scss/thread.scss
deleted file mode 100644
index ecf42cef033..00000000000
--- a/addons/snailmail/static/src/scss/thread.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-.o_thread_message_snailmail {
- padding: 4px;
-
- &:not(.o_thread_message_snailmail_error) {
- color: grey;
- }
-
- &.o_thread_message_snailmail_error {
- color: red;
- opacity: 1;
- cursor: pointer;
- }
-}
-
-.o_thread_tooltip_snailmail_icon {
- min-width: 1rem;
-}
diff --git a/addons/snailmail/static/src/xml/thread.xml b/addons/snailmail/static/src/xml/thread.xml
index aeda07445be..df3ae1e08d4 100644
--- a/addons/snailmail/static/src/xml/thread.xml
+++ b/addons/snailmail/static/src/xml/thread.xml
@@ -1,37 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<template id="template" xml:space="preserve">
- <t t-extend="mail.widget.Thread.Message">
- <t t-jquery=".o_thread_tooltip_container" t-operation="after">
- <span t-if="message.getType() === 'snailmail' and options.displaySnailmailIcons" class="o_thread_snailmail_tooltip_container">
- <t t-set="thread_icon_class" t-value="'o_thread_snailmail_tooltip o_thread_message_snailmail o_thread_message_snailmail_' + message.getSnailmailStatus()" />
- <t t-if="message.getSnailmailError()">
- <t t-set="thread_icon_class" t-value="thread_icon_class + ' o_thread_message_snailmail_error'"/>
- </t>
- <i t-att-class="thread_icon_class + ' fa fa-paper-plane'" t-att-data-message-id="message.getID()"/>
- </span>
- </t>
+ <t t-inherit="mail.widget.Thread.Message" t-inherit-mode="extension">
+ <xpath expr="//span[@name='notification_icon']" position="attributes">
+ <!--
+ Messages from snaimail are considered to have at most one notification.
+ The failure type of the whole message is considered to be the same as the one from
+ that first notification, and the click action will depend on it.
+ -->
+ <attribute name="t-att-data-failure-type">message.getNotifications()[0].failure_type</attribute>
+ </xpath>
</t>
- <t t-name="snailmail.widget.Thread.Message.SnailmailTooltip">
- <span class="d-inline-block text-center o_thread_tooltip_snailmail">
- <t t-if="status === 'sent'">
- <i class='fa fa-check o_thread_tooltip_snailmail_icon' title="Sent" role="img" aria-label="Sent"/>
- Sent
+ <t t-inherit="mail.widget.Thread.Message.MailTooltip" t-inherit-mode="extension">
+ <!--
+ It was decided that the information displayed for snaimail messages has to be
+ different than for standard messages, see task-1907998.
+ -->
+ <xpath expr="//span[@name='notification_status']" position="replace">
+ <t t-if="notification.notification_type === 'snail'">
+ <span class="d-inline-block text-center o_thread_tooltip_icon">
+ <t t-if="notification.notification_status === 'sent'">
+ <i class="fa fa-check o_thread_tooltip_snailmail_icon" title="Sent" role="img" aria-label="Sent"/>
+ Sent
+ </t>
+ <t t-elif="notification.notification_status === 'canceled'">
+ <i class="fa fa-trash-o o_thread_tooltip_snailmail_icon" title="Canceled" role="img" aria-label="Canceled"/>
+ Canceled
+ </t>
+ <t t-elif="notification.notification_status === 'ready'">
+ <i class="fa fa-clock-o o_thread_tooltip_snailmail_icon" title="Awaiting Dispatch" role="img" aria-label="Awaiting Dispatch"/>
+ Awaiting Dispatch
+ </t>
+ <t t-else="">
+ <i class="fa fa-exclamation text-danger o_thread_tooltip_snailmail_icon" title="Error" role="img" aria-label="Error"/>
+ Error
+ </t>
+ </span>
</t>
- <t t-elif="status === 'canceled'">
- <i class='fa fa-trash-o o_thread_tooltip_snailmail_icon' title="Canceled" role="img" aria-label="Canceled"/>
- Canceled
- </t>
- <t t-elif="status === 'pending'">
- <i class='fa fa-clock-o o_thread_tooltip_snailmail_icon' title="Awaiting Dispatch" role="img" aria-label="Awaiting Dispatch"/>
- Awaiting Dispatch
- </t>
- <t t-else="">
- <i class='fa fa-exclamation text-danger o_thread_tooltip_snailmail_icon' title="Error" role="img" aria-label="Error"/>
- Error
- </t>
- </span>
+ <t t-else="">$0</t>
+ </xpath>
+ <xpath expr="//span[@name='partner_name']" position="replace">
+ <t t-if="notification.notification_type === 'snail'"></t>
+ <t t-else="">$0</t>
+ </xpath>
</t>
</template>
diff --git a/addons/snailmail/static/tests/chatter_snailmail_tests.js b/addons/snailmail/static/tests/chatter_snailmail_tests.js
index 8fcf0ce587b..fe416e424e5 100644
--- a/addons/snailmail/static/tests/chatter_snailmail_tests.js
+++ b/addons/snailmail/static/tests/chatter_snailmail_tests.js
@@ -70,18 +70,14 @@ QUnit.module('Chatter', {
string: "Related Document ID",
type: 'integer',
},
- snailmail_status: {
- string: "Snailmail Status",
- type: "char"
- },
- snailmail_error: {
- string: "Snailmail Error",
- type: "boolean",
- },
message_type: {
string: "Type",
type: 'selection',
},
+ notifications: {
+ string: "Notifications",
+ type: 'array',
+ }
},
records: [{
id: 11,
@@ -106,10 +102,12 @@ QUnit.module('Chatter', {
});
QUnit.test('Sent', async function (assert) {
- assert.expect(7);
+ assert.expect(6);
- this.data['mail.message'].records[0].snailmail_status = 'sent';
- this.data['mail.message'].records[0].snailmail_error = false;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'sent',
+ }];
var form = await createView({
View: FormView,
@@ -120,31 +118,32 @@ QUnit.test('Sent', async function (assert) {
arch: getArch(),
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_sent',
- "Snailmail status of message should be 'sent'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- await testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_sent'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ await testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-check').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-check').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Sent")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Sent")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_sent'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.containsNone($('.modal'), "No modal should open on click");
form.destroy();
});
QUnit.test('Canceled', async function (assert) {
- assert.expect(7);
+ assert.expect(6);
- this.data['mail.message'].records[0].snailmail_status = 'canceled';
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'canceled',
+ }];
var form = await createView({
View: FormView,
@@ -155,32 +154,32 @@ QUnit.test('Canceled', async function (assert) {
arch: getArch(),
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_canceled',
- "Snailmail status of message should be 'canceled'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_canceled'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-trash-o').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-trash-o').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Canceled")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Canceled")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_canceled'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.containsNone($('.modal'), "No modal should open on click");
form.destroy();
});
QUnit.test('Pending', async function (assert) {
- assert.expect(7);
+ assert.expect(6);
- this.data['mail.message'].records[0].snailmail_status = 'pending';
- this.data['mail.message'].records[0].snailmail_error = false;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'ready',
+ }];
var form = await createView({
View: FormView,
@@ -191,32 +190,33 @@ QUnit.test('Pending', async function (assert) {
arch: getArch(),
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_pending',
- "Snailmail status of message should be 'pending'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_pending'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-clock-o').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-clock-o').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Awaiting Dispatch")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Awaiting Dispatch")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_pending'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.containsNone($('.modal'), "No modal should open on click");
form.destroy();
});
QUnit.test('No Price Available', async function (assert) {
- assert.expect(11);
+ assert.expect(10);
- this.data['mail.message'].records[0].snailmail_status = 'no_price_available';
- this.data['mail.message'].records[0].snailmail_error = true;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'exception',
+ 'failure_type': 'sn_price',
+ }];
var form = await createView({
View: FormView,
@@ -234,22 +234,20 @@ QUnit.test('No Price Available', async function (assert) {
}
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_no_price_available',
- "Snailmail status of message should be 'no_price_available'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_no_price_available'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-exclamation').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Error")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_no_price_available'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
var $modal = $('.modal');
assert.ok($modal.length, "A modal should open on click");
@@ -267,10 +265,13 @@ QUnit.test('No Price Available', async function (assert) {
});
QUnit.test('Format Error', async function (assert) {
- assert.expect(8);
+ assert.expect(7);
- this.data['mail.message'].records[0].snailmail_status = 'format_error';
- this.data['mail.message'].records[0].snailmail_error = true;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'exception',
+ 'failure_type': 'sn_format',
+ }];
testUtils.mock.patch(ThreadWidget, {
do_action: function (action, options) {
@@ -294,22 +295,20 @@ QUnit.test('Format Error', async function (assert) {
}
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_format_error',
- "Snailmail status of message should be 'format_error'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_format_error'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-exclamation').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Error")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_format_error'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.verifySteps(['do_action'], "'do_action' should have been called");
form.destroy();
@@ -317,10 +316,13 @@ QUnit.test('Format Error', async function (assert) {
});
QUnit.test('Credit Error', async function (assert) {
- assert.expect(14);
+ assert.expect(13);
- this.data['mail.message'].records[0].snailmail_status = 'credit_error';
- this.data['mail.message'].records[0].snailmail_error = true;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'exception',
+ 'failure_type': 'sn_credit',
+ }];
var form = await createView({
View: FormView,
@@ -343,22 +345,20 @@ QUnit.test('Credit Error', async function (assert) {
}
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_credit_error',
- "Snailmail status of message should be 'credit_error'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_credit_error'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-exclamation').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Error")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
var $modal = $('.modal');
assert.ok($modal.length, "A modal should open on click");
@@ -369,7 +369,7 @@ QUnit.test('Credit Error', async function (assert) {
assert.notOk($('.modal').length,
"The modal should be closed after click on 'Re-send letter'");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.containsOnce($modal, 'button:contains("Cancel letter")',
"Modal should have a 'Cancel letter' button");
@@ -385,10 +385,13 @@ QUnit.test('Credit Error', async function (assert) {
});
QUnit.test('Trial Error', async function (assert) {
- assert.expect(14);
+ assert.expect(13);
- this.data['mail.message'].records[0].snailmail_status = 'trial_error';
- this.data['mail.message'].records[0].snailmail_error = true;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'exception',
+ 'failure_type': 'sn_trial',
+ }];
var form = await createView({
View: FormView,
@@ -411,22 +414,20 @@ QUnit.test('Trial Error', async function (assert) {
}
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_trial_error',
- "Snailmail status of message should be 'trial_error'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_trial_error'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-exclamation').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Error")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
var $modal = $('.modal');
assert.ok($modal.length, "A modal should open on click");
@@ -437,7 +438,7 @@ QUnit.test('Trial Error', async function (assert) {
assert.containsNone($('.modal'),
"The modal should be closed after click on 'Re-send letter'");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
$modal = $('.modal');
assert.containsOnce($modal, 'button:contains("Cancel letter")',
@@ -454,10 +455,13 @@ QUnit.test('Trial Error', async function (assert) {
});
QUnit.test('Missing Required Fields', async function (assert) {
- assert.expect(8);
+ assert.expect(7);
- this.data['mail.message'].records[0].snailmail_status = 'missing_required_fields';
- this.data['mail.message'].records[0].snailmail_error = true;
+ this.data['mail.message'].records[0].notifications = [{
+ 'notification_type': 'snail',
+ 'notification_status': 'exception',
+ 'failure_type': 'sn_fields',
+ }];
testUtils.mock.patch(ThreadWidget, {
do_action: function (action, options) {
@@ -480,22 +484,20 @@ QUnit.test('Missing Required Fields', async function (assert) {
}
});
- assert.containsOnce(form, '.o_thread_snailmail_tooltip_container',
+ assert.containsOnce(form, '.o_thread_message_notification',
"Snailmail icon should appear on message");
- assert.containsOnce(form, '.o_thread_message_snailmail_missing_required_fields',
- "Snailmail status of message should be 'missing_required_fields'");
- assert.containsNone(form, '.o_thread_tooltip_snailmail',
+ assert.containsNone(form, '.o_thread_tooltip_icon',
"No tooltip should be present");
- testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_snailmail_missing_required_fields'), 'mouseover');
- assert.ok($('.o_thread_tooltip_snailmail:visible').length,
+ testUtils.dom.triggerMouseEvent(form.$('.o_thread_message_notification'), 'mouseover');
+ assert.ok($('.o_thread_tooltip_icon:visible').length,
"Tooltip should appear when hovering the Snailmail Icon");
- assert.ok($('.o_thread_tooltip_snailmail_icon.fa-exclamation').length,
+ assert.ok($('.o_thread_tooltip_icon .fa-exclamation').length,
"Tooltip should show correct icon");
- assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length,
+ assert.ok($('.o_thread_tooltip_icon:contains("Error")').length,
"Tooltip should show correct text");
- await testUtils.dom.click(form.$('.o_thread_message_snailmail_missing_required_fields'));
+ await testUtils.dom.click(form.$('.o_thread_message_notification'));
assert.verifySteps(['do_action'], "'do_action' should have been called");
form.destroy();
diff --git a/addons/snailmail/views/assets.xml b/addons/snailmail/views/assets.xml
index 3aacde296c2..def7326b0cd 100644
--- a/addons/snailmail/views/assets.xml
+++ b/addons/snailmail/views/assets.xml
@@ -4,10 +4,8 @@
<xpath expr="." position="inside">
<script type="text/javascript" src="/snailmail/static/src/js/mail_failure.js"></script>
<script type="text/javascript" src="/snailmail/static/src/js/message.js"></script>
- <script type="text/javascript" src="/snailmail/static/src/js/snailmail_notification_manager.js"></script>
<script type="text/javascript" src="/snailmail/static/src/js/systray_messaging_menu.js"></script>
<script type="text/javascript" src="/snailmail/static/src/js/thread_widget.js"></script>
- <link rel="stylesheet" type="text/scss" href="/snailmail/static/src/scss/thread.scss"/>
</xpath>
</template>