Initial commit: Mail packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 4e53507711
1948 changed files with 751201 additions and 0 deletions

View file

@ -0,0 +1,71 @@
/** @odoo-module **/
import { setupViewRegistries } from "@web/../tests/views/helpers";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { MassMailingFullWidthViewController } from "@mass_mailing/js/mailing_mailing_view_form_full_width";
let serverData;
QUnit.module("mass_mailing", {}, function () {
QUnit.module("MassMailingFullWidthViewController", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
},
},
},
actions: {
1: {
id: 1,
name: "test",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
2: {
id: 2,
name: "test",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
},
views: {
"partner,false,form": `<form js_class="mailing_mailing_view_form_full_width">
<field name="display_name"/>
</form>`,
"partner,false,list": `<tree><field name="display_name"/></tree>`,
"partner,false,search": `<search/>`,
},
};
setupViewRegistries();
});
QUnit.test("unregister ResizeObserver on unmount", async (assert) => {
patchWithCleanup(MassMailingFullWidthViewController.prototype, {
setup() {
this._super();
patchWithCleanup(this._resizeObserver, {
disconnect() {
assert.step("disconnect");
return this._super(...arguments);
},
});
},
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
assert.verifySteps([]);
await doAction(webClient, 2);
assert.verifySteps(["disconnect"]);
});
});
});

View file

