19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:53 +01:00
parent dc68f80d3f
commit 7221b9ac46
610 changed files with 135477 additions and 161677 deletions

View file

@ -0,0 +1,282 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { beforeEach, expect, test } from "@odoo/hoot";
import { animationFrame, Deferred, queryAllTexts } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fields,
models,
mountView,
onRpc,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { user } from "@web/core/user";
import { AnimatedNumber } from "@web/views/view_components/animated_number";
class Users extends models.Model {
name = fields.Char();
_records = [
{ id: 1, name: "Dhvanil" },
{ id: 2, name: "Trivedi" },
];
}
class Stage extends models.Model {
_name = "crm.stage";
name = fields.Char();
is_won = fields.Boolean({ string: "Is won" });
_records = [
{ id: 1, name: "New" },
{ id: 2, name: "Qualified" },
{ id: 3, name: "Won", is_won: true },
];
}
class Lead extends models.Model {
_name = "crm.lead";
name = fields.Char();
bar = fields.Boolean();
activity_state = fields.Char({ string: "Activity State" });
expected_revenue = fields.Integer({ string: "Revenue", sortable: true, aggregator: "sum" });
recurring_revenue_monthly = fields.Integer({
string: "Recurring Revenue",
sortable: true,
aggregator: "sum",
});
stage_id = fields.Many2one({ string: "Stage", relation: "crm.stage" });
user_id = fields.Many2one({ string: "Salesperson", relation: "users" });
_records = [
{
id: 1,
bar: false,
name: "Lead 1",
activity_state: "planned",
expected_revenue: 125,
recurring_revenue_monthly: 5,
stage_id: 1,
user_id: 1,
},
{
id: 2,
bar: true,
name: "Lead 2",
activity_state: "today",
expected_revenue: 5,
stage_id: 2,
user_id: 2,
},
{
id: 3,
bar: true,
name: "Lead 3",
activity_state: "planned",
expected_revenue: 13,
recurring_revenue_monthly: 20,
stage_id: 3,
user_id: 1,
},
{
id: 4,
bar: true,
name: "Lead 4",
activity_state: "today",
expected_revenue: 4,
stage_id: 2,
user_id: 2,
},
{
id: 5,
bar: false,
name: "Lead 5",
activity_state: "overdue",
expected_revenue: 8,
recurring_revenue_monthly: 25,
stage_id: 3,
user_id: 1,
},
{
id: 6,
bar: true,
name: "Lead 4",
activity_state: "today",
expected_revenue: 4,
recurring_revenue_monthly: 15,
stage_id: 1,
user_id: 2,
},
];
}
defineModels([Lead, Users, Stage]);
defineMailModels();
beforeEach(() => {
patchWithCleanup(AnimatedNumber, { enableAnimations: false });
patchWithCleanup(user, { hasGroup: (group) => group === "crm.group_use_recurring_revenues" });
});
test("Progressbar: do not show sum of MRR if recurring revenues is not enabled", async () => {
patchWithCleanup(user, { hasGroup: () => false });
await mountView({
type: "kanban",
resModel: "crm.lead",
groupBy: ["stage_id"],
arch: `
<kanban js_class="crm_kanban">
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="card" class="flex-row justify-content-between">
<field name="name" class="p-2"/>
<field name="recurring_revenue_monthly" class="p-2"/>
</t>
</templates>
</kanban>`,
});
expect(queryAllTexts(".o_kanban_counter")).toEqual(["129", "9", "21"], {
message: "counter should not display recurring_revenue_monthly content",
});
});
test("Progressbar: ensure correct MRR sum is displayed if recurring revenues is enabled", async () => {
await mountView({
type: "kanban",
resModel: "crm.lead",
groupBy: ["stage_id"],
arch: `
<kanban js_class="crm_kanban">
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="card" class="flex-row justify-content-between">
<field name="name" class="p-2"/>
<field name="recurring_revenue_monthly" class="p-2"/>
</t>
</templates>
</kanban>`,
});
// When no values are given in column it should return 0 and counts value if given
// MRR=0 shouldn't be displayed, however.
expect(queryAllTexts(".o_kanban_counter")).toEqual(["129\n+20", "9", "21\n+45"], {
message: "counter should display the sum of recurring_revenue_monthly values",
});
});
test.tags("desktop");
test("Progressbar: ensure correct MRR updation after state change", async () => {
await mountView({
type: "kanban",
resModel: "crm.lead",
groupBy: ["bar"],
arch: `
<kanban js_class="crm_kanban">
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="card" class="flex-row justify-content-between">
<field name="name" class="p-2"/>
<field name="expected_revenue" class="p-2"/>
<field name="recurring_revenue_monthly" class="p-2"/>
</t>
</templates>
</kanban>`,
});
//MRR before state change
expect(queryAllTexts(".o_animated_number[data-tooltip='Recurring Revenue']")).toEqual(
["+30", "+35"],
{
message: "counter should display the sum of recurring_revenue_monthly values",
}
);
// Drag the first kanban record from 1st column to the top of the last column
await contains(".o_kanban_record:first").dragAndDrop(".o_kanban_record:last");
//check MRR after drag&drop
expect(queryAllTexts(".o_animated_number[data-tooltip='Recurring Revenue']")).toEqual(
["+25", "+40"],
{
message:
"counter should display the sum of recurring_revenue_monthly correctly after drag and drop",
}
);
//Activate "planned" filter on first column
await contains('.o_kanban_group:eq(1) .progress-bar[aria-valuenow="2"]').click();
//check MRR after applying filter
expect(queryAllTexts(".o_animated_number[data-tooltip='Recurring Revenue']")).toEqual(
["+25", "+25"],
{
message:
"counter should display the sum of recurring_revenue_monthly only of overdue filter in 1st column",
}
);
});
test.tags("desktop");
test("Quickly drag&drop records when grouped by stage_id", async () => {
const def = new Deferred();
await mountView({
type: "kanban",
resModel: "crm.lead",
groupBy: ["stage_id"],
arch: `
<kanban js_class="crm_kanban">
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="card" class="flex-row justify-content-between">
<field name="name" class="p-2"/>
<field name="expected_revenue" class="p-2"/>
<field name="recurring_revenue_monthly" class="p-2"/>
</t>
</templates>
</kanban>`,
});
onRpc("web_save", async () => {
await def;
});
expect(".o_kanban_group").toHaveCount(3);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(2);
// drag the first record of the first column on top of the second column
await contains(".o_kanban_group:eq(0) .o_kanban_record").dragAndDrop(
".o_kanban_group:eq(1) .o_kanban_record"
);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(3);
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(2);
// drag that same record to the third column -> should have no effect as save still pending
// (but mostly, should not crash)
await contains(".o_kanban_group:eq(1) .o_kanban_record").dragAndDrop(
".o_kanban_group:eq(2) .o_kanban_record"
);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(3);
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(2);
def.resolve();
await animationFrame();
// drag that same record to the third column
await contains(".o_kanban_group:eq(1) .o_kanban_record").dragAndDrop(
".o_kanban_group:eq(2) .o_kanban_record"
);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(1);
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2);
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(3);
});

View file

