diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/snailmail/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/snailmail/static/src')
20 files changed, 1698 insertions, 0 deletions
diff --git a/addons/snailmail/static/src/bugfix/bugfix.js b/addons/snailmail/static/src/bugfix/bugfix.js new file mode 100644 index 00000000..a375f40f --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.js @@ -0,0 +1,10 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('snailmail/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); diff --git a/addons/snailmail/static/src/bugfix/bugfix.scss b/addons/snailmail/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000..c4272e52 --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/snailmail/static/src/bugfix/bugfix.xml b/addons/snailmail/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000..c17906f7 --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + +<!-- + This file allows introducing new static templates without contaminating other files. + This is useful when bug fixing requires adding new components for instance in stable + versions of Odoo. Any template that is defined in this file should be isolated + in its own file in master. +--> + +</templates> diff --git a/addons/snailmail/static/src/bugfix/bugfix_tests.js b/addons/snailmail/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000..beffb29b --- /dev/null +++ b/addons/snailmail/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('snailmail/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('snailmail', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/message/message.js b/addons/snailmail/static/src/components/message/message.js new file mode 100644 index 00000000..1033c901 --- /dev/null +++ b/addons/snailmail/static/src/components/message/message.js @@ -0,0 +1,87 @@ +odoo.define('snailmail/static/src/components/message/message.js', function (require) { +'use strict'; + +const components = { + Message: require('mail/static/src/components/message/message.js'), + SnailmailErrorDialog: require('snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js'), + SnailmailNotificationPopover: require('snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js'), +}; + +const { patch } = require('web.utils'); + +const { useState } = owl; + +Object.assign(components.Message.components, { + SnailmailErrorDialog: components.SnailmailErrorDialog, + SnailmailNotificationPopover: components.SnailmailNotificationPopover, +}); + +patch(components.Message, 'snailmail/static/src/components/message/message.js', { + /** + * @override + */ + _constructor() { + this._super(...arguments); + this.snailmailState = useState({ + // Determine if the error dialog is displayed. + hasDialog: false, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onClickFailure() { + if (this.message.message_type === 'snailmail') { + /** + * Messages from snailmail 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. + */ + switch (this.message.notifications[0].failure_type) { + case 'sn_credit': + // URL only used in this component, not received at init + this.env.messaging.fetchSnailmailCreditsUrl(); + this.snailmailState.hasDialog = true; + break; + case 'sn_error': + this.snailmailState.hasDialog = true; + break; + case 'sn_fields': + this.message.openMissingFieldsLetterAction(); + break; + case 'sn_format': + this.message.openFormatLetterAction(); + break; + case 'sn_price': + this.snailmailState.hasDialog = true; + break; + case 'sn_trial': + // URL only used in this component, not received at init + this.env.messaging.fetchSnailmailCreditsUrlTrial(); + this.snailmailState.hasDialog = true; + break; + } + } else { + this._super(...arguments); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onDialogClosedSnailmailError() { + this.snailmailState.hasDialog = false; + }, +}); + +}); diff --git a/addons/snailmail/static/src/components/message/message.xml b/addons/snailmail/static/src/components/message/message.xml new file mode 100644 index 00000000..d563083f --- /dev/null +++ b/addons/snailmail/static/src/components/message/message.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-inherit="mail.Message" t-inherit-mode="extension"> + <xpath expr="//*[@name='failureIcon']" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <i class="o_Message_notificationIcon fa fa-paper-plane"/> + </t> + <t t-else="">$0</t> + </xpath> + + <xpath expr="//*[@name='notificationIcon']" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <i class="o_Message_notificationIcon fa fa-paper-plane"/> + </t> + <t t-else="">$0</t> + </xpath> + + <xpath expr="//*[@name='rootCondition']" position="inside"> + <t t-if="snailmailState.hasDialog"> + <SnailmailErrorDialog messageLocalId="message.localId" t-on-dialog-closed="_onDialogClosedSnailmailError"/> + </t> + </xpath> + + <!-- + It was decided that the information displayed for snailmail messages + has to be different than for standard messages, see task-1907998. + --> + <xpath expr="//NotificationPopover" position="replace"> + <t t-if="message.message_type === 'snailmail'"> + <SnailmailNotificationPopover messageLocalId="message.localId"/> + </t> + <t t-else="">$0</t> + </xpath> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/message/message_tests.js b/addons/snailmail/static/src/components/message/message_tests.js new file mode 100644 index 00000000..67044509 --- /dev/null +++ b/addons/snailmail/static/src/components/message/message_tests.js @@ -0,0 +1,680 @@ +odoo.define('snailmail/static/src/components/message/message_tests.js', function (require) { +'use strict'; + +const components = { + Message: require('mail/static/src/components/message/message.js'), +}; +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('snailmail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('message', {}, function () { +QUnit.module('message_tests.js', { + beforeEach() { + beforeEach(this); + + this.createMessageComponent = async (message, otherProps) => { + const props = Object.assign({ messageLocalId: message.localId }, otherProps); + await createRootComponent(this, components.Message, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('Sent', async function (assert) { + assert.expect(8); + + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'sent', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-check', + "popover should have the sent icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Sent", + "popover should have the sent text" + ); +}); + +QUnit.test('Canceled', async function (assert) { + assert.expect(8); + + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'canceled', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-trash-o', + "popover should have the canceled icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Canceled", + "popover should have the canceled text" + ); +}); + +QUnit.test('Pending', async function (assert) { + assert.expect(8); + + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + id: 11, + notification_status: 'ready', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover', + "notification popover should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailNotificationPopover_icon', + "popover should have one icon" + ); + assert.hasClass( + document.querySelector('.o_SnailmailNotificationPopover_icon'), + 'fa-clock-o', + "popover should have the pending icon" + ); + assert.strictEqual( + document.querySelector('.o_SnailmailNotificationPopover').textContent.trim(), + "Awaiting Dispatch", + "popover should have the pending text" + ); +}); + +QUnit.test('No Price Available', async function (assert) { + assert.expect(10); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + } + return this._super(...arguments); + }, + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_price', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentPrice', + "error dialog should have the 'no price' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_cancelLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Cancel letter'" + ); + assert.verifySteps( + ['cancel_letter'], + "should have made a RPC call to 'cancel_letter'" + ); +}); + +QUnit.test('Credit Error', async function (assert) { + assert.expect(11); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + } + return this._super(...arguments); + }, + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_credit', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentCredit', + "error dialog should have the 'credit' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_resendLetterButton', + "dialog should have a 'Re-send letter' button" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_resendLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Re-send letter'" + ); + assert.verifySteps( + ['send_letter'], + "should have made a RPC call to 'send_letter'" + ); +}); + +QUnit.test('Trial Error', async function (assert) { + assert.expect(11); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'send_letter' && args.model === 'mail.message' && args.args[0][0] === 10) { + assert.step(args.method); + } + return this._super(...arguments); + }, + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_trial', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog', + "error dialog should be open" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_contentTrial', + "error dialog should have the 'trial' content" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_resendLetterButton', + "dialog should have a 'Re-send letter' button" + ); + assert.containsOnce( + document.body, + '.o_SnailmailErrorDialog_cancelLetterButton', + "dialog should have a 'Cancel letter' button" + ); + + await afterNextRender(() => { + document.querySelector('.o_SnailmailErrorDialog_resendLetterButton').click(); + }); + assert.containsNone( + document.body, + '.o_SnailmailErrorDialog', + "dialog should be closed after click on 'Re-send letter'" + ); + assert.verifySteps( + ['send_letter'], + "should have made a RPC call to 'send_letter'" + ); +}); + +QUnit.test('Format Error', async function (assert) { + assert.expect(8); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'snailmail.snailmail_letter_format_error_action', + "action should be the one for format error" + ); + assert.strictEqual( + payload.options.additional_context.message_id, + 10, + "action should have correct message id" + ); + }); + + await this.start({ env: { bus } }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['create', { + id: 11, + model: 'mail.channel', + }]], + }); + const message = this.env.models['mail.message'].create({ + id: 10, + message_type: 'snailmail', + notifications: [['insert', { + failure_type: 'sn_format', + id: 11, + notification_status: 'exception', + notification_type: 'snail', + }]], + originThread: [['link', threadViewer.thread]], + }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.verifySteps( + ['do_action'], + "should do an action to display the format error dialog" + ); +}); + +QUnit.test('Missing Required Fields', async function (assert) { + assert.expect(8); + + this.data['mail.message'].records.push({ + id: 10, // random unique id, useful to link letter and notification + message_type: 'snailmail', + res_id: 20, // non 0 id, necessary to fetch failure at init + model: 'res.partner', // not mail.compose.message, necessary to fetch failure at init + }); + this.data['mail.notification'].records.push({ + failure_type: 'sn_fields', + mail_message_id: 10, + notification_status: 'exception', + notification_type: 'snail', + }); + this.data['snailmail.letter'].records.push({ + id: 22, // random unique id, will be asserted in the test + message_id: 10, // id of related message + }); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action, + 'snailmail.snailmail_letter_missing_required_fields_action', + "action should be the one for missing fields" + ); + assert.strictEqual( + payload.options.additional_context.default_letter_id, + 22, + "action should have correct letter id" + ); + }); + + await this.start({ + env: { bus }, + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['insert', { id: 20, model: 'res.partner' }]], + }); + const message = this.env.models['mail.message'].findFromIdentifyingData({ id: 10 }); + await this.createMessageComponent(message, { + threadViewLocalId: threadViewer.threadView.localId, + }); + + assert.containsOnce( + document.body, + '.o_Message', + "should display a message component" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIconClickable', + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + '.o_Message_notificationIcon', + "should display the notification icon" + ); + assert.hasClass( + document.querySelector('.o_Message_notificationIcon'), + 'fa-paper-plane', + "icon should represent snailmail" + ); + + await afterNextRender(() => { + document.querySelector('.o_Message_notificationIconClickable').click(); + }); + assert.verifySteps( + ['do_action'], + "an action should be done to display the missing fields dialog" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/notification_group/notification_group.js b/addons/snailmail/static/src/components/notification_group/notification_group.js new file mode 100644 index 00000000..00cdebe1 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_group/notification_group.js @@ -0,0 +1,27 @@ +odoo.define('snailmail/static/src/components/notification_group/notification_group.js', function (require) { +'use strict'; + +const components = { + NotificationGroup: require('mail/static/src/components/notification_group/notification_group.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.NotificationGroup, 'snailmail/static/src/components/notification_group/notification_group.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + image() { + if (this.group.notification_type === 'snail') { + return '/snailmail/static/img/snailmail_failure.png'; + } + return this._super(...arguments); + }, +}); + +}); diff --git a/addons/snailmail/static/src/components/notification_group/notification_group.xml b/addons/snailmail/static/src/components/notification_group/notification_group.xml new file mode 100644 index 00000000..7f04e716 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_group/notification_group.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-inherit="mail.NotificationGroup" t-inherit-mode="extension"> + <xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside"> + <t t-if="group.notification_type === 'snail'"> + An error occurred when sending a letter with Snailmail. + </t> + </xpath> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js new file mode 100644 index 00000000..d80b4b26 --- /dev/null +++ b/addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js @@ -0,0 +1,304 @@ +odoo.define('snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js', function (require) { +'use strict'; + +const components = { + NotificationList: require('mail/static/src/components/notification_list/notification_list.js'), +}; + +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const Bus = require('web.Bus'); + +QUnit.module('snailmail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('notification_list', {}, function () { +QUnit.module('notification_list_notification_group_tests.js', { + beforeEach() { + beforeEach(this); + + /** + * @param {Object} param0 + * @param {string} [param0.filter='all'] + */ + this.createNotificationListComponent = async ({ filter = 'all' } = {}) => { + await createRootComponent(this, components.NotificationList, { + props: { filter }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('mark as read', async function (assert) { + assert.expect(6); + + // 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 + }); + // 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 + }); + 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" + ); + }); + await this.start({ env: { bus } }); + await this.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" + ); +}); + +QUnit.test('notifications grouped by notification_type', async function (assert) { + assert.expect(11); + + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + id: 11, // random unique id, will be used to link failure to message + message_type: 'email', // different type from second message + model: 'res.partner', // same model as second message (and not `mail.channel`) + res_id: 31, // same res_id as second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + id: 12, // random unique id, will be used to link failure to message + message_type: 'snailmail', // different type from first message + model: 'res.partner', // same model as first message (and not `mail.channel`) + res_id: 31, // same res_id as first message + res_model_name: "Partner", // same related model name for consistency + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'email', // different type from second failure + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'snail', // different type from first failure + } + ); + await this.start(); + await this.createNotificationListComponent(); + + assert.containsN( + document.body, + '.o_NotificationGroup', + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll('.o_NotificationGroup'); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_name', + "should have 1 group name in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_name').textContent, + "Partner", + "should have model name as group name" + ); + assert.containsOnce( + groups[0], + '.o_NotificationGroup_counter', + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in first group" + ); + assert.strictEqual( + groups[0].querySelector('.o_NotificationGroup_inlineText').textContent.trim(), + "An error occurred when sending an email.", + "should have the group text corresponding to email" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_name', + "should have 1 group name in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_name').textContent, + "Partner", + "should have second model name as group name" + ); + assert.containsOnce( + groups[1], + '.o_NotificationGroup_counter', + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(1)", + "should have 1 notification in second group" + ); + assert.strictEqual( + groups[1].querySelector('.o_NotificationGroup_inlineText').textContent.trim(), + "An error occurred when sending a letter with Snailmail.", + "should have the group text corresponding to snailmail" + ); +}); + +QUnit.test('grouped notifications by document model', async function (assert) { + // If all failures linked to a document model refers to different documents, + // a single notification should group all failures that are linked to this + // document model. + assert.expect(12); + + this.data['mail.message'].records.push( + // first message that is expected to have a failure + { + 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: 'res.partner', // same model as second message (and not `mail.channel`) + res_id: 31, // different res_id from second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + id: 12, // random unique id, will be used to link failure to message + message_type: 'snailmail', // message must be snailmail (goal of the test) + model: 'res.partner', // same model as first message (and not `mail.channel`) + res_id: 32, // different res_id from first message + res_model_name: "Partner", // same related model name for consistency + } + ); + this.data['mail.notification'].records.push( + // first failure that is expected to be used in the test + { + mail_message_id: 11, // id of the related first message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'snail', // expected failure type for snailmail message + }, + // second failure that is expected to be used in the test + { + mail_message_id: 12, // id of the related second message + notification_status: 'exception', // necessary value to have a failure + notification_type: 'snail', // expected failure type for snailmail message + } + ); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.name, + "Snailmail Failures", + "action should have 'Snailmail Failures' as name", + ); + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + "action should have the type act_window" + ); + assert.strictEqual( + payload.action.view_mode, + 'kanban,list,form', + "action should have 'kanban,list,form' as view_mode" + ); + assert.strictEqual( + JSON.stringify(payload.action.views), + JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]), + "action should have correct views" + ); + assert.strictEqual( + payload.action.target, + 'current', + "action should have 'current' as target" + ); + assert.strictEqual( + payload.action.res_model, + 'res.partner', + "action should have the group model as res_model" + ); + assert.strictEqual( + JSON.stringify(payload.action.domain), + JSON.stringify([['message_ids.snailmail_error', '=', true]]), + "action should have 'message_has_sms_error' as domain" + ); + }); + + await this.start({ env: { bus } }); + await this.createNotificationListComponent(); + + assert.containsOnce( + document.body, + '.o_NotificationGroup', + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + '.o_NotificationGroup_counter', + "should have 1 group counter" + ); + assert.strictEqual( + document.querySelector('.o_NotificationGroup_counter').textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + + document.querySelector('.o_NotificationGroup').click(); + assert.verifySteps( + ['do_action'], + "should do an action to display the related records" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js new file mode 100644 index 00000000..a7daf7c1 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js @@ -0,0 +1,113 @@ +odoo.define('snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const Dialog = require('web.OwlDialog'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class SnailmailErrorDialog extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const message = this.env.models['mail.message'].get(props.messageLocalId); + const notifications = message ? message.notifications : []; + return { + message: message ? message.__state : undefined, + notifications: notifications.map(notification => + notification ? notification.__state : undefined + ), + snailmail_credits_url: this.env.messaging.snailmail_credits_url, + snailmail_credits_url_trial: this.env.messaging.snailmail_credits_url_trial, + }; + }, { + compareDepth: { + notifications: 1, + }, + }); + // to manually trigger the dialog close event + this._dialogRef = useRef('dialog'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {boolean} + */ + get hasCreditsError() { + return ( + this.notification.failure_type === 'sn_credit' || + this.notification.failure_type === 'sn_trial' + ); + } + + /** + * @returns {mail.message} + */ + get message() { + return this.env.models['mail.message'].get(this.props.messageLocalId); + } + + /** + * @returns {mail.notification} + */ + get notification() { + // Messages from snailmail are considered to have at most one notification. + return this.message.notifications[0]; + } + + /** + * @returns {string} + */ + get title() { + return this.env._t("Failed letter"); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickCancelLetter() { + this._dialogRef.comp._close(); + this.message.cancelLetter(); + } + + /** + * @private + */ + _onClickClose() { + this._dialogRef.comp._close(); + } + + /** + * @private + */ + _onClickResendLetter() { + this._dialogRef.comp._close(); + this.message.resendLetter(); + } + +} + +Object.assign(SnailmailErrorDialog, { + components: { Dialog }, + props: { + messageLocalId: String, + }, + template: 'snailmail.SnailmailErrorDialog', +}); + +return SnailmailErrorDialog; + +}); diff --git a/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml new file mode 100644 index 00000000..bf4f026e --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="snailmail.SnailmailErrorDialog" owl="1"> + <Dialog contentClass="'o_SnailmailErrorDialog'" title="title" size="'medium'" t-ref="dialog"> + <t t-if="message and notification"> + <t t-if="notification.failure_type === 'sn_credit'"> + <p class="o_SnailmailErrorDialog_contentCredit"> + The letter could not be sent due to insufficient credits on your IAP account. + </p> + <t t-if="env.messaging.snailmail_credits_url"> + <div class="text-right"> + <a class="btn btn-link" t-att-href="env.messaging.snailmail_credits_url" target="_blank"> + <i class="fa fa-arrow-right"/> Buy credits + </a> + </div> + </t> + </t> + <t t-elif="notification.failure_type === 'sn_trial'"> + <p class="o_SnailmailErrorDialog_contentTrial"> + You need credits on your IAP account to send a letter. + </p> + <t t-if="env.messaging.snailmail_credits_url_trial"> + <div class="text-right"> + <a class="btn btn-link" t-att-href="env.messaging.snailmail_credits_url_trial"> + <i class="fa fa-arrow-right"/> Buy credits + </a> + </div> + </t> + </t> + <t t-elif="notification.failure_type === 'sn_price'"> + <p class="o_SnailmailErrorDialog_contentPrice"> + The country to which you want to send the letter is not supported by our service. + </p> + </t> + <t t-elif="notification.failure_type === 'sn_error'"> + <p class="o_SnailmailErrorDialog_contentError"> + An unknown error occurred. Please contact our <a href="https://www.odoo.com/help" target="new">support</a> for further assistance. + </p> + </t> + + <t t-set-slot="buttons"> + <t t-if="hasCreditsError"> + <button class="o_SnailmailErrorDialog_resendLetterButton btn btn-primary" t-on-click="_onClickResendLetter">Re-send letter</button> + </t> + <button class="o_SnailmailErrorDialog_cancelLetterButton btn" + t-att-class="{ + 'btn-primary': !hasCreditsError, + 'btn-secondary': hasCreditsError, + }" + t-on-click="_onClickCancelLetter" + > + Cancel letter + </button> + <button class="o_SnailmailErrorDialog_closeButton btn btn-secondary" t-on-click="_onClickClose">Close</button> + </t> + </t> + </Dialog> + </t> + +</templates> diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js new file mode 100644 index 00000000..0845ce07 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js @@ -0,0 +1,86 @@ +odoo.define('snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js', function (require) { +'use strict'; + +const { Component } = owl; +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +class SnailmailNotificationPopover extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const message = this.env.models['mail.message'].get(props.messageLocalId); + const notifications = message ? message.notifications : []; + return { + message: message ? message.__state : undefined, + notifications: notifications.map(notification => notification ? notification.__state : undefined), + }; + }, { + compareDepth: { + notifications: 1, + }, + }); + } + + /** + * @returns {string} + */ + get iconClass() { + switch (this.notification.notification_status) { + case 'sent': + return 'fa fa-check'; + case 'ready': + return 'fa fa-clock-o'; + case 'canceled': + return 'fa fa-trash-o'; + default: + return 'fa fa-exclamation text-danger'; + } + } + + /** + * @returns {string} + */ + get iconTitle() { + switch (this.notification.notification_status) { + case 'sent': + return this.env._t("Sent"); + case 'ready': + return this.env._t("Awaiting Dispatch"); + case 'canceled': + return this.env._t("Canceled"); + default: + return this.env._t("Error"); + } + } + + /** + * @returns {mail.message} + */ + get message() { + return this.env.models['mail.message'].get(this.props.messageLocalId); + } + + /** + * @returns {mail.notification} + */ + get notification() { + // Messages from snailmail are considered to have at most one notification. + return this.message.notifications[0]; + } + +} + +Object.assign(SnailmailNotificationPopover, { + props: { + messageLocalId: String, + }, + template: 'snailmail.SnailmailNotificationPopover', +}); + +return SnailmailNotificationPopover; + +}); diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss new file mode 100644 index 00000000..970bc79d --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss @@ -0,0 +1,7 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +.o_SnailmailNotificationPopover_icon { + margin-inline-end: map-get($spacers, 2); +} diff --git a/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml new file mode 100644 index 00000000..873b08f4 --- /dev/null +++ b/addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="snailmail.SnailmailNotificationPopover" owl="1"> + <div class="o_SnailmailNotificationPopover"> + <t t-if="message and notification"> + <i class="o_SnailmailNotificationPopover_icon" t-att-class="iconClass" role="img"/> + <span t-esc="iconTitle"/> + </t> + </div> + </t> + +</templates> diff --git a/addons/snailmail/static/src/js/snailmail_external_layout.js b/addons/snailmail/static/src/js/snailmail_external_layout.js new file mode 100644 index 00000000..be691d26 --- /dev/null +++ b/addons/snailmail/static/src/js/snailmail_external_layout.js @@ -0,0 +1,7 @@ +// Change address font-size if needed +document.addEventListener('DOMContentLoaded', function (evt) { + var recipientAddress = document.getElementsByClassName('address row')[0].getElementsByTagName('address')[0]; + var height = parseFloat(window.getComputedStyle(recipientAddress, null).getPropertyValue('height')); + var fontSize = parseFloat(window.getComputedStyle(recipientAddress, null).getPropertyValue('font-size')); + recipientAddress.style.fontSize = (85/height) * fontSize + 'px'; +}); diff --git a/addons/snailmail/static/src/models/message/message.js b/addons/snailmail/static/src/models/message/message.js new file mode 100644 index 00000000..f685e48a --- /dev/null +++ b/addons/snailmail/static/src/models/message/message.js @@ -0,0 +1,69 @@ +odoo.define('snailmail/static/src/models/message.message.js', function (require) { +'use strict'; + +const { registerInstancePatchModel } = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.message', 'snailmail/static/src/models/message.message.js', { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Cancels the 'snailmail.letter' corresponding to this message. + * + * @returns {Deferred} + */ + async cancelLetter() { + // the result will come from longpolling: message_notification_update + await this.async(() => this.env.services.rpc({ + model: 'mail.message', + method: 'cancel_letter', + args: [[this.id]], + })); + }, + /** + * Opens the action about 'snailmail.letter' format error. + */ + openFormatLetterAction() { + this.env.bus.trigger('do-action', { + action: 'snailmail.snailmail_letter_format_error_action', + options: { + additional_context: { + message_id: this.id, + }, + }, + }); + }, + /** + * Opens the action about 'snailmail.letter' missing fields. + */ + async openMissingFieldsLetterAction() { + const letterIds = await this.async(() => this.env.services.rpc({ + model: 'snailmail.letter', + method: 'search', + args: [[['message_id', '=', this.id]]], + })); + this.env.bus.trigger('do-action', { + action: 'snailmail.snailmail_letter_missing_required_fields_action', + options: { + additional_context: { + default_letter_id: letterIds[0], + }, + }, + }); + }, + /** + * Retries to send the 'snailmail.letter' corresponding to this message. + */ + async resendLetter() { + // the result will come from longpolling: message_notification_update + await this.async(() => this.env.services.rpc({ + model: 'mail.message', + method: 'send_letter', + args: [[this.id]], + })); + }, +}); + +}); diff --git a/addons/snailmail/static/src/models/messaging/messaging.js b/addons/snailmail/static/src/models/messaging/messaging.js new file mode 100644 index 00000000..f00d0544 --- /dev/null +++ b/addons/snailmail/static/src/models/messaging/messaging.js @@ -0,0 +1,38 @@ +odoo.define('snailmail/static/src/models/messaging/messaging.js', function (require) { +'use strict'; + +const { + registerInstancePatchModel, + registerFieldPatchModel, +} = require('mail/static/src/model/model_core.js'); +const { attr } = require('mail/static/src/model/model_field.js'); + +registerInstancePatchModel('mail.messaging', 'snailmail/static/src/models/messaging/messaging.js', { + async fetchSnailmailCreditsUrl() { + const snailmail_credits_url = await this.async(() => this.env.services.rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail'], + })); + this.update({ + snailmail_credits_url, + }); + }, + async fetchSnailmailCreditsUrlTrial() { + const snailmail_credits_url_trial = await this.async(() => this.env.services.rpc({ + model: 'iap.account', + method: 'get_credits_url', + args: ['snailmail', '', 0, true], + })); + this.update({ + snailmail_credits_url_trial, + }); + }, +}); + +registerFieldPatchModel('mail.messaging', 'snailmail/static/src/models/messaging/messaging.js', { + snailmail_credits_url: attr(), + snailmail_credits_url_trial: attr(), +}); + +}); diff --git a/addons/snailmail/static/src/models/notification_group/notification_group.js b/addons/snailmail/static/src/models/notification_group/notification_group.js new file mode 100644 index 00000000..3fff33cb --- /dev/null +++ b/addons/snailmail/static/src/models/notification_group/notification_group.js @@ -0,0 +1,62 @@ +odoo.define('snailmail/static/src/models/notification_group/notification_group.js', function (require) { +'use strict'; + +const { + registerInstancePatchModel, +} = require('mail/static/src/model/model_core.js'); + +registerInstancePatchModel('mail.notification_group', 'snailmail/static/src/models/notification_group/notification_group.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @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, + }, + }, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _openDocuments() { + if (this.notification_type !== 'snail') { + return this._super(...arguments); + } + this.env.bus.trigger('do-action', { + action: { + name: this.env._t("Snailmail Failures"), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: this.res_model, + domain: [['message_ids.snailmail_error', '=', true]], + }, + }); + if (this.env.messaging.device.isMobile) { + // messaging menu has a higher z-index than views so it must + // be closed to ensure the visibility of the view + this.env.messaging.messagingMenu.close(); + } + }, +}); + +}); diff --git a/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss b/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss new file mode 100644 index 00000000..ae60f3d4 --- /dev/null +++ b/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss @@ -0,0 +1,50 @@ +/* Generic report layouts*/ +.header { + max-height: 40mm; + overflow: hidden; +} +.container { + margin-top: 5mm; +} +.o_standard_footer, .o_clean_footer, .o_boxed_footer, .o_background_footer { + max-height: 90px !important; + overflow: hidden; + width: 95%; + div > div:first-child { + padding-left: 10mm; + } +} +.o_background_footer { + padding-left: 8mm; +} +.article>.page { + > h1, > h2, > h3 { + width: 50%; + } +} +.article > .address.row > div[name="address"] { + height: 65mm; + background-color: #ffffff; + padding-top: 23mm; + padding-left: 5mm; + padding-bottom: 5mm; + left: -20px !important; + address { + height: 28mm; + } +} + +/*l10n_de_din layout*/ +.din { + .invoice_address { + top: 27.7mm !important; + } + &.article { + .page { + top: 18.46mm !important; + } + } + &.o_background_footer { + top: -8mm !important; + } +} |