@ -0,0 +1,521 @@
/** @odoo-module alias=mass_mailing.FieldMassMailingFavoriteFilter.test */
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import * as testUtils from "@web/../tests/helpers/utils";
import weTestUtils from "web_editor.test_utils";
let fixture;
let serverData;
QUnit.module('mass_mailing_favourite_filter', {}, function () {
QUnit.module('favorite filter widget', (hooks) => {
hooks.beforeEach(() => {
fixture = testUtils.getFixture();
const models = weTestUtils.wysiwygData({
'mailing.mailing': {
fields: {
display_name: {
string: 'Display name',
type: 'char',
},
subject: {
string: 'subject',
type: 'char',
},
mailing_model_id: {
string: 'Recipients',
type: 'many2one',
relation: 'ir.model',
},
mailing_model_name: {
string: 'Recipients Model Name',
type: 'char',
},
mailing_filter_id: {
string: 'Filters',
type: 'many2one',
relation: 'mailing.filter',
},
mailing_domain: {
string: 'Domain',
type: 'char',
},
mailing_filter_domain: {
string: 'Domain',
type: 'char',
related: 'mailing_filter_id.mailing_domain',
},
mailing_filter_count: {
string: 'filter Count',
type: 'integer',
},
},
records: [{
id: 1,
display_name: 'Belgian Event promotion',
subject: 'Early bird discount for Belgian Events! Register Now!',
mailing_model_id: 1,
mailing_model_name: 'event',
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: 1,
mailing_filter_count: 1,
mailing_filter_domain: '[["country","=","be"]]',
}, {
id: 2,
display_name: 'New Users Promotion',
subject: 'Early bird discount for new users! Register Now!',
mailing_model_id: 1,
mailing_filter_count: 1,
mailing_model_name: 'event',
mailing_domain: '[["new_user","=",True]]',
mailing_filter_domain: '[["new_user","=",True]]',
}],
},
'ir.model': {
fields: {
model: {string: 'Model', type: 'char'},
},
records: [{
id: 1, name: 'Event', model: 'event',
}, {
id: 2, name: 'Partner', model: 'partner',
}],
},
'mailing.filter': {
fields: {
name: {
string: 'Name',
type: 'char',
},
mailing_domain: {
string: 'Mailing Domain',
type: 'char',
},
mailing_model_id: {
string: 'Recipients Model',
type: 'many2one',
relation: 'ir.model'
},
},
records: [{
id: 1,
name: 'Belgian Events',
mailing_domain: '[["country","=","be"]]',
mailing_model_id: 1,
}],
},
});
serverData = { models };
setupViewRegistries();
});
QUnit.test('create favorite filter', async (assert) => {
assert.expect(8);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === 'create' && args.model === 'mailing.filter') {
assert.deepEqual(args.args,
[{mailing_domain: '[["new_user","=",True]]', mailing_model_id: 1, name: 'event promo - new users'}],
"should pass correct data in create");
}
},
});
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should hide the option to remove filter if no filter is set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter if no filter is set");
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsOnce($dropdown, 'li.ui-menu-item',
"there should be only one existing filter");
// create a new filter
await testUtils.click(fixture, '.o_mass_mailing_add_filter');
fixture.querySelector('.o_mass_mailing_filter_name').value = 'event promo - new users';
// Simulate 'Enter' key, which actually 'clicks' the 'o_mass_mailing_btn_save_filter' btn
await testUtils.triggerEvent(fixture, '.o_mass_mailing_filter_name', 'keydown', { key: 'Enter'});
// check if filter is set correctly
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'event promo - new users', "saved filter should be set automatically");
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is already set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if filter is already set");
// Ensures input is not focussed otherwise clicking on it will just close the dropdown instead of opening it
fixture.querySelector('.o_field_mailing_filter .o_input_dropdown input').blur();
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsN($dropdown, 'li.ui-menu-item', 2,
"there should be two existing filters");
await testUtils.clickSave(fixture);
});
QUnit.test('unlink favorite filter', async (assert) => {
assert.expect(10);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === 'unlink' && args.model === 'mailing.filter') {
assert.deepEqual(args.args[0], [1], "should pass correct filter ID for deletion");
} else if (args.method === 'write' && args.model === 'mailing.mailing') {
assert.strictEqual(args.args[1].mailing_filter_id,
false, "filter id should be");
assert.strictEqual(args.args[1].mailing_domain,
'[["country","=","be"]]', "mailing domain should be retained while unlinking filter");
}
},
});
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'Belgian Events', "there should be filter set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is already set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should hide the option to save filter if filter is already set");
// unlink filter
await testUtils.click(fixture.querySelector('.o_mass_mailing_remove_filter'));
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'', "filter should be empty");
await testUtils.nextTick();
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should hide the option to remove filter if no filter is set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not hide the option to save filter if no filter is set");
// check drop-down after filter deletion
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsOnce($dropdown, 'li.ui-menu-item.o_m2o_no_result',
"there should be no available filters");
await testUtils.clickSave(fixture);
});
QUnit.test('changing filter correctly applies the domain', async (assert) => {
assert.expect(2);
serverData.models.partner = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'Azure Interior'},
{id: 2, name: 'Deco Addict'},
{id: 3, name: 'Marc Demo'},
]
};
serverData.models['mailing.filter'].records = [{
id: 1,
name: 'Azure Partner Only',
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records.push({
id: 3,
display_name: 'Partner Event promotion',
subject: 'Early bird discount for Partners!',
mailing_model_id: 2,
mailing_model_name: 'partner',
mailing_filter_count: 1,
mailing_domain: "[['name','!=', 'Azure Interior']]",
});
serverData.models['mailing.mailing'].onchanges = {
mailing_filter_id: obj => {
obj.mailing_domain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
assert.equal(fixture.querySelector('.o_domain_show_selection_button').textContent.trim(), '2 record(s)',
"default domain should filter 2 records (all but Azure)");
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
await testUtils.click($dropdown.lastElementChild, 'li');
assert.equal(fixture.querySelector('.o_domain_show_selection_button').textContent.trim(), '1 record(s)',
"applied filter should only display single record (only Azure)");
await testUtils.clickSave(fixture);
});
QUnit.test('filter drop-down and filter icons visibility toggles properly based on filters available', async (assert) => {
assert.expect(11);
serverData.models.partner = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'Azure Interior'},
]
};
serverData.models.event = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
country: {string: 'Country', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'BE Event', country: 'be'},
]
};
serverData.models['mailing.filter'].records = [{
id: 2,
name: 'Azure partner',
mailing_domain: '[["name","=","Azure Interior"]]',
mailing_model_id: 2,
}, {
id: 3,
name: 'Ready Mat partner',
mailing_domain: '[["name","=","Ready Mat"]]',
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records = [{
id: 1,
display_name: 'Belgian Event promotion',
subject: 'Early bird discount for Belgian Events! Register Now!',
mailing_model_id: 1,
mailing_model_name: 'event',
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: false,
mailing_filter_count: 0,
}];
serverData.models['mailing.mailing'].onchanges = {
mailing_model_id: obj => {
obj.mailing_filter_count = serverData.models['mailing.filter'].records.filter(r => r.mailing_model_id === obj.mailing_model_id).length;
},
mailing_filter_id: obj => {
const filterDomain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
obj.mailing_domain = filterDomain, obj.mailing_filter_domain = filterDomain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="mailing_filter_domain" invisible="1"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
assert.isNotVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should hide the drop-down to select a filter because there is no filter available for 'Event'");
assert.isVisible(fixture.querySelector('.o_mass_mailing_no_filter'),
"should show custom message because there is no filter available for 'Event'");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should show icon to save the filter because domain is set in the mailing");
// If domain is not set on mailing and no filter available, both drop-down and icon container are hidden
await testUtils.click(fixture.querySelector('.o_domain_delete_node_button'));
assert.isNotVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should not display drop-down because there is still no filter available to select from");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_filter_container'),
"should not show filter container because there is no filter available and no domain set in mailing");
// If domain is not set on mailing but filters available, display drop-down but hide the icon container
await testUtils.clickDropdown(fixture, 'mailing_model_id');
await testUtils.clickOpenedDropdownItem(fixture, 'mailing_model_id', 'Partner');
assert.isVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should show the drop-down to select a filter because there are filters available for 'Partner'");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_filter_container'),
"should not show filter container because there is no filter selected and no domain set in mailing");
// Save / Remove icons visibility
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
await testUtils.clickOpenedDropdownItem(fixture, 'mailing_filter_id', 'Azure partner');
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is selected");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if filter is selected");
await testUtils.click(fixture.querySelector('.o_domain_add_node_button'));
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter because mailing domain is changed");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should not have option to remove filter because mailing domain is changed");
});
QUnit.test('filter widget does not raise traceback when losing focus with unexpected domain format', async (assert) => {
assert.expect(4);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
});
// Initial state of icons with no filter
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter if no filter is set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'));
// Set incorrect domain format
await testUtils.editInput(fixture, "#mailing_domain", "[");
// Wait to lose the focus
await testUtils.nextTick();
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if domain format is incorrect");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should still not be visible");
});
QUnit.test('filter widget works in edit and readonly', async (assert) => {
assert.expect(4);
serverData.models.partner = {
fields: {
name: { string: 'Name', type: 'char', searchable: true },
},
};
serverData.models['mailing.filter'].records = [{
id: 1,
name: 'Azure Partner Only',
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records.push({
id: 3,
display_name: 'Partner Event promotion',
subject: 'Early bird discount for Partners!',
mailing_model_id: 2,
mailing_model_name: 'partner',
mailing_filter_count: 1,
mailing_filter_domain: "[['name','=', 'Azure Interior']]",
mailing_filter_id: 1,
mailing_domain: "[['name','=', 'Azure Interior']]",
state: 'draft',
});
serverData.models['mailing.mailing'].fields.state = {
string: 'Stage',
type: 'selection',
selection: [['draft', 'Draft'], ['running', 'Running']]
};
serverData.models['mailing.mailing'].onchanges = {
mailing_filter_id: obj => {
obj.mailing_domain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="state" widget="statusbar" options="{'clickable' : '1'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
await testUtils.nextTick();
const selectField = fixture.querySelector("button[data-value='running']");
assert.containsOnce(fixture, "div[name='mailing_model_id']:not(.o_readonly_modifier)");
assert.ok(fixture.querySelector(".o_mass_mailing_save_filter_container").checkVisibility());
// set to 'running'
selectField.dispatchEvent(new Event('click'));
selectField.dispatchEvent(new Event('change'));
await testUtils.nextTick();
assert.containsOnce(fixture, "div[name='mailing_model_id'].o_readonly_modifier");
assert.ok(fixture.querySelector(".o_mass_mailing_save_filter_container").checkVisibility());
});
});
});