@ -1,260 +0,0 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
click,
dragAndDrop,
getFixture,
makeDeferred,
nextTick,
patchWithCleanup,
} from '@web/../tests/helpers/utils';
import { KanbanAnimatedNumber } from "@web/views/kanban/kanban_animated_number";
const serviceRegistry = registry.category("services");
let target;
let serverData;
QUnit.module('Crm Kanban Progressbar', {
beforeEach: function () {
patchWithCleanup(KanbanAnimatedNumber, { enableAnimations: false });
serverData = {
models: {
'res.users': {
fields: {
display_name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Dhvanil' },
{ id: 2, name: 'Trivedi' },
],
},
'crm.stage': {
fields: {
display_name: { string: 'Name', type: 'char' },
is_won: { string: 'Is won', type: 'boolean' },
},
records: [
{ id: 1, name: 'New' },
{ id: 2, name: 'Qualified' },
{ id: 3, name: 'Won', is_won: true },
],
},
'crm.lead': {
fields: {
display_name: { string: 'Name', type: 'char' },
bar: {string: "Bar", type: "boolean"},
activity_state: {string: "Activity State", type: "char"},
expected_revenue: { string: 'Revenue', type: 'integer', sortable: true },
recurring_revenue_monthly: { string: 'Recurring Revenue', type: 'integer', sortable: true },
stage_id: { string: 'Stage', type: 'many2one', relation: 'crm.stage' },
user_id: { string: 'Salesperson', type: 'many2one', relation: 'res.users' },
},
records : [
{ id: 1, bar: false, name: 'Lead 1', activity_state: 'planned', expected_revenue: 125, recurring_revenue_monthly: 5, stage_id: 1, user_id: 1 },
{ id: 2, bar: true, name: 'Lead 2', activity_state: 'today', expected_revenue: 5, stage_id: 2, user_id: 2 },
{ id: 3, bar: true, name: 'Lead 3', activity_state: 'planned', expected_revenue: 13, recurring_revenue_monthly: 20, stage_id: 3, user_id: 1 },
{ id: 4, bar: true, name: 'Lead 4', activity_state: 'today', expected_revenue: 4, stage_id: 2, user_id: 2 },
{ id: 5, bar: false, name: 'Lead 5', activity_state: 'overdue', expected_revenue: 8, recurring_revenue_monthly: 25, stage_id: 3, user_id: 1 },
{ id: 6, bar: true, name: 'Lead 4', activity_state: 'today', expected_revenue: 4, recurring_revenue_monthly: 15, stage_id: 1, user_id: 2 },
],
},
},
views: {},
};
target = getFixture();
setupViewRegistries();
serviceRegistry.add(
"user",
makeFakeUserService((group) => group === "crm.group_use_recurring_revenues"),
{ force: true },
);
},
}, function () {
QUnit.test("Progressbar: do not show sum of MRR if recurring revenues is not enabled", async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
serverData,
resModel: 'crm.lead',
groupBy: ['stage_id'],
arch: `
<kanban js_class="crm_kanban">
<field name="stage_id"/>
<field name="expected_revenue"/>
<field name="recurring_revenue_monthly"/>
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
<div><field name="recurring_revenue_monthly"/></div>
</t>
</templates>
</kanban>`,
});
const reccurringRevenueNoValues = [...target.querySelectorAll('.o_crm_kanban_mrr_counter_side')].map((elem) => elem.textContent)
assert.deepEqual(reccurringRevenueNoValues, [],
"counter should not display recurring_revenue_monthly content");
});
QUnit.test("Progressbar: ensure correct MRR sum is displayed if recurring revenues is enabled", async function (assert) {
assert.expect(1);
await makeView({
type: "kanban",
serverData,
resModel: 'crm.lead',
groupBy: ['stage_id'],
arch: `
<kanban js_class="crm_kanban">
<field name="stage_id"/>
<field name="expected_revenue"/>
<field name="recurring_revenue_monthly"/>
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
<div><field name="recurring_revenue_monthly"/></div>
</t>
</templates>
</kanban>`,
});
const reccurringRevenueValues = [...target.querySelectorAll('.o_kanban_counter_side:nth-child(3)')].map((elem) => elem.textContent);
// When no values are given in column it should return 0 and counts value if given.
assert.deepEqual(reccurringRevenueValues, ["+20", "+0", "+45"],
"counter should display the sum of recurring_revenue_monthly values if values are given else display 0");
});
QUnit.test("Progressbar: ensure correct MRR updation after state change", async function (assert) {
assert.expect(3);
await makeView({
type: "kanban",
serverData,
resModel: 'crm.lead',
groupBy: ['bar'],
arch: `
<kanban js_class="crm_kanban">
<field name="stage_id"/>
<field name="expected_revenue"/>
<field name="recurring_revenue_monthly"/>
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
<div><field name="expected_revenue"/></div>
<div><field name="recurring_revenue_monthly"/></div>
</t>
</templates>
</kanban>`,
});
//MRR before state change
let reccurringRevenueNoValues = [...target.querySelectorAll('.o_kanban_counter_side:nth-child(3)')].map((elem) => elem.textContent);
assert.deepEqual(reccurringRevenueNoValues, ['+30','+35'],
"counter should display the sum of recurring_revenue_monthly values");
// Drag the first kanban record from 1st column to the top of the last column
await dragAndDrop(
[...target.querySelectorAll('.o_kanban_record')].shift(),
[...target.querySelectorAll('.o_kanban_record')].pop(),
{ position: 'bottom' }
);
//check MRR after drag&drop
reccurringRevenueNoValues = [...target.querySelectorAll('.o_kanban_counter_side:nth-child(3)')].map((elem) => elem.textContent);
assert.deepEqual(reccurringRevenueNoValues, ['+25', '+40'],
"counter should display the sum of recurring_revenue_monthly correctly after drag and drop");
//Activate "planned" filter on first column
await click(target.querySelector('.o_kanban_group:nth-child(2) .progress-bar[aria-valuenow="2"]'), null);
//check MRR after applying filter
reccurringRevenueNoValues = [...target.querySelectorAll('.o_kanban_counter_side:nth-child(3)')].map((elem) => elem.textContent);
assert.deepEqual(reccurringRevenueNoValues, ['+25','+25'],
"counter should display the sum of recurring_revenue_monthly only of overdue filter in 1st column");
});
QUnit.test("Quickly drag&drop records when grouped by stage_id", async function (assert) {
const def = makeDeferred();
await makeView({
type: "kanban",
serverData,
resModel: 'crm.lead',
groupBy: ['stage_id'],
arch: `
<kanban js_class="crm_kanban">
<field name="stage_id"/>
<field name="expected_revenue"/>
<field name="recurring_revenue_monthly"/>
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}' sum_field="expected_revenue" recurring_revenue_sum_field="recurring_revenue_monthly"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
<div><field name="expected_revenue"/></div>
<div><field name="recurring_revenue_monthly"/></div>
</t>
</templates>
</kanban>`,
async mockRPC(route, args) {
if (args.method === "write") {
await def;
}
}
});
assert.containsN(target, ".o_kanban_group", 3);
assert.containsN(target.querySelectorAll(".o_kanban_group")[0], ".o_kanban_record", 2);
assert.containsN(target.querySelectorAll(".o_kanban_group")[1], ".o_kanban_record", 2);
assert.containsN(target.querySelectorAll(".o_kanban_group")[2], ".o_kanban_record", 2);
// drag the first record of the first column on top of the second column
await dragAndDrop(
target.querySelectorAll('.o_kanban_group')[0].querySelector('.o_kanban_record'),
target.querySelectorAll('.o_kanban_group')[1].querySelector('.o_kanban_record'),
{ position: 'top' }
);
assert.containsOnce(target.querySelectorAll(".o_kanban_group")[0], ".o_kanban_record");
assert.containsN(target.querySelectorAll(".o_kanban_group")[1], ".o_kanban_record", 3);
assert.containsN(target.querySelectorAll(".o_kanban_group")[2], ".o_kanban_record", 2);
// drag that same record to the third column -> should have no effect as save still pending
// (but mostly, should not crash)
await dragAndDrop(
target.querySelectorAll('.o_kanban_group')[1].querySelector('.o_kanban_record'),
target.querySelectorAll('.o_kanban_group')[2].querySelector('.o_kanban_record'),
{ position: 'top' }
);
assert.containsOnce(target.querySelectorAll(".o_kanban_group")[0], ".o_kanban_record");
assert.containsN(target.querySelectorAll(".o_kanban_group")[1], ".o_kanban_record", 3);
assert.containsN(target.querySelectorAll(".o_kanban_group")[2], ".o_kanban_record", 2);
def.resolve();
await nextTick();
// drag that same record to the third column
await dragAndDrop(
target.querySelectorAll('.o_kanban_group')[1].querySelector('.o_kanban_record'),
target.querySelectorAll('.o_kanban_group')[2].querySelector('.o_kanban_record'),
{ position: 'top' }
);
assert.containsOnce(target.querySelectorAll(".o_kanban_group")[0], ".o_kanban_record");
assert.containsN(target.querySelectorAll(".o_kanban_group")[1], ".o_kanban_record", 2);
assert.containsN(target.querySelectorAll(".o_kanban_group")[2], ".o_kanban_record", 3);
});
});

View file

@ -0,0 +1,70 @@
import { onRpc } from "@web/../tests/web_test_helpers";
import { deserializeDateTime } from "@web/core/l10n/dates";
onRpc("get_rainbowman_message", function getRainbowmanMessage({ args, model }) {
let message = false;
if (model !== "crm.lead") {
return message;
}
const records = this.env["crm.lead"];
const record = records.browse(args[0])[0];
const won_stage = this.env["crm.stage"].search_read([["is_won", "=", true]])[0];
if (
record.stage_id === won_stage.id &&
record.user_id &&
record.team_id &&
record.planned_revenue > 0
) {
const now = luxon.DateTime.now();
const query_result = {};
// Total won
query_result["total_won"] = records.filter(
(r) => r.stage_id === won_stage.id && r.user_id === record.user_id
).length;
// Max team 30 days
const recordsTeam30 = records.filter(
(r) =>
r.stage_id === won_stage.id &&
r.team_id === record.team_id &&
(!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as("days") <= 30)
);
query_result["max_team_30"] = Math.max(...recordsTeam30.map((r) => r.planned_revenue));
// Max team 7 days
const recordsTeam7 = records.filter(
(r) =>
r.stage_id === won_stage.id &&
r.team_id === record.team_id &&
(!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as("days") <= 7)
);
query_result["max_team_7"] = Math.max(...recordsTeam7.map((r) => r.planned_revenue));
// Max User 30 days
const recordsUser30 = records.filter(
(r) =>
r.stage_id === won_stage.id &&
r.user_id === record.user_id &&
(!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as("days") <= 30)
);
query_result["max_user_30"] = Math.max(...recordsUser30.map((r) => r.planned_revenue));
// Max User 7 days
const recordsUser7 = records.filter(
(r) =>
r.stage_id === won_stage.id &&
r.user_id === record.user_id &&
(!r.date_closed || now.diff(deserializeDateTime(r.date_closed)).as("days") <= 7)
);
query_result["max_user_7"] = Math.max(...recordsUser7.map((r) => r.planned_revenue));
if (query_result.total_won === 1) {
message = "Go, go, go! Congrats for your first deal.";
} else if (query_result.max_team_30 === record.planned_revenue) {
message = "Boom! Team record for the past 30 days.";
} else if (query_result.max_team_7 === record.planned_revenue) {
message = "Yeah! Best deal out of the last 7 days for the team.";
} else if (query_result.max_user_30 === record.planned_revenue) {
message = "You just beat your personal record for the past 30 days.";
} else if (query_result.max_user_7 === record.planned_revenue) {
message = "You just beat your personal record for the past 7 days.";
}
}
return message;
});

