[IMP] project: improve the list view by adding personal stage
This will improve the following generic UX - rename 'personal user stage' into 'personal stage' for private task - added the 'personal stage' field to the right of the stage optional=hide - earlier gantt view open with the user now it will open with groupby project - removed groupby stage in pivot view - rename 'undefined' into 'private' for graph view of private task task-3251648 closes odoo/odoo#120094 Related: odoo/upgrade#4607 Related: odoo/enterprise#40515 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
committed by
Xavier Bol (xbo)
parent
6a60b7df35
commit
496c0a275a
@@ -1,13 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { GraphModel } from "@web/views/graph/graph_model";
|
||||
import { ProjectTaskGraphModel } from "@project/views/project_task_graph/project_task_graph_model";
|
||||
|
||||
const FIELDS = [
|
||||
'unit_amount', 'effective_hours', 'planned_hours', 'remaining_hours', 'total_hours_spent', 'subtask_effective_hours',
|
||||
'overtime', 'number_hours', 'difference', 'timesheet_unit_amount'
|
||||
];
|
||||
|
||||
export class hrTimesheetGraphModel extends GraphModel {
|
||||
export class hrTimesheetGraphModel extends ProjectTaskGraphModel {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -36,4 +36,4 @@ export class hrTimesheetGraphModel extends GraphModel {
|
||||
return super._getProcessedDataPoints(...arguments);
|
||||
}
|
||||
}
|
||||
hrTimesheetGraphModel.services = [...GraphModel.services, "company"];
|
||||
hrTimesheetGraphModel.services = [...ProjectTaskGraphModel.services, "company"];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { projectGraphView } from "@project/views/project_task_graph/project_graph_view";
|
||||
import { projectTaskGraphView } from "@project/views/project_task_graph/project_task_graph_view";
|
||||
import { hrTimesheetGraphModel } from "./timesheet_graph_model";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
export const hrTimesheetGraphView = {
|
||||
...projectGraphView,
|
||||
...projectTaskGraphView,
|
||||
Model: hrTimesheetGraphModel,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { getGraphRenderer } from "@web/../tests/views/graph_view_tests";
|
||||
import {
|
||||
checkLabels,
|
||||
checkLegend,
|
||||
getGraphRenderer,
|
||||
selectMode
|
||||
} from "@web/../tests/views/graph_view_tests";
|
||||
import { makeView } from "@web/../tests/views/helpers";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { session } from "@web/session";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
@@ -11,16 +16,65 @@ import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module('hr_timesheet', function (hooks) {
|
||||
let serverData;
|
||||
let serverData, target;
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
'account.analytic.line': {
|
||||
fields: {
|
||||
unit_amount: { string: "Unit Amount", type: "float", group_operator: "sum", store: true },
|
||||
project_id: {
|
||||
string: "Project",
|
||||
type: "many2one",
|
||||
relation: "project.project",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, unit_amount: 8 }
|
||||
{ id: 1, unit_amount: 8, project_id: false },
|
||||
],
|
||||
},
|
||||
"project.task": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", type: "char" },
|
||||
milestone_id: {
|
||||
string: "Milestone",
|
||||
type: "many2one",
|
||||
relation: "project.milestone",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
project_id: {
|
||||
string: "Project",
|
||||
type: "many2one",
|
||||
relation: "project.project",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Task 1", project_id: 1, milestone_id: 1 },
|
||||
{ id: 2, name: "Task 2", project_id: false, milestone_id: false },
|
||||
],
|
||||
},
|
||||
"project.milestone": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", "type": "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Milestone 1" },
|
||||
]
|
||||
},
|
||||
"project.project": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", "type": "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Project 1" },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -35,6 +89,7 @@ QUnit.module('hr_timesheet', function (hooks) {
|
||||
}
|
||||
}
|
||||
setupControlPanelServiceRegistry();
|
||||
target = getFixture();
|
||||
serviceRegistry.add("company", companyService, { force: true });
|
||||
});
|
||||
|
||||
@@ -73,4 +128,63 @@ QUnit.module('hr_timesheet', function (hooks) {
|
||||
const renderedData = getGraphRenderer(graph).chart.data.datasets[0].data;
|
||||
assert.deepEqual(renderedData, [1], 'The timesheet graph view is taking the timesheet_uom_factor into account (factor !== 1)');
|
||||
});
|
||||
|
||||
QUnit.test("check custom default label", async function (assert) {
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "project.task",
|
||||
arch: `
|
||||
<graph js_class="hr_timesheet_graphview">
|
||||
<field name="project_id"/>
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["Count"]);
|
||||
|
||||
await selectMode(target, "line");
|
||||
|
||||
checkLabels(assert, graph, ["", "Project 1", ""]);
|
||||
checkLegend(assert, graph, ["Count"]);
|
||||
|
||||
await selectMode(target, "pie");
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
});
|
||||
|
||||
QUnit.test("check default label with 2 fields in groupby", async function (assert) {
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "project.task",
|
||||
arch: `
|
||||
<graph js_class="hr_timesheet_graphview">
|
||||
<field name="project_id"/>
|
||||
<field name="milestone_id"/>
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["Undefined", "Milestone 1", "Sum"]);
|
||||
|
||||
await selectMode(target, "line");
|
||||
|
||||
checkLabels(assert, graph, ["", "Project 1", ""]);
|
||||
checkLegend(assert, graph, ["Milestone 1"]);
|
||||
|
||||
await selectMode(target, "pie");
|
||||
|
||||
checkLabels(assert, graph, [
|
||||
"🔒 Private / Undefined",
|
||||
"Project 1 / Milestone 1"
|
||||
]);
|
||||
checkLegend(assert, graph, [
|
||||
"🔒 Private / Undefined",
|
||||
"Project 1 / Milestone 1"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_project_task_pivot"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stage_id']" position='after'>
|
||||
<xpath expr="//field[@name='planned_hours']" position='after'>
|
||||
<field name="planned_hours" widget="timesheet_uom"/>
|
||||
<field name="remaining_hours" widget="timesheet_uom"/>
|
||||
<field name="effective_hours" widget="timesheet_uom"/>
|
||||
|
||||
@@ -167,17 +167,17 @@ class Task(models.Model):
|
||||
# See project_task_stage_personal.py for the model defininition
|
||||
personal_stage_type_ids = fields.Many2many('project.task.type', 'project_task_user_rel', column1='task_id', column2='stage_id',
|
||||
ondelete='restrict', group_expand='_read_group_personal_stage_type_ids', copy=False,
|
||||
domain="[('user_id', '=', user.id)]", depends=['user_ids'], string='Personal Stage')
|
||||
domain="[('user_id', '=', user.id)]", depends=['user_ids'], string='Personal Stages')
|
||||
# Personal Stage computed from the user
|
||||
personal_stage_id = fields.Many2one('project.task.stage.personal', string='Personal Stage State', compute_sudo=False,
|
||||
compute='_compute_personal_stage_id', help="The current user's personal stage.")
|
||||
# This field is actually a related field on personal_stage_id.stage_id
|
||||
# However due to the fact that personal_stage_id is computed, the orm throws out errors
|
||||
# saying the field cannot be searched.
|
||||
personal_stage_type_id = fields.Many2one('project.task.type', string='Personal User Stage',
|
||||
personal_stage_type_id = fields.Many2one('project.task.type', string='Personal Stage',
|
||||
compute='_compute_personal_stage_type_id', inverse='_inverse_personal_stage_type_id', store=False,
|
||||
search='_search_personal_stage_type_id', default=_default_personal_stage_type_id,
|
||||
help="The current user's personal task stage.")
|
||||
help="The current user's personal task stage.", domain="[('user_id', '=', uid)]")
|
||||
partner_id = fields.Many2one('res.partner',
|
||||
string='Customer', recursive=True, tracking=True, compute='_compute_partner_id', store=True, readonly=False,
|
||||
domain="['|', ('company_id', '=?', company_id), ('company_id', '=', False)]", )
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { GraphModel } from "@web/views/graph/graph_model";
|
||||
|
||||
export class ProjectTaskGraphModel extends GraphModel {
|
||||
_getDefaultFilterLabel(field) {
|
||||
if (field.fieldName === "project_id") {
|
||||
return this.env._t("🔒 Private");
|
||||
}
|
||||
return super._getDefaultFilterLabel(field);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -3,9 +3,14 @@
|
||||
import { ProjectControlPanel } from "@project/components/project_control_panel/project_control_panel";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { graphView } from "@web/views/graph/graph_view";
|
||||
import { ProjectTaskGraphModel } from "./project_task_graph_model";
|
||||
|
||||
const viewRegistry = registry.category("views");
|
||||
|
||||
export const projectGraphView = {...graphView, ControlPanel: ProjectControlPanel};
|
||||
export const projectTaskGraphView = {
|
||||
...graphView,
|
||||
ControlPanel: ProjectControlPanel,
|
||||
Model: ProjectTaskGraphModel,
|
||||
};
|
||||
|
||||
viewRegistry.add("project_graph", projectGraphView);
|
||||
viewRegistry.add("project_task_graph", projectTaskGraphView);
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { fakeCookieService } from "@web/../tests/helpers/mock_services";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { checkLabels, checkLegend, selectMode } from "@web/../tests/views/graph_view_tests";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData, target;
|
||||
|
||||
QUnit.module("Project", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
"project.task": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", type: "char" },
|
||||
milestone_id: {
|
||||
string: "Milestone",
|
||||
type: "many2one",
|
||||
relation: "project.milestone",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
project_id: {
|
||||
string: "Project",
|
||||
type: "many2one",
|
||||
relation: "project.project",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Task 1", project_id: 1, milestone_id: 1 },
|
||||
{ id: 2, name: "Task 2", project_id: false, milestone_id: false },
|
||||
],
|
||||
},
|
||||
"project.milestone": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", "type": "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Milestone 1" },
|
||||
]
|
||||
},
|
||||
"project.project": {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "Name", "type": "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Project 1" },
|
||||
],
|
||||
},
|
||||
},
|
||||
views: {},
|
||||
};
|
||||
setupViewRegistries();
|
||||
|
||||
target = getFixture();
|
||||
registry.category("services").add("cookie", fakeCookieService);
|
||||
});
|
||||
|
||||
QUnit.module("ProjectTaskGraphView");
|
||||
|
||||
QUnit.test("check custom default label", async function (assert) {
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "project.task",
|
||||
arch: `
|
||||
<graph js_class="project_task_graph">
|
||||
<field name="project_id"/>
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["Count"]);
|
||||
|
||||
await selectMode(target, "line");
|
||||
|
||||
checkLabels(assert, graph, ["", "Project 1", ""]);
|
||||
checkLegend(assert, graph, ["Count"]);
|
||||
|
||||
await selectMode(target, "pie");
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
});
|
||||
|
||||
QUnit.test("check default label with 2 fields in groupby", async function (assert) {
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "project.task",
|
||||
arch: `
|
||||
<graph js_class="project_task_graph">
|
||||
<field name="project_id"/>
|
||||
<field name="milestone_id"/>
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
|
||||
checkLabels(assert, graph, ["🔒 Private", "Project 1"]);
|
||||
checkLegend(assert, graph, ["Undefined", "Milestone 1", "Sum"]);
|
||||
|
||||
await selectMode(target, "line");
|
||||
|
||||
checkLabels(assert, graph, ["", "Project 1", ""]);
|
||||
checkLegend(assert, graph, ["Milestone 1"]);
|
||||
|
||||
await selectMode(target, "pie");
|
||||
|
||||
checkLabels(assert, graph, [
|
||||
"🔒 Private / Undefined",
|
||||
"Project 1 / Milestone 1"
|
||||
]);
|
||||
checkLegend(assert, graph, [
|
||||
"🔒 Private / Undefined",
|
||||
"Project 1 / Milestone 1"
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -119,7 +119,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Tasks" sample="1" js_class="project_pivot">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="stage_id" type="col"/>
|
||||
<field name="color" invisible="1"/>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="planned_hours" widget="float_time"/>
|
||||
@@ -138,6 +137,12 @@
|
||||
<xpath expr="/pivot" position="inside">
|
||||
<field name="user_ids" type="row"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='project_id']" position="after">
|
||||
<field name="stage_id" type="col"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='project_id']" position="attributes">
|
||||
<attribute name="invisible">True</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -683,6 +688,9 @@
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="parent_id" optional="hide" context="{'search_view_ref': 'project.view_task_search_form', 'search_default_project_id': project_id}" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="personal_stage_type_id" string="Personal Stage" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -746,7 +754,7 @@
|
||||
<field name="name">project.task.graph</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Tasks" sample="1" js_class="project_graph">
|
||||
<graph string="Tasks" sample="1" js_class="project_task_graph">
|
||||
<field name="project_id" invisible="context.get('default_project_id', False)"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="color" invisible="1"/>
|
||||
|
||||
@@ -373,6 +373,14 @@ export class GraphModel extends Model {
|
||||
return new DateClasses(arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @returns {string}
|
||||
*/
|
||||
_getDefaultFilterLabel(field) {
|
||||
return this.env._t("Undefined");
|
||||
}
|
||||
|
||||
/**
|
||||
* Eventually filters and sort data points.
|
||||
* @protected
|
||||
@@ -383,7 +391,7 @@ export class GraphModel extends Model {
|
||||
let processedDataPoints = [];
|
||||
if (mode === "line") {
|
||||
processedDataPoints = this.dataPoints.filter(
|
||||
(dataPoint) => dataPoint.labels[0] !== this.env._t("Undefined")
|
||||
(dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0]),
|
||||
);
|
||||
} else if (mode === "pie") {
|
||||
processedDataPoints = this.dataPoints.filter(
|
||||
@@ -505,7 +513,7 @@ export class GraphModel extends Model {
|
||||
if (type === "boolean") {
|
||||
label = `${val}`; // toUpperCase?
|
||||
} else if (val === false) {
|
||||
label = this.env._t("Undefined");
|
||||
label = this._getDefaultFilterLabel(gb);
|
||||
} else if (["many2many", "many2one"].includes(type)) {
|
||||
const [id, name] = val;
|
||||
const key = JSON.stringify([fieldName, name]);
|
||||
|
||||
@@ -65,12 +65,12 @@ function checkDatasets(assert, graph, keys, expectedDatasets) {
|
||||
assert.deepEqual(actualValues, expectedDatasets);
|
||||
}
|
||||
|
||||
function checkLabels(assert, graph, expectedLabels) {
|
||||
export function checkLabels(assert, graph, expectedLabels) {
|
||||
const labels = getChart(graph).data.labels.map((l) => l.toString());
|
||||
assert.deepEqual(labels, expectedLabels);
|
||||
}
|
||||
|
||||
function checkLegend(assert, graph, expectedLegendLabels) {
|
||||
export function checkLegend(assert, graph, expectedLegendLabels) {
|
||||
expectedLegendLabels =
|
||||
expectedLegendLabels instanceof Array ? expectedLegendLabels : [expectedLegendLabels];
|
||||
const chart = getChart(graph);
|
||||
@@ -124,7 +124,7 @@ function getModeButton(el, mode) {
|
||||
return el.querySelector(`.o_graph_button[data-mode="${mode}"`);
|
||||
}
|
||||
|
||||
async function selectMode(el, mode) {
|
||||
export async function selectMode(el, mode) {
|
||||
await click(getModeButton(el, mode));
|
||||
}
|
||||
|
||||
@@ -4239,4 +4239,50 @@ QUnit.module("Views", (hooks) => {
|
||||
await validateSearch(target);
|
||||
assert.verifySteps(["rendering"]);
|
||||
});
|
||||
|
||||
QUnit.test("apply default filter label", async function (assert) {
|
||||
const graphView = registry.category("views").get("graph");
|
||||
class CustomGraphModel extends graphView.Model {
|
||||
_getDefaultFilterLabel(fields) {
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
registry.category("views").add("custom_graph", {
|
||||
...graphView,
|
||||
Model: CustomGraphModel,
|
||||
});
|
||||
|
||||
const graph = await makeView({
|
||||
serverData,
|
||||
type: "graph",
|
||||
resModel: "foo",
|
||||
arch: `
|
||||
<graph js_class="custom_graph">
|
||||
<field name="product_id"/>
|
||||
<field name="color_id"/>
|
||||
</graph>
|
||||
`,
|
||||
});
|
||||
|
||||
checkLabels(assert, graph, ["xphone", "xpad"]);
|
||||
checkLegend(assert, graph, ["None", "red", "Sum"]);
|
||||
|
||||
await selectMode(target, "line");
|
||||
|
||||
checkLabels(assert, graph, ["xphone", "xpad"]);
|
||||
checkLegend(assert, graph, ["None", "red"]);
|
||||
|
||||
await selectMode(target, "pie");
|
||||
|
||||
checkLabels(assert, graph, [
|
||||
"xphone / None",
|
||||
"xphone / red",
|
||||
"xpad / None",
|
||||
]);
|
||||
checkLegend(assert, graph, [
|
||||
"xphone / None",
|
||||
"xphone / red",
|
||||
"xpad / None",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user