View file

@ -0,0 +1,140 @@
/** @odoo-module alias=mass_mailing.field_html_tests **/
import * as ajax from "web.ajax";
import weTestUtils from "web_editor.test_utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
editInput,
getFixture,
makeDeferred,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import * as legacyTestUtils from "web.test_utils";
import { assets } from "@web/core/assets";
let serverData;
let fixture;
QUnit.module('mass_mailing', {}, function () {
QUnit.module('field html', (hooks) => {
hooks.beforeEach(() => {
fixture = getFixture();
const models = weTestUtils.wysiwygData({
'mailing.mailing': {
fields: {
display_name: {
string: "Displayed name",
type: "char"
},
body_html: {
string: "Message Body inline (to send)",
type: "html"
},
body_arch: {
string: "Message Body for edition",
type: "html"
},
},
records: [{
id: 1,
display_name: "first record",
body_html: "<div class='field_body' style='background-color: red;'><p>code to edit</p></div>",
body_arch: "<div class='field_body'><p>code to edit</p></div>",
}],
},
});
serverData = { models };
setupViewRegistries();
legacyTestUtils.mock.patch(ajax, {
loadAsset: function (xmlId) {
if (xmlId === 'template.assets') {
return Promise.resolve({
cssLibs: [],
cssContents: ['.field_body {background-color: red;}'],
jsContents: ['window.odoo = {define: function(){}}; // inline asset'],
});
}
if (xmlId === 'template.assets_all_style') {
return Promise.resolve({
cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
return $(this).attr('href');
}).get(),
cssContents: ['.field_body {background-color: red;}']
});
}
if (xmlId === 'web_editor.wysiwyg_iframe_editor_assets') {
return Promise.resolve({});
}
throw 'Wrong template';
},
});
});
hooks.afterEach(() => {
legacyTestUtils.mock.unpatch(ajax);
});
QUnit.test('save arch and html', async function (assert) {
assert.expect(2);
await makeView({
type: "form",
resModel: 'mailing.mailing',
resId: 1,
serverData,
arch: '<form>' +
' <field name="body_html" class="oe_read_only"'+
' options="{'+
' \'cssReadonly\': \'template.assets\','+
' }"'+
' />'+
' <field name="body_arch" class="oe_edit_only" widget="mass_mailing_html"'+
' options="{'+
' \'snippets\': \'web_editor.snippets\','+
' \'cssEdit\': \'template.assets\','+
' \'inline-field\': \'body_html\''+
' }"'+
' />'+
'</form>',
});
await nextTick();
let fieldReadonly = fixture.querySelector('.o_field_widget[name="body_html"]');
let fieldEdit = fixture.querySelector('.o_field_widget[name="body_arch"]');
assert.strictEqual($(fieldReadonly).css('display'), 'none', "should hide the readonly mode");
assert.strictEqual($(fieldEdit).css('display'), 'block', "should display the edit mode");
});
QUnit.test('component destroyed while loading', async function (assert) {
const def = makeDeferred();
patchWithCleanup(assets, {
loadBundle() {
assert.step("loadBundle");
return def;
}
})
await makeView({
type: "form",
resModel: 'mailing.mailing',
resId: 1,
serverData,
arch: `
<form>
<field name="display_name"/>
<field name="body_arch" widget="mass_mailing_html" attrs="{'invisible': [['display_name', '=', 'hide']]}"/>
</form>`,
});
assert.containsOnce(fixture, ".o_field_widget[name=body_arch]");
await editInput(fixture, ".o_field_widget[name=display_name] input", "hide");
assert.containsNone(fixture, ".o_field_widget[name=body_arch]");
def.resolve();
await nextTick();
assert.containsNone(fixture, ".o_field_widget[name=body_arch]");
assert.verifySteps(["loadBundle"]);
});
});
});