View file

@ -0,0 +1,444 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import {
contains,
defineModels,
fields,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
import { serializeDateTime } from "@web/core/l10n/dates";
const now = luxon.DateTime.now();
class Users extends models.Model {
name = fields.Char();
_records = [
{ id: 1, name: "Mario" },
{ id: 2, name: "Luigi" },
{ id: 3, name: "Link" },
{ id: 4, name: "Zelda" },
];
}
class Team extends models.Model {
_name = "crm.team";
name = fields.Char();
member_ids = fields.Many2many({ string: "Members", relation: "users" });
_records = [
{ id: 1, name: "Mushroom Kingdom", member_ids: [1, 2] },
{ id: 2, name: "Hyrule", member_ids: [3, 4] },
];
}
class Stage extends models.Model {
_name = "crm.stage";
name = fields.Char();
is_won = fields.Boolean({ string: "Is won" });
_records = [
{ id: 1, name: "Start" },
{ id: 2, name: "Middle" },
{ id: 3, name: "Won", is_won: true },
];
}
class Lead extends models.Model {
_name = "crm.lead";
name = fields.Char();
planned_revenue = fields.Float({ string: "Revenue" });
date_closed = fields.Datetime({ string: "Date closed" });
stage_id = fields.Many2one({ string: "Stage", relation: "crm.stage" });
user_id = fields.Many2one({ string: "Salesperson", relation: "users" });
team_id = fields.Many2one({ string: "Sales Team", relation: "crm.team" });
_records = [
{
id: 1,
name: "Lead 1",
planned_revenue: 5.0,
stage_id: 1,
team_id: 1,
user_id: 1,
},
{
id: 2,
name: "Lead 2",
planned_revenue: 5.0,
stage_id: 2,
team_id: 2,
user_id: 4,
},
{
id: 3,
name: "Lead 3",
planned_revenue: 3.0,
stage_id: 3,
team_id: 1,
user_id: 1,
date_closed: serializeDateTime(now.minus({ days: 5 })),
},
{
id: 4,
name: "Lead 4",
planned_revenue: 4.0,
stage_id: 3,
team_id: 2,
user_id: 4,
date_closed: serializeDateTime(now.minus({ days: 23 })),
},
{
id: 5,
name: "Lead 5",
planned_revenue: 7.0,
stage_id: 3,
team_id: 1,
user_id: 1,
date_closed: serializeDateTime(now.minus({ days: 20 })),
},
{
id: 6,
name: "Lead 6",
planned_revenue: 4.0,
stage_id: 2,
team_id: 1,
user_id: 2,
},
{
id: 7,
name: "Lead 7",
planned_revenue: 1.8,
stage_id: 3,
team_id: 2,
user_id: 3,
date_closed: serializeDateTime(now.minus({ days: 23 })),
},
{
id: 8,
name: "Lead 8",
planned_revenue: 1.9,
stage_id: 1,
team_id: 2,
user_id: 3,
},
{
id: 9,
name: "Lead 9",
planned_revenue: 1.5,
stage_id: 3,
team_id: 2,
user_id: 3,
date_closed: serializeDateTime(now.minus({ days: 5 })),
},
{
id: 10,
name: "Lead 10",
planned_revenue: 1.7,
stage_id: 2,
team_id: 2,
user_id: 3,
},
{
id: 11,
name: "Lead 11",
planned_revenue: 2.0,
stage_id: 3,
team_id: 2,
user_id: 4,
date_closed: serializeDateTime(now.minus({ days: 5 })),
},
];
}
defineModels([Lead, Users, Stage, Team]);
defineMailModels();
const testFormView = {
arch: `
<form js_class="crm_form">
<header><field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/></header>
<field name="name"/>
<field name="planned_revenue"/>
<field name="team_id"/>
<field name="user_id"/>
</form>`,
type: "form",
resModel: "crm.lead",
};
const testKanbanView = {
arch: `
<kanban js_class="crm_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>`,
resModel: "crm.lead",
type: "kanban",
groupBy: ["stage_id"],
};
onRpc("crm.lead", "get_rainbowman_message", ({ parent }) => {
const result = parent();
expect.step(result || "no rainbowman");
return result;
});
test.tags("desktop");
test("first lead won, click on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 6,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Go, go, go! Congrats for your first deal."]);
});
test.tags("mobile");
test("first lead won, click on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 6,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Go, go, go! Congrats for your first deal."]);
});
test.tags("desktop");
test("first lead won, click on statusbar in edit mode on desktop", async () => {
await mountView({
...testFormView,
resId: 6,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Go, go, go! Congrats for your first deal."]);
});
test.tags("mobile");
test("first lead won, click on statusbar in edit mode on mobile", async () => {
await mountView({
...testFormView,
resId: 6,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Go, go, go! Congrats for your first deal."]);
});
test.tags("desktop");
test("team record 30 days, click on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 2,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Boom! Team record for the past 30 days."]);
});
test.tags("mobile");
test("team record 30 days, click on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 2,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Boom! Team record for the past 30 days."]);
});
test.tags("desktop");
test("team record 7 days, click on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 1,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Yeah! Best deal out of the last 7 days for the team."]);
});
test.tags("mobile");
test("team record 7 days, click on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 1,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Yeah! Best deal out of the last 7 days for the team."]);
});
test.tags("desktop");
test("user record 30 days, click on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 8,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 30 days."]);
});
test.tags("mobile");
test("user record 30 days, click on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 8,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 30 days."]);
});
test.tags("desktop");
test("user record 7 days, click on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 10,
});
await contains(".o_statusbar_status button[data-value='3']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 7 days."]);
});
test.tags("mobile");
test("user record 7 days, click on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 10,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Won')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 7 days."]);
});
test.tags("desktop");
test("click on stage (not won) on statusbar on desktop", async () => {
await mountView({
...testFormView,
resId: 1,
});
await contains(".o_statusbar_status button[data-value='2']").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(0);
expect.verifySteps(["no rainbowman"]);
});
test.tags("mobile");
test("click on stage (not won) on statusbar on mobile", async () => {
await mountView({
...testFormView,
resId: 1,
});
await contains(".o_statusbar_status button.dropdown-toggle").click();
await contains(".o-dropdown--menu .dropdown-item:contains('Middle')").click();
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(0);
expect.verifySteps(["no rainbowman"]);
});
test.tags("desktop");
test("first lead won, drag & drop kanban", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 6):eq(0)").dragAndDrop(".o_kanban_group:eq(2)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Go, go, go! Congrats for your first deal."]);
});
test.tags("desktop");
test("team record 30 days, drag & drop kanban", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 2):eq(0)").dragAndDrop(".o_kanban_group:eq(2)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Boom! Team record for the past 30 days."]);
});
test.tags("desktop");
test("team record 7 days, drag & drop kanban", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 1):eq(0)").dragAndDrop(".o_kanban_group:eq(2)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["Yeah! Best deal out of the last 7 days for the team."]);
});
test.tags("desktop");
test("user record 30 days, drag & drop kanban", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 8):eq(0)").dragAndDrop(".o_kanban_group:eq(2)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 30 days."]);
});
test.tags("desktop");
test("user record 7 days, drag & drop kanban", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 10):eq(0)").dragAndDrop(".o_kanban_group:eq(2)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(1);
expect.verifySteps(["You just beat your personal record for the past 7 days."]);
});
test.tags("desktop");
test("drag & drop record kanban in stage not won", async () => {
await mountView({
...testKanbanView,
});
await contains(".o_kanban_record:contains(Lead 8):eq(0)").dragAndDrop(".o_kanban_group:eq(1)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(0);
expect.verifySteps(["no rainbowman"]);
});
test.tags("desktop");
test("drag & drop record in kanban not grouped by stage_id", async () => {
await mountView({
...testKanbanView,
groupBy: ["user_id"],
});
await contains(".o_kanban_group:eq(0)").dragAndDrop(".o_kanban_group:eq(1)");
expect(".o_reward svg.o_reward_rainbow_man").toHaveCount(0);
expect.verifySteps([]); // Should never pass by the rpc
});

View file

@ -1,345 +0,0 @@
/** @odoo-module **/
import "@crm/../tests/mock_server";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
click,
dragAndDrop,
getFixture,
selectDropdownItem,
} from '@web/../tests/helpers/utils';
import testUtils from 'web.test_utils';
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
import { startServer } from '@bus/../tests/helpers/mock_python_environment';
import { start } from "@mail/../tests/helpers/test_utils";
addModelNamesToFetch(["crm.stage", "crm.lead"]);
const find = testUtils.dom.find;
let target;
function getMockRpc(assert) {
return async (route, args, performRpc) => {
const result = await performRpc(route, args);
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
assert.step(result || "no rainbowman");
}
return result;
};
}
QUnit.module('Crm Rainbowman Triggers', {
beforeEach: function () {
const format = "YYYY-MM-DD HH:mm:ss";
const serverData = {
models: {
'res.users': {
fields: {
display_name: { string: 'Name', type: 'char' },
},
records: [
{ id: 1, name: 'Mario' },
{ id: 2, name: 'Luigi' },
{ id: 3, name: 'Link' },
{ id: 4, name: 'Zelda' },
],
},
'crm.team': {
fields: {
display_name: { string: 'Name', type: 'char' },
member_ids: { string: 'Members', type: 'many2many', relation: 'res.users' },
},
records: [
{ id: 1, name: 'Mushroom Kingdom', member_ids: [1, 2] },
{ id: 2, name: 'Hyrule', member_ids: [3, 4] },
],
},
'crm.stage': {
fields: {
display_name: { string: 'Name', type: 'char' },
is_won: { string: 'Is won', type: 'boolean' },
},
records: [
{ id: 1, name: 'Start' },
{ id: 2, name: 'Middle' },
{ id: 3, name: 'Won', is_won: true},
],
},
'crm.lead': {
fields: {
display_name: { string: 'Name', type: 'char' },
planned_revenue: { string: 'Revenue', type: 'float' },
stage_id: { string: 'Stage', type: 'many2one', relation: 'crm.stage' },
team_id: { string: 'Sales Team', type: 'many2one', relation: 'crm.team' },
user_id: { string: 'Salesperson', type: 'many2one', relation: 'res.users' },
date_closed: { string: 'Date closed', type: 'datetime' },
},
records : [
{ id: 1, name: 'Lead 1', planned_revenue: 5.0, stage_id: 1, team_id: 1, user_id: 1 },
{ id: 2, name: 'Lead 2', planned_revenue: 5.0, stage_id: 2, team_id: 2, user_id: 4 },
{ id: 3, name: 'Lead 3', planned_revenue: 3.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(5, 'days').format(format) },
{ id: 4, name: 'Lead 4', planned_revenue: 4.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(23, 'days').format(format) },
{ id: 5, name: 'Lead 5', planned_revenue: 7.0, stage_id: 3, team_id: 1, user_id: 1, date_closed: moment().subtract(20, 'days').format(format) },
{ id: 6, name: 'Lead 6', planned_revenue: 4.0, stage_id: 2, team_id: 1, user_id: 2 },
{ id: 7, name: 'Lead 7', planned_revenue: 1.8, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(23, 'days').format(format) },
{ id: 8, name: 'Lead 8', planned_revenue: 1.9, stage_id: 1, team_id: 2, user_id: 3 },
{ id: 9, name: 'Lead 9', planned_revenue: 1.5, stage_id: 3, team_id: 2, user_id: 3, date_closed: moment().subtract(5, 'days').format(format) },
{ id: 10, name: 'Lead 10', planned_revenue: 1.7, stage_id: 2, team_id: 2, user_id: 3 },
{ id: 11, name: 'Lead 11', planned_revenue: 2.0, stage_id: 3, team_id: 2, user_id: 4, date_closed: moment().subtract(5, 'days').format(format) },
],
},
},
views: {},
};
this.testFormView = {
arch: `
<form js_class="crm_form">
<header><field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/></header>
<field name="name"/>
<field name="planned_revenue"/>
<field name="team_id"/>
<field name="user_id"/>
</form>`,
serverData,
type: "form",
resModel: 'crm.lead',
};
this.testKanbanView = {
arch: `
<kanban js_class="crm_kanban">
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
serverData,
resModel: 'crm.lead',
type: "kanban",
groupBy: ['stage_id'],
};
target = getFixture();
setupViewRegistries();
},
}, function () {
QUnit.test("first lead won, click on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 6,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
});
QUnit.test("first lead won, click on statusbar in edit mode", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 6,
mockRPC: getMockRpc(assert),
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
});
QUnit.test("team record 30 days, click on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 2,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Boom! Team record for the past 30 days.']);
});
QUnit.test("team record 7 days, click on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 1,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['Yeah! Deal of the last 7 days for the team.']);
});
QUnit.test("user record 30 days, click on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 8,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['You just beat your personal record for the past 30 days.']);
});
QUnit.test("user record 7 days, click on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 10,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='3']"));
assert.verifySteps(['You just beat your personal record for the past 7 days.']);
});
QUnit.test("click on stage (not won) on statusbar", async function (assert) {
assert.expect(2);
await makeView({
...this.testFormView,
resId: 1,
mockRPC: getMockRpc(assert),
mode: "readonly",
});
await click(target.querySelector(".o_statusbar_status button[data-value='2']"));
assert.verifySteps(['no rainbowman']);
});
QUnit.test("first lead won, drag & drop kanban", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 6"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
});
QUnit.test("team record 30 days, drag & drop kanban", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 2"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Boom! Team record for the past 30 days.']);
});
QUnit.test("team record 7 days, drag & drop kanban", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 1"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Yeah! Deal of the last 7 days for the team.']);
});
QUnit.test("user record 30 days, drag & drop kanban", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 8"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['You just beat your personal record for the past 30 days.']);
});
QUnit.test("user record 7 days, drag & drop kanban", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 10"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['You just beat your personal record for the past 7 days.']);
});
QUnit.test("drag & drop record kanban in stage not won", async function (assert) {
assert.expect(2);
await makeView({
...this.testKanbanView,
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 8"), target.querySelector('.o_kanban_group:nth-of-type(2)'));
assert.verifySteps(["no rainbowman"]);
});
QUnit.test("drag & drop record in kanban not grouped by stage_id", async function (assert) {
assert.expect(1);
await makeView({
...this.testKanbanView,
groupBy: ["user_id"],
mockRPC: getMockRpc(assert),
});
await dragAndDrop(target.querySelector('.o_kanban_group:nth-of-type(1)'), target.querySelector('.o_kanban_group:nth-of-type(2)'));
assert.verifySteps([]); // Should never pass by the rpc
});
QUnit.test("send a message on a new record after changing the stage", async function (assert) {
assert.expect(1);
const pyEnv = await startServer();
pyEnv["crm.stage"].create({ name : "Dummy Stage", is_won: true });
const views = {
"crm.lead,false,form": `
<form js_class="crm_form">
<sheet>
<field name="stage_id"/>
</sheet>
<div class="oe_chatter">
<field name="message_ids" options="{'open_attachments': True}"/>
</div>
</form>`,
};
const messageBody = "some message";
const { insertText, openView } = await start({
serverData: { views },
mockRPC: function (route, args) {
if (route === "/mail/message/post") {
assert.deepEqual(args.post_data.body, messageBody);
}
}
});
await openView({
res_model: "crm.lead",
views: [[false, "form"]],
});
await selectDropdownItem(target, "stage_id", "Dummy Stage");
await click(target, ".o_ChatterTopbar_buttonSendMessage");
await insertText(".o_ComposerTextInput_textarea", messageBody);
await click(target, ".o_Composer_buttonSend");
});
});

View file

@ -0,0 +1,12 @@
import { CrmLead } from "@crm/../tests/mock_server/mock_models/crm_lead";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels } from "@web/../tests/web_test_helpers";
export const crmModels = {
...mailModels,
CrmLead
};
export function defineCrmModels() {
defineModels(crmModels);
}

View file

@ -0,0 +1,371 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { queryAll, queryAllTexts, runAllTimers } from "@odoo/hoot-dom";
import { mockDate } from "@odoo/hoot-mock";
import {
contains,
defineModels,
fields,
getService,
models,
mountView,
onRpc,
quickCreateKanbanColumn,
} from "@web/../tests/web_test_helpers";
class Lead extends models.Model {
_name = "crm.lead";
name = fields.Char();
date_deadline = fields.Date({ string: "Expected closing" });
}
defineModels([Lead]);
defineMailModels();
const kanbanArch = `
<kanban js_class="forecast_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`;
test.tags("desktop");
test("filter out every records before the start of the current month with forecast_filter for a date field", async () => {
// the filter is used by the forecast model extension, and applies the forecast_filter context key,
// which adds a domain constraint on the field marked in the other context key forecast_field
mockDate("2021-02-10 00:00:00");
Lead._records = [
{ id: 1, name: "Lead 1", date_deadline: "2021-01-01" },
{ id: 2, name: "Lead 2", date_deadline: "2021-01-20" },
{ id: 3, name: "Lead 3", date_deadline: "2021-02-01" },
{ id: 4, name: "Lead 4", date_deadline: "2021-02-20" },
{ id: 5, name: "Lead 5", date_deadline: "2021-03-01" },
{ id: 6, name: "Lead 6", date_deadline: "2021-03-20" },
];
await mountView({
arch: kanbanArch,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
</search>`,
resModel: "crm.lead",
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: "date_deadline",
},
groupBy: ["date_deadline"],
});
// the filter is active
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2, {
message: "1st column (February) should contain 2 record",
});
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2, {
message: "2nd column (March) should contain 2 records",
});
// remove the filter(
await contains(".o_searchview_facet:contains(Forecast) .o_facet_remove").click();
expect(".o_kanban_group").toHaveCount(3);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2, {
message: "1st column (January) should contain 2 record",
});
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2, {
message: "2nd column (February) should contain 2 records",
});
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(2, {
message: "3nd column (March) should contain 2 records",
});
});
test.tags("desktop");
test("filter out every records before the start of the current month with forecast_filter for a datetime field", async () => {
// same as for the date field
mockDate("2021-02-10 00:00:00");
Lead._fields.date_closed = fields.Datetime({ string: "Closed Date" });
Lead._records = [
{
id: 1,
name: "Lead 1",
date_deadline: "2021-01-01",
date_closed: "2021-01-01 00:00:00",
},
{
id: 2,
name: "Lead 2",
date_deadline: "2021-01-20",
date_closed: "2021-01-20 00:00:00",
},
{
id: 3,
name: "Lead 3",
date_deadline: "2021-02-01",
date_closed: "2021-02-01 00:00:00",
},
{
id: 4,
name: "Lead 4",
date_deadline: "2021-02-20",
date_closed: "2021-02-20 00:00:00",
},
{
id: 5,
name: "Lead 5",
date_deadline: "2021-03-01",
date_closed: "2021-03-01 00:00:00",
},
{
id: 6,
name: "Lead 6",
date_deadline: "2021-03-20",
date_closed: "2021-03-20 00:00:00",
},
];
await mountView({
arch: kanbanArch,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
<filter name='groupby_date_closed' context="{'group_by':'date_closed'}"/>
</search>`,
resModel: "crm.lead",
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_closed: true,
forecast_field: "date_closed",
},
groupBy: ["date_closed"],
});
// with the filter
expect(".o_kanban_group").toHaveCount(2);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2, {
message: "1st column (February) should contain 2 record",
});
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2, {
message: "2nd column (March) should contain 2 records",
});
// remove the filter
await contains(".o_searchview_facet:contains(Forecast) .o_facet_remove").click();
expect(".o_kanban_group").toHaveCount(3);
expect(".o_kanban_group:eq(0) .o_kanban_record").toHaveCount(2, {
message: "1st column (January) should contain 2 record",
});
expect(".o_kanban_group:eq(1) .o_kanban_record").toHaveCount(2, {
message: "2nd column (February) should contain 2 records",
});
expect(".o_kanban_group:eq(2) .o_kanban_record").toHaveCount(2, {
message: "3nd column (March) should contain 2 records",
});
});
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
test("Forecast on months, until the end of the year of the latest data", async () => {
expect.assertions(3);
mockDate("2021-10-10 00:00:00");
Lead._records = [
{ id: 1, name: "Lead 1", date_deadline: "2021-01-01" },
{ id: 2, name: "Lead 2", date_deadline: "2021-02-01" },
{ id: 3, name: "Lead 3", date_deadline: "2021-11-01" },
{ id: 4, name: "Lead 4", date_deadline: "2022-01-01" },
];
onRpc("crm.lead", "web_read_group", ({ kwargs }) => {
expect(kwargs.context.fill_temporal).toEqual({
fill_from: "2021-10-01",
min_groups: 4,
});
expect(kwargs.domain).toEqual([
"&",
"|",
["date_deadline", "=", false],
["date_deadline", ">=", "2021-10-01"],
"|",
["date_deadline", "=", false],
["date_deadline", "<", "2023-01-01"],
]);
});
await mountView({
arch: kanbanArch,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
</search>`,
resModel: "crm.lead",
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: "date_deadline",
},
groupBy: ["date_deadline"],
});
expect(
getService("fillTemporalService")
.getFillTemporalPeriod({
modelName: "crm.lead",
field: {
name: "date_deadline",
type: "date",
},
granularity: "month",
})
.end.toFormat("yyyy-MM-dd")
).toBe("2022-02-01");
});
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
test("Forecast on years, until the end of the year of the latest data", async () => {
expect.assertions(3);
mockDate("2021-10-10 00:00:00");
Lead._records = [
{ id: 1, name: "Lead 1", date_deadline: "2021-01-01" },
{ id: 2, name: "Lead 2", date_deadline: "2022-02-01" },
{ id: 3, name: "Lead 3", date_deadline: "2027-11-01" },
];
onRpc("crm.lead", "web_read_group", ({ kwargs }) => {
expect(kwargs.context.fill_temporal).toEqual({
fill_from: "2021-01-01",
min_groups: 4,
});
expect(kwargs.domain).toEqual([
"&",
"|",
["date_deadline", "=", false],
["date_deadline", ">=", "2021-01-01"],
"|",
["date_deadline", "=", false],
["date_deadline", "<", "2025-01-01"],
]);
});
await mountView({
arch: kanbanArch,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline:year'}"/>
</search>`,
resModel: "crm.lead",
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: "date_deadline",
},
groupBy: ["date_deadline:year"],
});
expect(
getService("fillTemporalService")
.getFillTemporalPeriod({
modelName: "crm.lead",
field: {
name: "date_deadline",
type: "date",
},
granularity: "year",
})
.end.toFormat("yyyy-MM-dd")
).toBe("2023-01-01");
});
test.tags("desktop");
test("Forecast drag&drop and add column", async () => {
mockDate("2023-09-01 00:00:00");
Lead._fields.color = fields.Char();
Lead._fields.int_field = fields.Integer({ string: "Value" });
Lead._records = [
{ id: 1, int_field: 7, color: "d", name: "Lead 1", date_deadline: "2023-09-03" },
{ id: 2, int_field: 20, color: "w", name: "Lead 2", date_deadline: "2023-09-05" },
{ id: 3, int_field: 300, color: "s", name: "Lead 3", date_deadline: "2023-10-10" },
];
onRpc(({ route, method }) => {
expect.step(method || route);
});
await mountView({
arch: `
<kanban js_class="forecast_kanban">
<progressbar field="color" colors='{"s": "success", "w": "warning", "d": "danger"}' sum_field="int_field"/>
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>`,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
</search>`,
resModel: "crm.lead",
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: "date_deadline",
},
groupBy: ["date_deadline"],
});
const getProgressBarsColors = () =>
queryAll(".o_column_progress").map((columnProgressEl) =>
queryAll(".progress-bar", { root: columnProgressEl }).map((progressBarEl) =>
[...progressBarEl.classList].find((htmlClass) => htmlClass.startsWith("bg-"))
)
);
expect(queryAllTexts(".o_animated_number")).toEqual(["27", "300"]);
expect(getProgressBarsColors()).toEqual([["bg-warning", "bg-danger"], ["bg-success"]]);
await contains(".o_kanban_group:first .o_kanban_record").dragAndDrop(".o_kanban_group:eq(1)");
await runAllTimers();
expect(queryAllTexts(".o_animated_number")).toEqual(["20", "307"]);
expect(getProgressBarsColors()).toEqual([["bg-warning"], ["bg-success", "bg-danger"]]);
await quickCreateKanbanColumn();
// Counters and progressbars should be unchanged after adding a column.
expect(queryAllTexts(".o_animated_number")).toEqual(["20", "307"]);
expect(getProgressBarsColors()).toEqual([["bg-warning"], ["bg-success", "bg-danger"]]);
expect.verifySteps([
// mountView
"get_views",
"read_progress_bar",
"web_read_group",
"has_group",
// drag&drop
"web_save",
"read_progress_bar",
"formatted_read_group",
// add column
"read_progress_bar",
"web_read_group",
]);
});

View file

@ -1,267 +0,0 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { fillTemporalService } from "@crm/views/fill_temporal_service";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
click,
getFixture,
patchDate,
} from '@web/../tests/helpers/utils';
import testUtils from 'web.test_utils';
const find = testUtils.dom.find;
const serviceRegistry = registry.category("services");
let target;
QUnit.module('Crm Forecast Model Extension', {
beforeEach: async function () {
serviceRegistry.add("fillTemporalService", fillTemporalService);
this.testKanbanView = {
arch: `
<kanban js_class="forecast_kanban">
<field name="date_deadline"/>
<field name="date_closed"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
<filter name='groupby_date_closed' context="{'group_by':'date_closed'}"/>
</search>`,
serverData: {
models: {
'crm.lead': {
fields: {
name: {string: 'Name', type: 'char'},
date_deadline: {string: "Expected closing", type: 'date'},
date_closed: {string: "Closed Date", type: 'datetime'},
},
records: [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01', date_closed: '2021-01-01 00:00:00'},
{id: 2, name: 'Lead 2', date_deadline: '2021-01-20', date_closed: '2021-01-20 00:00:00'},
{id: 3, name: 'Lead 3', date_deadline: '2021-02-01', date_closed: '2021-02-01 00:00:00'},
{id: 4, name: 'Lead 4', date_deadline: '2021-02-20', date_closed: '2021-02-20 00:00:00'},
{id: 5, name: 'Lead 5', date_deadline: '2021-03-01', date_closed: '2021-03-01 00:00:00'},
{id: 6, name: 'Lead 6', date_deadline: '2021-03-20', date_closed: '2021-03-20 00:00:00'},
],
},
},
views: {},
},
resModel: 'crm.lead',
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: 'date_deadline',
},
groupBy: ['date_deadline'],
};
target = getFixture();
setupViewRegistries();
patchDate(2021, 1, 10, 0, 0, 0);
},
}, function () {
QUnit.test("filter out every records before the start of the current month with forecast_filter for a date field", async function (assert) {
// the filter is used by the forecast model extension, and applies the forecast_filter context key,
// which adds a domain constraint on the field marked in the other context key forecast_field
assert.expect(7);
await makeView(this.testKanbanView);
// the filter is active
assert.containsN(target, '.o_kanban_group', 2, "There should be 2 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column February should contain 2 record");
assert.containsN(target, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column March should contain 2 records");
// remove the filter
await click(find(target, '.o_searchview_facet', "Forecast"), '.o_facet_remove');
assert.containsN(target, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column January should contain 2 record");
assert.containsN(target, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column February should contain 2 records");
assert.containsN(target, '.o_kanban_group:nth-child(3) .o_kanban_record', 2,
"3nd column March should contain 2 records");
});
QUnit.test("filter out every records before the start of the current month with forecast_filter for a datetime field", async function (assert) {
// same tests as for the date field
assert.expect(7);
await makeView({
...this.testKanbanView,
context: {
search_default_forecast: true,
search_default_groupby_date_closed: true,
forecast_field: 'date_closed',
},
groupBy: ['date_closed'],
});
// with the filter
assert.containsN(target, '.o_kanban_group', 2, "There should be 2 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column February should contain 2 record");
assert.containsN(target, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column March should contain 2 records");
// remove the filter
await click(find(target, '.o_searchview_facet', "Forecast"), '.o_facet_remove');
assert.containsN(target, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
"1st column January should contain 2 record");
assert.containsN(target, '.o_kanban_group:nth-child(2) .o_kanban_record', 2,
"2nd column February should contain 2 records");
assert.containsN(target, '.o_kanban_group:nth-child(3) .o_kanban_record', 2,
"3nd column March should contain 2 records");
});
});
QUnit.module('Crm Fill Temporal Service', {
/**
* Remark: -> the filter with the groupBy is needed for the model_extension to access the groupby
* when created with makeView. Not needed in production.
* -> testKanbanView.groupBy is still needed to apply the groupby on the view
*/
beforeEach: async function () {
serviceRegistry.add("fillTemporalService", fillTemporalService);
this.testKanbanView = {
arch: `
<kanban js_class="forecast_kanban">
<field name="date_deadline"/>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>`,
searchViewArch: `
<search>
<filter name="forecast" string="Forecast" context="{'forecast_filter':1}"/>
<filter name='groupby_date_deadline' context="{'group_by':'date_deadline'}"/>
</search>`
,
serverData: {
models: {
'crm.lead': {
fields: {
name: {string: 'Name', type: 'char'},
date_deadline: {string: "Expected Closing", type: 'date'},
},
},
},
views: {},
},
resModel: 'crm.lead',
type: "kanban",
context: {
search_default_forecast: true,
search_default_groupby_date_deadline: true,
forecast_field: 'date_deadline',
},
groupBy: ['date_deadline'],
};
target = getFixture();
setupViewRegistries();
patchDate(2021, 9, 10, 0, 0, 0);
},
}, function () {
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
QUnit.test("Forecast on months, until the end of the year of the latest data", async function (assert) {
assert.expect(3);
this.testKanbanView.serverData.models['crm.lead'].records = [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01'},
{id: 2, name: 'Lead 2', date_deadline: '2021-02-01'},
{id: 3, name: 'Lead 3', date_deadline: '2021-11-01'},
{id: 4, name: 'Lead 4', date_deadline: '2022-01-01'},
];
const kanban = await makeView({
...this.testKanbanView,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/crm.lead/web_read_group') {
assert.deepEqual(args.kwargs.context.fill_temporal, {
fill_from: "2021-10-01",
min_groups: 4,
});
assert.deepEqual(args.kwargs.domain, [
"&", "|",
["date_deadline", "=", false], ["date_deadline", ">=", "2021-10-01"],
"|",
["date_deadline", "=", false], ["date_deadline", "<", "2023-01-01"],
]);
}
},
});
assert.strictEqual(kanban.env.services.fillTemporalService.getFillTemporalPeriod({
modelName: 'crm.lead',
field: {
name: 'date_deadline',
type: 'date',
},
granularity: 'month',
}).end.format('YYYY-MM-DD'), '2022-02-01');
});
/**
* Since mock_server does not support fill_temporal,
* we only check the domain and the context sent to the read_group, as well
* as the end value of the FillTemporal Service after the read_group (which should have been updated in the model)
*/
QUnit.test("Forecast on years, until the end of the year of the latest data", async function (assert) {
assert.expect(3);
this.testKanbanView.serverData.models['crm.lead'].records = [
{id: 1, name: 'Lead 1', date_deadline: '2021-01-01'},
{id: 2, name: 'Lead 2', date_deadline: '2022-02-01'},
{id: 3, name: 'Lead 3', date_deadline: '2027-11-01'},
];
const kanban = await makeView({
...this.testKanbanView,
groupBy: ['date_deadline:year'],
searchViewArch: this.testKanbanView.searchViewArch.replace("'date_deadline'", "'date_deadline:year'"),
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/crm.lead/web_read_group') {
assert.deepEqual(args.kwargs.context.fill_temporal, {
fill_from: "2021-01-01",
min_groups: 4,
});
assert.deepEqual(args.kwargs.domain, [
"&", "|",
["date_deadline", "=", false], ["date_deadline", ">=", "2021-01-01"],
"|",
["date_deadline", "=", false], ["date_deadline", "<", "2025-01-01"],
]);
}
},
});
assert.strictEqual(kanban.env.services.fillTemporalService.getFillTemporalPeriod({
modelName: 'crm.lead',
field: {
name: 'date_deadline',
type: 'date',
},
granularity: 'year',
}).end.format('YYYY-MM-DD'), '2023-01-01');
});
});

View file

@ -0,0 +1,119 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import {
defineModels,
fields,
models,
mountView,
onRpc,
toggleMenuItem,
toggleMenuItemOption,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
class Foo extends models.Model {
date_field = fields.Date({ store: true, sortable: true });
bar = fields.Many2one({ store: true, relation: "partner", sortable: true });
value = fields.Float({ store: true, sortable: true });
number = fields.Integer({ store: true, sortable: true });
_views = {
"graph,1": `<graph js_class="forecast_graph"/>`,
};
}
class Partner extends models.Model {}
defineModels([Foo, Partner]);
defineMailModels();
const forecastDomain = (forecastStart) => [
"|",
["date_field", "=", false],
["date_field", ">=", forecastStart],
];
test("Forecast graph view", async () => {
expect.assertions(5);
mockDate("2021-09-16 16:54:00");
const expectedDomains = [
forecastDomain("2021-09-01"), // month granularity due to no groupby
forecastDomain("2021-09-16"), // day granularity due to simple bar groupby
// quarter granularity due to date field groupby activated with quarter interval option
forecastDomain("2021-07-01"),
// quarter granularity due to date field groupby activated with quarter and year interval option
forecastDomain("2021-01-01"),
// forecast filter no more active
[],
];
onRpc("formatted_read_group", ({ kwargs }) => {
expect(kwargs.domain).toEqual(expectedDomains.shift());
});
await mountView({
resModel: "foo",
viewId: 1,
type: "graph",
searchViewArch: `
<search>
<filter name="forecast_filter" string="Forecast Filter" context="{ 'forecast_filter': 1 }"/>
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
<filter name="group_by_date_field" string="Date Field" context="{ 'group_by': 'date_field' }"/>
</search>
`,
context: {
search_default_forecast_filter: 1,
forecast_field: "date_field",
},
});
await toggleSearchBarMenu();
await toggleMenuItem("Bar");
await toggleMenuItem("Date Field");
await toggleMenuItemOption("Date Field", "Quarter");
await toggleMenuItemOption("Date Field", "Year");
await toggleMenuItem("Forecast Filter");
});
test("forecast filter domain is combined with other domains following the same rules as other filters (OR in same group, AND between groups)", async () => {
expect.assertions(1);
mockDate("2021-09-16 16:54:00");
onRpc("formatted_read_group", ({ kwargs }) => {
expect(kwargs.domain).toEqual([
"&",
["number", ">", 2],
"|",
["bar", "=", 2],
"&",
["value", ">", 0.0],
"|",
["date_field", "=", false],
["date_field", ">=", "2021-09-01"],
]);
});
await mountView({
resModel: "foo",
type: "graph",
viewId: 1,
searchViewArch: `
<search>
<filter name="other_group_filter" string="Other Group Filter" domain="[('number', '>', 2)]"/>
<separator/>
<filter name="same_group_filter" string="Same Group Filter" domain="[('bar', '=', 2)]"/>
<filter name="forecast_filter" string="Forecast Filter" context="{ 'forecast_filter': 1 }" domain="[('value', '>', 0.0)]"/>
</search>
`,
context: {
search_default_same_group_filter: 1,
search_default_forecast_filter: 1,
search_default_other_group_filter: 1,
forecast_field: "date_field",
},
});
});

View file

@ -1,164 +0,0 @@
/** @odoo-module **/
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { menuService } from "@web/webclient/menus/menu_service";
import {
toggleFilterMenu,
toggleGroupByMenu,
toggleMenuItem,
toggleMenuItemOption,
} from "@web/../tests/search/helpers";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
import { mock } from "web.test_utils";
import { browser } from "@web/core/browser/browser";
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
const patchDate = mock.patchDate;
const serviceRegistry = registry.category("services");
const forecastDomain = (forecastStart) => {
return ["|", ["date_field", "=", false], ["date_field", ">=", forecastStart]];
};
let serverData;
let target;
QUnit.module("Views", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
date_field: {
string: "Date Field",
type: "date",
store: true,
sortable: true,
},
bar: {
string: "Bar",
type: "many2one",
relation: "partner",
store: true,
sortable: true,
},
},
records: [],
},
partner: {},
},
views: {
"foo,false,legacy_toy": `<legacy_toy/>`,
"foo,false,graph": `<graph js_class="forecast_graph"/>`,
"foo,false,search": `
<search>
<filter name="forecast_filter" string="Forecast Filter" context="{ 'forecast_filter': 1 }"/>
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
<filter name="group_by_date_field" string="Date Field" context="{ 'group_by': 'date_field' }"/>
</search>
`,
},
};
setupViewRegistries();
serviceRegistry.add("menu", menuService);
serviceRegistry.add("cookie", fakeCookieService);
target = getFixture();
});
QUnit.module("Forecast views");
QUnit.test("Forecast graph view", async function (assert) {
assert.expect(5);
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
const unpatchDate = patchDate(2021, 8, 16, 16, 54, 0);
const expectedDomains = [
forecastDomain("2021-09-01"), // month granularity due to no groupby
forecastDomain("2021-09-16"), // day granularity due to simple bar groupby
// quarter granularity due to date field groupby activated with quarter interval option
forecastDomain("2021-07-01"),
// quarter granularity due to date field groupby activated with quarter and year interval option
forecastDomain("2021-01-01"),
// forecast filter no more active
[],
];
await makeView({
resModel: "foo",
type: "graph",
serverData,
searchViewId: false,
context: {
search_default_forecast_filter: 1,
forecast_field: "date_field",
},
mockRPC(_, args) {
if (args.method === "web_read_group") {
const { domain } = args.kwargs;
assert.deepEqual(domain, expectedDomains.shift());
}
},
});
await toggleGroupByMenu(target);
await toggleMenuItem(target, "Bar");
await toggleMenuItem(target, "Date Field");
await toggleMenuItemOption(target, "Date Field", "Quarter");
await toggleMenuItemOption(target, "Date Field", "Year");
await toggleFilterMenu(target);
await toggleMenuItem(target, "Forecast Filter");
unpatchDate();
});
QUnit.test(
"forecast filter domain is combined with other domains with an AND",
async function (assert) {
assert.expect(1);
const unpatchDate = patchDate(2021, 8, 16, 16, 54, 0);
serverData.views["foo,false,search"] = `
<search>
<filter name="other_filter" string="Other Filter" domain="[('bar', '=', 2)]"/>
<filter name="forecast_filter" string="Forecast Filter" context="{ 'forecast_filter': 1 }"/>
</search>
`;
await makeView({
resModel: "foo",
type: "graph",
serverData,
searchViewId: false,
context: {
search_default_other_filter: 1,
search_default_forecast_filter: 1,
forecast_field: "date_field",
},
mockRPC(_, args) {
if (args.method === "web_read_group") {
const { domain } = args.kwargs;
assert.deepEqual(domain, [
"&",
["bar", "=", 2],
"|",
["date_field", "=", false],
["date_field", ">=", "2021-09-01"],
]);
}
},
});
// note that the facets of the two filters are combined with an OR.
// --> current behavior in legacy
unpatchDate();
}
);
});

View file

@ -1,55 +0,0 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { MockServer } from "@web/../tests/helpers/mock_server";
patch(MockServer.prototype, "CRM", {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
async _performRPC(route, args) {
if (args.model === 'crm.lead' && args.method === 'get_rainbowman_message') {
let message = false;
const records = this.models['crm.lead'].records;
const record = records.find(r => r.id === args.args[0][0]);
const won_stage = this.models['crm.stage'].records.find(s => s.is_won);
if (record.stage_id === won_stage.id && record.user_id && record.team_id && record.planned_revenue > 0) {
const now = moment();
let query_result = {};
// Total won
query_result['total_won'] = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id).length;
// Max team 30 days
const recordsTeam30 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
query_result['max_team_30'] = Math.max(...recordsTeam30.map(r => r.planned_revenue));
// Max team 7 days
const recordsTeam7 = records.filter(r => r.stage_id === won_stage.id && r.team_id === record.team_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
query_result['max_team_7'] = Math.max(...recordsTeam7.map(r => r.planned_revenue));
// Max User 30 days
const recordsUser30 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 30));
query_result['max_user_30'] = Math.max(...recordsUser30.map(r => r.planned_revenue));
// Max User 7 days
const recordsUser7 = records.filter(r => r.stage_id === won_stage.id && r.user_id === record.user_id && (!r.date_closed || moment.duration(now.diff(moment(r.date_closed))).days() <= 7));
query_result['max_user_7'] = Math.max(...recordsUser7.map(r => r.planned_revenue));
if (query_result.total_won === 1) {
message = "Go, go, go! Congrats for your first deal.";
} else if (query_result.max_team_30 === record.planned_revenue) {
message = "Boom! Team record for the past 30 days.";
} else if (query_result.max_team_7 === record.planned_revenue) {
message = "Yeah! Deal of the last 7 days for the team.";
} else if (query_result.max_user_30 === record.planned_revenue) {
message = "You just beat your personal record for the past 30 days.";
} else if (query_result.max_user_7 === record.planned_revenue) {
message = "You just beat your personal record for the past 7 days.";
}
}
return message;
}
return this._super(...arguments);
},
});

View file

@ -0,0 +1,13 @@
import { models } from "@web/../tests/web_test_helpers";
export class CrmLead extends models.ServerModel {
_name = "crm.lead";
_views = {
form: /* xml */ `
<form string="Lead">
<sheet>
<field name="name"/>
</sheet>
</form>`,
};
}

View file

@ -1,23 +1,25 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
tour.register('create_crm_team_tour', {
url: "/web",
test: true,
}, [
...tour.stepUtils.goToAppSteps('crm.crm_menu_root'),
registry.category("web_tour.tours").add('create_crm_team_tour', {
url: "/odoo",
steps: () => [
...stepUtils.goToAppSteps('crm.crm_menu_root'),
{
trigger: 'button[data-menu-xmlid="crm.crm_menu_config"]',
run: "click",
}, {
trigger: 'a[data-menu-xmlid="crm.crm_team_config"]',
run: "click",
}, {
trigger: 'button.o_list_button_add',
run: "click",
}, {
trigger: 'input[id="name"]',
run: 'text My CRM Team',
trigger: 'input[id="name_0"]',
run: "edit My CRM Team",
}, {
trigger: 'button.o-kanban-button-new',
trigger: '.btn.o-kanban-button-new',
run: "click",
}, {
trigger: 'div.modal-dialog tr:contains("Test Salesman") input.form-check-input',
run: 'click',
@ -26,9 +28,11 @@ tour.register('create_crm_team_tour', {
run: 'click',
}, {
trigger: 'div.modal-dialog tr:contains("Test Sales Manager") input.form-check-input:checked',
run: () => {},
}, {
trigger: '.o_selection_box:contains(2)',
}, {
trigger: 'button.o_select_button',
},
...tour.stepUtils.saveForm()
]);
run: "click",
},
...stepUtils.saveForm()
]});

View file

@ -1,56 +1,25 @@
odoo.define('crm.crm_email_and_phone_propagation', function (require) {
'use strict';
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const tour = require('web_tour.tour');
tour.register('crm_email_and_phone_propagation_edit_save', {
test: true,
url: '/web',
}, [
tour.stepUtils.showAppsMenuItem(),
registry.category("web_tour.tours").add('crm_email_and_phone_propagation_edit_save', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
content: 'open crm app',
run: "click",
}, {
trigger: '.o_kanban_record .o_kanban_record_title span:contains(Test Lead Propagation)',
trigger: '.o_kanban_record:contains(Test Lead Propagation)',
content: 'Open the first lead',
run: 'click',
}, {
trigger: '.o_form_button_save',
extra_trigger: '.o_form_editable .o_field_widget[name=email_from] input',
},
{
trigger: ".o_form_editable .o_field_widget[name=email_from] input",
},
{
trigger: ".o_form_button_save:not(:visible)",
content: 'Save the lead',
run: 'click',
},
]);
tour.register('crm_email_and_phone_propagation_remove_email_and_phone', {
test: true,
url: '/web',
}, [
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
content: 'open crm app',
}, {
trigger: '.o_kanban_record .o_kanban_record_title span:contains(Test Lead Propagation)',
content: 'Open the first lead',
run: 'click',
}, {
trigger: '.o_form_editable .o_field_widget[name=email_from] input',
extra_trigger: '.o_form_editable .o_field_widget[name=phone] input',
content: 'Remove the email and the phone',
run: function (action) {
action.remove_text("", ".o_form_editable .o_field_widget[name=email_from] input");
action.remove_text("", ".o_form_editable .o_field_widget[name=phone] input");
},
}, {
trigger: '.o_back_button',
// wait the warning message to be visible
extra_trigger: '.o_form_sheet_bg .fa-exclamation-triangle:not(.o_invisible_modifier)',
content: 'Save the lead and exit to kanban',
run: 'click',
},{
trigger: '.o_kanban_renderer',
}
]);
});
]});

View file

@ -1,15 +1,15 @@
/** @odoo-module */
import tour from 'web_tour.tour';
const today = moment();
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const today = luxon.DateTime.now();
tour.register('crm_forecast', {
test: true,
url: "/web",
}, [
tour.stepUtils.showAppsMenuItem(),
registry.category("web_tour.tours").add('crm_forecast', {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: ".o_app[data-menu-xmlid='crm.crm_menu_root']",
content: "open crm app",
run: "click",
}, {
trigger: '.dropdown-toggle[data-menu-xmlid="crm.crm_menu_report"]',
content: 'Open Reporting menu',
@ -19,8 +19,8 @@ tour.register('crm_forecast', {
content: 'Open Forecast menu',
run: 'click',
}, {
trigger: '.o_column_quick_create:contains(Add next month)',
content: 'Wait page loading'
trigger: '.o_column_quick_create',
content: 'Wait page loading',
}, {
trigger: ".o-kanban-button-new",
content: "click create",
@ -28,69 +28,66 @@ tour.register('crm_forecast', {
}, {
trigger: ".o_field_widget[name=name] input",
content: "complete name",
run: "text Test Opportunity 1",
run: "edit Test Opportunity 1",
}, {
trigger: ".o_field_widget[name=expected_revenue] input",
content: "complete expected revenue",
run: "text 999999",
run: "edit 999999",
}, {
trigger: "button.o_kanban_edit",
content: "edit lead",
run: "click",
}, {
trigger: "div[name=date_deadline] button",
content: "open date picker",
run: "click",
}, {
trigger: "div[name=date_deadline] input",
content: "complete expected closing",
run: `text ${today.format("MM/DD/YYYY")}`,
run: `edit ${today.toFormat("MM/dd/yyyy")}`,
}, {
trigger: "div[name=date_deadline] input",
content: "click to make the datepicker disappear",
run: "click"
}, {
trigger: "body:not(:has(div.bootstrap-datetimepicker-widget))",
content: "wait for date_picker to disappear",
run: function () {},
}, {
trigger: '.o_back_button',
content: 'navigate back to the kanban view',
position: "bottom",
tooltipPosition: "bottom",
run: "click"
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Opportunity 1')",
trigger: ".o_kanban_record:contains('Test Opportunity 1')",
content: "move to the next month",
run: function (actions) {
const undefined_groups = $('.o_column_title:contains("None")').length;
actions.drag_and_drop_native(`.o_opportunity_kanban .o_kanban_group:eq(${1 + undefined_groups})`, this.$anchor);
async run({ queryAll, drag_and_drop }) {
const undefined_groups = queryAll('.o_column_title:contains("None")').length;
await drag_and_drop(`.o_opportunity_kanban .o_kanban_group:eq(${1 + undefined_groups})`);
},
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Opportunity 1')",
trigger: ".o_kanban_record:contains('Test Opportunity 1')",
content: "edit lead",
run: "click"
}, {
trigger: "div[name=date_deadline] button",
content: "open date picker",
run: "click",
}, {
trigger: ".o_field_widget[name=date_deadline] input",
content: "complete expected closing",
run: function (actions) {
actions.text(`text ${moment(today).add(5, 'months').startOf('month').subtract(1, 'days').format("MM/DD/YYYY")}`, this.$anchor);
this.$anchor[0].dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
},
run: `edit ${today.plus({ months: 5 }).startOf("month").minus({ days: 1 }).toFormat("MM/dd/yyyy")} && press Escape`,
}, {
trigger: "body:not(:has(div.bootstrap-datetimepicker-widget))",
content: "wait for date_picker to disappear",
run: function () {},
}, {
trigger: ".o_field_widget[name=probability] input",
content: "max out probability",
run: "text 100"
trigger: "button[name=action_set_won_rainbowman]",
content: "win the lead",
run:"click"
}, {
trigger: '.o_back_button',
content: 'navigate back to the kanban view',
position: "bottom",
tooltipPosition: "bottom",
run: "click"
}, {
trigger: '.o_kanban_add_column',
trigger: '.o_column_quick_create.o_quick_create_folded div',
content: "add next month",
run: "click"
}, {
trigger: ".o_kanban_record:contains('Test Opportunity 1'):contains('Won')",
content: "assert that the opportunity has the Won banner",
run: function () {},
}
]);
]});

View file

@ -1,87 +1,119 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
tour.register('crm_rainbowman', {
test: true,
url: "/web",
}, [
tour.stepUtils.showAppsMenuItem(),
registry.category("web_tour.tours").add("crm_rainbowman", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: ".o_app[data-menu-xmlid='crm.crm_menu_root']",
content: "open crm app",
}, {
trigger: ".o-kanban-button-new",
run: "click",
},
{
trigger: "body:has(.o_kanban_renderer) .o-kanban-button-new",
content: "click create",
}, {
run: "click",
},
{
trigger: ".o_field_widget[name=name] input",
content: "complete name",
run: "text Test Lead 1",
}, {
run: "edit Test Lead 1",
},
{
trigger: ".o_field_widget[name=expected_revenue] input",
content: "complete expected revenue",
run: "text 999999997",
}, {
run: "edit 999999997",
},
{
trigger: "button.o_kanban_add",
content: "create lead",
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 1')",
run: "click",
},
{
trigger: ".o_kanban_record:contains('Test Lead 1')",
content: "move to won stage",
run: "drag_and_drop_native .o_opportunity_kanban .o_kanban_group:has(.o_column_title:contains('Won')) "
}, {
run: "drag_and_drop (.o_opportunity_kanban .o_kanban_group:has(.o_column_title:contains('Won')))",
},
{
trigger: ".o_reward_rainbow",
extra_trigger: ".o_reward_rainbow",
run: function () {} // check rainbowman is properly displayed
}, {
},
{
// This step and the following simulates the fact that after drag and drop,
// from the previous steps, a click event is triggered on the window element,
// which closes the currently shown .o_kanban_quick_create.
trigger: ".o_kanban_renderer",
}, {
run: "click",
},
{
trigger: ".o_kanban_renderer:not(:has(.o_kanban_quick_create))",
run() {},
}, {
},
{
trigger: ".o-kanban-button-new",
content: "create second lead",
}, {
run: "click",
},
{
trigger: ".o_field_widget[name=name] input",
content: "complete name",
run: "text Test Lead 2",
}, {
run: "edit Test Lead 2",
},
{
trigger: ".o_field_widget[name=expected_revenue] input",
content: "complete expected revenue",
run: "text 999999998",
}, {
run: "edit 999999998",
},
{
trigger: "button.o_kanban_add",
content: "create lead",
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
run: function () {} // wait for the record to be properly created
}, {
run: "click",
},
{
trigger: ".o_kanban_record:contains('Test Lead 2')",
},
{
// move first test back to new stage to be able to test rainbowman a second time
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 1')",
trigger: ".o_kanban_record:contains('Test Lead 1')",
content: "move back to new stage",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(0) "
}, {
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(0) ",
},
{
trigger: ".o_kanban_record:contains('Test Lead 2')",
content: "click on second lead",
}, {
run: "click",
},
{
trigger: ".o_statusbar_status button[data-value='4']",
content: "move lead to won stage",
run: "click",
},
{
content: "wait for save completion",
trigger: ".o_form_readonly, .o_form_saved",
},
{
trigger: ".o_reward_rainbow",
},
...tour.stepUtils.saveForm(),
{
trigger: ".o_statusbar_status button[data-value='1']",
extra_trigger: ".o_reward_rainbow",
content: "move lead to previous stage & rainbowman appears",
}, {
run: "click",
},
{
trigger: "button[name=action_set_won_rainbowman]",
content: "click button mark won",
run: "click",
},
{
content: "wait for save completion",
trigger: ".o_form_readonly, .o_form_saved",
},
{
trigger: ".o_reward_rainbow",
},
...tour.stepUtils.saveForm(),
{
trigger: ".o_menu_brand",
extra_trigger: ".o_reward_rainbow",
content: "last rainbowman appears",
}
]);
},
],
});