Files
odoo_source/addons/analytic/static/tests/analytic_distribution_tests.js
T
williamandh4818 dc696c8ed4 [IMP] analytic: allow cross analytics
Purpose
=======

Make it possible to compare analytic values between plans by inputting
up to one account per plan on each line.
For instance, if we have 2 plans: "Country" and "Product Type", with the
following accounts

| Country | Product Type |
| ------- | ------------ |
| BE      | Drinks       |
| LU      | Food         |

And the following invoices:
* 1000€ of drinks in Belgium
* 2000€ of food in Belgium
* 3000€ of food in Luxemburg

We want to be able to display the following pivot tables:

|     | Drinks | Food | Total |
| --- | ------ | ---- | ----- |
| BE  |   1000 | 2000 |  3000 |
| LU  |      0 | 3000 |  3000 |
| Tot |   1000 | 5000 |  6000 |

|        | Total |
| ------ | ----- |
| Food   |  5000 |
| * BE   |  2000 |
| * LU   |  3000 |
| Drinks |  1000 |
| * BE   |  1000 |
| Total  |  6000 |

Implementation
==============

* `ir.fields` are added/removed dynamically on `account.analytic.line`
* Each analytic plan corresponds to one field on `account.analytic.line`
* The fields are added dynamically in the views.
* `account.analytic.plan` doesn't have a `company_id` field anymore,
  meaning that all the companies have access to all the plans. This
  means that the applicability fields and lines are now company
  dependent, and those can be used to know which fields/plans to display
  for which company.
* There can no longer be one "Project" plan per company, even if the
  accounts of that project can still be owned by only one company.
* There is now only one method to get the default (or "Project" plan),
  which is `_get_plan_columns`

task-3497653

closes odoo/odoo#139225

Related: odoo/upgrade#5306
Related: odoo/enterprise#49226
Signed-off-by: William André (wan) <wan@odoo.com>
Co-authored-by: william <wan@odoo.com>
Co-authored-by: h4818 <ayh@odoo.com>
2023-10-25 18:10:57 +00:00

398 lines
18 KiB
JavaScript

