Eliminate Python dependency: embed frontend assets in odoo-go
- Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/ directory (2925 files, 43MB) — no more runtime reads from Python Odoo - Replace OdooAddonsPath config with FrontendDir pointing to local frontend/ - Rewire bundle.go, static.go, templates.go, webclient.go to read from frontend/ instead of external Python Odoo addons directory - Auto-detect frontend/ and build/ dirs relative to binary in main.go - Delete obsolete Python helper scripts (tools/*.py) The Go server is now fully self-contained: single binary + frontend/ folder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
70
frontend/crm/static/tests/crm_mock_server.js
Normal file
70
frontend/crm/static/tests/crm_mock_server.js
Normal 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;
|
||||
});
|
||||
444
frontend/crm/static/tests/crm_rainbowman.test.js
Normal file
444
frontend/crm/static/tests/crm_rainbowman.test.js
Normal 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
|
||||
});
|
||||
12
frontend/crm/static/tests/crm_test_helpers.js
Normal file
12
frontend/crm/static/tests/crm_test_helpers.js
Normal 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);
|
||||
}
|
||||
371
frontend/crm/static/tests/forecast_kanban.test.js
Normal file
371
frontend/crm/static/tests/forecast_kanban.test.js
Normal 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",
|
||||
]);
|
||||
});
|
||||
119
frontend/crm/static/tests/forecast_view.test.js
Normal file
119
frontend/crm/static/tests/forecast_view.test.js
Normal 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",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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>`,
|
||||
};
|
||||
}
|
||||
38
frontend/crm/static/tests/tours/create_crm_team_tour.js
Normal file
38
frontend/crm/static/tests/tours/create_crm_team_tour.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
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_0"]',
|
||||
run: "edit My CRM Team",
|
||||
}, {
|
||||
trigger: '.btn.o-kanban-button-new',
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: 'div.modal-dialog tr:contains("Test Salesman") input.form-check-input',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.modal-dialog tr:contains("Test Sales Manager") input.form-check-input',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.modal-dialog tr:contains("Test Sales Manager") input.form-check-input:checked',
|
||||
}, {
|
||||
trigger: '.o_selection_box:contains(2)',
|
||||
}, {
|
||||
trigger: 'button.o_select_button',
|
||||
run: "click",
|
||||
},
|
||||
...stepUtils.saveForm()
|
||||
]});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
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:contains(Test Lead Propagation)',
|
||||
content: 'Open the first lead',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: ".o_form_editable .o_field_widget[name=email_from] input",
|
||||
},
|
||||
{
|
||||
trigger: ".o_form_button_save:not(:visible)",
|
||||
content: 'Save the lead',
|
||||
run: 'click',
|
||||
},
|
||||
]});
|
||||
93
frontend/crm/static/tests/tours/crm_forecast_tour.js
Normal file
93
frontend/crm/static/tests/tours/crm_forecast_tour.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
const today = luxon.DateTime.now();
|
||||
|
||||
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',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.dropdown-item[data-menu-xmlid="crm.crm_menu_forecast"]',
|
||||
content: 'Open Forecast menu',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_column_quick_create',
|
||||
content: 'Wait page loading',
|
||||
}, {
|
||||
trigger: ".o-kanban-button-new",
|
||||
content: "click create",
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=name] input",
|
||||
content: "complete name",
|
||||
run: "edit Test Opportunity 1",
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=expected_revenue] input",
|
||||
content: "complete expected revenue",
|
||||
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: `edit ${today.toFormat("MM/dd/yyyy")}`,
|
||||
}, {
|
||||
trigger: "div[name=date_deadline] input",
|
||||
content: "click to make the datepicker disappear",
|
||||
run: "click"
|
||||
}, {
|
||||
trigger: '.o_back_button',
|
||||
content: 'navigate back to the kanban view',
|
||||
tooltipPosition: "bottom",
|
||||
run: "click"
|
||||
}, {
|
||||
trigger: ".o_kanban_record:contains('Test Opportunity 1')",
|
||||
content: "move to the next month",
|
||||
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: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: `edit ${today.plus({ months: 5 }).startOf("month").minus({ days: 1 }).toFormat("MM/dd/yyyy")} && press Escape`,
|
||||
}, {
|
||||
trigger: "button[name=action_set_won_rainbowman]",
|
||||
content: "win the lead",
|
||||
run:"click"
|
||||
}, {
|
||||
trigger: '.o_back_button',
|
||||
content: 'navigate back to the kanban view',
|
||||
tooltipPosition: "bottom",
|
||||
run: "click"
|
||||
}, {
|
||||
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",
|
||||
}
|
||||
]});
|
||||
119
frontend/crm/static/tests/tours/crm_rainbowman.js
Normal file
119
frontend/crm/static/tests/tours/crm_rainbowman.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
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",
|
||||
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: "edit Test Lead 1",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=expected_revenue] input",
|
||||
content: "complete expected revenue",
|
||||
run: "edit 999999997",
|
||||
},
|
||||
{
|
||||
trigger: "button.o_kanban_add",
|
||||
content: "create lead",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_record:contains('Test Lead 1')",
|
||||
content: "move to won stage",
|
||||
run: "drag_and_drop (.o_opportunity_kanban .o_kanban_group:has(.o_column_title:contains('Won')))",
|
||||
},
|
||||
{
|
||||
trigger: ".o_reward_rainbow",
|
||||
},
|
||||
{
|
||||
// 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))",
|
||||
},
|
||||
{
|
||||
trigger: ".o-kanban-button-new",
|
||||
content: "create second lead",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=name] input",
|
||||
content: "complete name",
|
||||
run: "edit Test Lead 2",
|
||||
},
|
||||
{
|
||||
trigger: ".o_field_widget[name=expected_revenue] input",
|
||||
content: "complete expected revenue",
|
||||
run: "edit 999999998",
|
||||
},
|
||||
{
|
||||
trigger: "button.o_kanban_add",
|
||||
content: "create lead",
|
||||
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: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: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",
|
||||
},
|
||||
{
|
||||
trigger: ".o_statusbar_status button[data-value='1']",
|
||||
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",
|
||||
},
|
||||
{
|
||||
trigger: ".o_menu_brand",
|
||||
content: "last rainbowman appears",
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user