Files
goodie/frontend/crm/static/tests/forecast_kanban.test.js
Marc 8741282322 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>
2026-03-31 23:09:12 +02:00

372 lines
13 KiB
JavaScript

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",
]);
});