/** @odoo-module **/
import {
addRow,
click,
editInput,
getFixture,
nextTick,
selectDropdownItem,
triggerEvent,
triggerHotkey,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Analytic", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
"account.analytic.account": {
fields: {
plan_id: { string: "Plan", type: "many2one", relation: "plan" },
root_plan_id: { string: "Root Plan", type: "many2one", relation: "plan" },
color: { string: "Color", type: "integer" },
code: { string: "Ref", type: "string"},
partner_id: { string: "Partner", type: "many2one", relation: "partner" },
},
records: [
{id: 1, color: 1, root_plan_id: 2, plan_id: 2, name: "RD" },
{id: 2, color: 1, root_plan_id: 2, plan_id: 2, name: "HR" },
{id: 3, color: 1, root_plan_id: 2, plan_id: 2, name: "FI" },
{id: 4, color: 2, root_plan_id: 1, plan_id: 1, name: "Time Off" },
{id: 5, color: 2, root_plan_id: 1, plan_id: 1, name: "Operating Costs" },
{id: 6, color: 6, root_plan_id: 4, plan_id: 4, name: "Incognito" },
{id: 7, color: 5, root_plan_id: 5, plan_id: 5, name: "Belgium" },
{id: 8, color: 6, root_plan_id: 5, plan_id: 6, name: "Brussels" },
{id: 9, color: 6, root_plan_id: 5, plan_id: 6, name: "Beirut" },
{id: 10, color: 6, root_plan_id: 5, plan_id: 6, name: "Berlin" },
{id: 11, color: 6, root_plan_id: 5, plan_id: 6, name: "Bruges" },
{id: 12, color: 6, root_plan_id: 5, plan_id: 6, name: "Birmingham" },
{id: 13, color: 6, root_plan_id: 5, plan_id: 6, name: "Bologna" },
{id: 14, color: 6, root_plan_id: 5, plan_id: 6, name: "Bratislava" },
{id: 15, color: 6, root_plan_id: 5, plan_id: 6, name: "Budapest" },
{id: 16, color: 6, root_plan_id: 5, plan_id: 6, name: "Namur" },
],
},
plan: {
fields: {
applicability: {
string: "Applicability",
type: "selection",
selection: [
["mandatory", "Mandatory"],
["optional", "Options"],
["unavailable", "Unavailable"],
],
},
color: { string: "Color", type: "integer" },
all_account_count: { type: "integer" },
parent_id: { type: "many2one", relation: "plan" },
},
records: [
{ id: 1, name: "Internal", applicability: "optional", all_account_count: 2 },
{ id: 2, name: "Departments", applicability: "mandatory", all_account_count: 3 },
{ id: 3, name: "Projects", applicability: "optional" },
{ id: 4, name: "Hidden", applicability: "unavailable", all_account_count: 1 },
{ id: 5, name: "Country", applicability: "optional", all_account_count: 3 },
{ id: 6, name: "City", applicability: "optional", all_account_count: 2, parent_id: 5 },
],
},
aml: {
fields: {
label: { string: "Label", type: "char" },
amount: { string: "Amount", type: "float" },
analytic_distribution: { string: "Analytic", type: "json" },
move_id: { string: "Account Move", type: "many2one", relation: "move" },
analytic_precision: { string: "Analytic Precision", type: "integer" },
},
records: [
{ id: 1, label: "Developer Time", amount: 100.00, analytic_distribution: {"1, 7": 30.3, "3": 69.704}, analytic_precision: 3},
{ id: 2, label: "Coke", amount: 100.00, analytic_distribution: {}},
{ id: 3, label: "Sprite", amount: 100.00, analytic_distribution: {}, analytic_precision: 3},
{ id: 4, label: "", amount: 100.00, analytic_distribution: {}},
],
},
partner: {
fields: {
name: { string: "Name", type: "char" },
},
records: [
{ id: 1, name: "Great Partner" },
],
},
move: {
fields: {
line_ids: { string: "Move Lines", type: "one2many", relation: "aml", relation_field: "move_line_id" },
},
records: [
{ id: 1, display_name: "INV0001", line_ids: [1, 2]},
{ id: 2, display_name: "INV0002", line_ids: [3, 4]},
],
},
},
views: {
"account.analytic.account,false,search": `<search/>`,
"account.analytic.account,analytic.view_account_analytic_account_list_select,list": `
<tree>
<field name="name"/>
</tree>
`,
}
};
setupViewRegistries();
});
QUnit.module("AnalyticDistribution");
QUnit.test("analytic field in form view basic features", async function (assert) {
await makeView({
type: "form",
resModel: "aml",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="label"/>
<field name="analytic_distribution" widget="analytic_distribution"/>
<field name="amount"/>
</group>
</sheet>
</form>`,
mockRPC(route, { method, model }) {
if (method === "get_relevant_plans" && model === "account.analytic.plan") {
return Promise.resolve(
serverData.models['plan'].records.filter((r) => !r.parent_id && r.applicability !== "unavailable")
);
}
},
});
// tags
assert.containsOnce(target, ".o_field_analytic_distribution", "widget should be visible");
assert.containsN(target, ".badge", 2, "should contain 2 tags");
assert.strictEqual(target.querySelector(".badge .o_tag_badge_text").textContent, "30.3% RD | 69.7% FI",
"should have rendered tag '30.3% RD | 69.7% FI'"
);
assert.strictEqual(target.querySelectorAll(".badge .o_tag_badge_text")[1].textContent, "30.3% Belgium",
"should have rendered tag '30.3% Belgium'"
);
// open popup
let field = target.querySelector('.o_field_analytic_distribution');
await click(field, ".o_input_dropdown");
assert.containsN(target, ".analytic_distribution_popup", 1, "popup should be visible");
let popup = target.querySelector('.analytic_distribution_popup');
let planTable = popup.querySelectorAll('table')[0];
// contents of popup
assert.containsN(planTable, 'tr', 4,
"table contains 4 rows including: header row, 2 lines, add a line"
);
assert.strictEqual(document.activeElement, planTable.querySelector('tr:first-of-type #x_plan1_id'),
"focus is on the first analytic account input of the first row"
);
// change percentage
await click(planTable, "tr:first-of-type .o_field_percentage input");
let input = document.activeElement;
await editInput(input, null, "19.7001");
// mandatory plan is red
assert.containsOnce(planTable, 'th:contains("Departments") .text-danger:contains("50%")', "Mandatory plan has invalid status");
// close and open popup with keyboard
triggerHotkey("Escape");
await nextTick();
assert.containsNone(target, '.analytic_distribution_popup', "The popup should be closed");
triggerHotkey("arrowdown");
await nextTick();
// add a line
popup = target.querySelector('.analytic_distribution_popup');
planTable = popup.querySelectorAll('table')[0];
await click(planTable, '.o_field_x2many_list_row_add a');
const newRow = planTable.querySelector('tr:nth-of-type(3)');
assert.equal(newRow.querySelector('.o_field_percentage input').value, 50);
// choose an account for the mandatory plan using the keyboard
await click(newRow, "#x_plan2_id");
input = document.activeElement;
await triggerEvent(input, null, "keydown", { key: "enter" });
await triggerEvent(input, null, "keyup", { key: "enter" });
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Tab");
await nextTick();
// mandatory plan is green
assert.containsOnce(planTable, 'th:contains("Departments") .text-success:contains("100%")', "Mandatory plan in complete");
// tags
await click(target, '.fa-close');
assert.containsNone(target, '.analytic_distribution_popup', "The popup should be closed");
assert.containsN(target, ".badge", 2, "should contain 2 tags");
assert.strictEqual(target.querySelector(".badge .o_tag_badge_text").textContent, "30.3% RD | 50% HR | 19.7% FI",
"should have rendered tag '30.3% RD | 50% HR | 19.7% FI'"
);
});
QUnit.test("analytic field in multi_edit list view + search more", async function (assert) {
await makeView({
type: "list",
resModel: "aml",
serverData,
arch: `
<tree multi_edit="1">
<field name="label"/>
<field name="analytic_distribution" widget="analytic_distribution" options="{'force_applicability': 'optional'}"/>
<field name="amount"/>
</tree>`,
mockRPC(route, { kwargs, method, model }) {
if (method === "get_relevant_plans" && model === "account.analytic.plan") {
assert.equal(kwargs.applicability, "optional");
return Promise.resolve(
serverData.models['plan'].records
.filter((r) => !r.parent_id && r.applicability !== "unavailable")
.map((r) => ({...r, applicability: "optional"}))
);
}
},
});
assert.containsN(target, ".badge", 2, "should contain 2 tags");
let badge1 = target.querySelector('.badge');
await click(badge1, ".o_tag_badge_text");
assert.containsNone(target, '.analytic_distribution_popup', "The popup should not open in readonly mode");
// select 2 rows
const amlrows = target.querySelectorAll(".o_data_row");
await click(amlrows[0].querySelector(".o_list_record_selector input"));
await click(amlrows[1].querySelector(".o_list_record_selector input"));
await click(badge1, ".o_tag_badge_text");
await nextTick();
assert.containsN(target, ".analytic_distribution_popup", 1, "popup should be visible");
let popup = target.querySelector('.analytic_distribution_popup');
assert.containsNone(popup, '.text-success', "All plans are optional");
// add a line
let planTable = popup.querySelectorAll('table')[0];
await addRow(planTable)
await selectDropdownItem(planTable.querySelector("tr[name='line_2']"), "x_plan5_id", "Search More...");
assert.containsN(target, ".modal-dialog .o_list_renderer", 1, "select create list dialog is visible");
await click(target, ".modal-dialog .modal-title");
await click(target, ".modal-dialog .o_data_row:nth-of-type(4) .o_data_cell:first-of-type");
assert.containsNone(target, ".modal-dialog .o_list_renderer", "select create list dialog is closed");
await click(popup, ".fa-close");
await click(target.querySelector(".modal-dialog .btn-primary"));
await nextTick();
assert.containsN(target, ".badge", 4, "should contain 2 rows of 2 tags each");
assert.strictEqual(target.querySelector("tr:nth-of-type(2) .badge:nth-of-type(2) .o_tag_badge_text").textContent, "30.3% Belgium | 69.7% Berlin",
"should have rendered tag '30.3% Belgium | 69.7% Berlin'"
);
});
QUnit.test("Rounding, value suggestions, keyboard only", async (assert) => {
await makeView({
type: "form",
resModel: "move",
serverData,
resId: 2,
arch: `
<form>
<sheet>
<field name="line_ids">
<tree editable="bottom">
<field name="label"/>
<field name="analytic_distribution" widget="analytic_distribution"/>
<field name="amount"/>
</tree>
</field>
</sheet>
</form>`,
mockRPC(route, { kwargs, method, model }) {
if (method === "get_relevant_plans" && model === "account.analytic.plan") {
return Promise.resolve(
serverData.models['plan'].records.filter((r) => !r.parent_id && r.applicability !== "unavailable" && r.all_account_count)
);
}
},
});
await click(target, ".o_data_row:nth-of-type(1) .o_list_char");
triggerHotkey("Tab");
await nextTick();
assert.containsN(target, ".analytic_distribution_popup", 1, "popup should be visible");
// department
triggerHotkey("Tab");
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose the RD account
await nextTick();
triggerHotkey("Tab"); // tab to country
triggerHotkey("Tab"); // tab to percentage
await editInput(document.activeElement, false, "99.9");
// internal
triggerHotkey("Tab"); // new line
await nextTick();
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose the Time off account
await nextTick();
triggerHotkey("Tab"); // tab to departments
triggerHotkey("Tab"); // tab to country
triggerHotkey("Tab"); // tab to percentage
await editInput(document.activeElement, false, "99.99");
// country
triggerHotkey("Tab"); // new line
await nextTick();
triggerHotkey("Tab"); // tab to departments
triggerHotkey("Tab"); // tab to country
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose the Belgium account
await nextTick();
triggerHotkey("Tab"); // tab to percentage
await editInput(document.activeElement, false, "99.999");
// tags
assert.containsOnce(target, ".badge:contains('99.9% RD')", "contains RD tag");
assert.containsOnce(target, ".badge:contains('99.99% Time Off')", "contains Time Off tag");
assert.containsOnce(target, ".badge:contains('100% Belgium')", "contains Belgium tag always rounded to 2 decimals");
// fill department
triggerHotkey("Tab"); // new line
await nextTick();
triggerHotkey("Tab"); // tab to departments
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose the HR account
await nextTick();
triggerHotkey("Tab");
triggerHotkey("Tab");
assert.equal(document.activeElement.value, 0.1, "Mandatory plan filled first with 0.1");
await editInput(document.activeElement, false, "0.0996");
assert.containsOnce(target, ".badge:contains('99.9% RD | 0.1% HR')", "contains RD|HR tag");
assert.containsOnce(target, ".text-success:contains('100%')", "Department header");
// fill country
triggerHotkey("Tab"); // new line
await nextTick();
triggerHotkey("Tab"); // tab to departments
triggerHotkey("Tab"); // tab to country
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose Belgium again
await nextTick();
triggerHotkey("Tab"); // tab to percentage
await nextTick();
assert.equal(document.activeElement.value, 0.001, "Country plan filled next with 0.001");
await editInput(document.activeElement, false, "0.0006");
assert.containsOnce(target, ".badge:contains('Belgium'):not(:contains('%'))", "contains Belgium tag");
// fill internal
triggerHotkey("Tab"); // new line
await nextTick();
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("arrowdown");
await nextTick();
triggerHotkey("Enter"); // choose Operating Costs
await nextTick();
triggerHotkey("Escape"); // close the popup
await nextTick();
assert.containsOnce(target, ".badge:contains('99.99% Time Off | 0.01% Operating Costs')", "the Time Off | Operating Costs tag");
});
QUnit.test("Analytic Account Deleted after json is saved", (assert) => { assert.expect(0) });
QUnit.test("No Plans Available", (assert) => { assert.expect(0) });
QUnit.test("amount_field Column", (assert) => { assert.expect(0) });
QUnit.test("save as model", (assert) => { assert.expect(0) });
});