diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index fb8610b993c..ae5d204eab6 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -1059,7 +1059,7 @@ parseInt(id)); const records = analytic_account_ids.length ? await this.fetchAnalyticAccounts([["id", "in", analytic_account_ids]]) : []; if (records.length < data.length) { console.log('removing tags... value should be updated'); } - - let res = Object.assign({}, ...this.allPlans.map((plan) => ({[plan.id]: {...plan, distribution: []}}))); + let widgetData = Object.assign({}, ...this.allPlans.map((plan) => ({[plan.id]: {...plan, distribution: []}}))); records.map((record) => { - res[record.root_plan_id[0]].distribution.push({ + if (!widgetData[record.root_plan_id[0]]) { + // plans might not have been retrieved + widgetData[record.root_plan_id[0]] = { distribution: [] } + } + widgetData[record.root_plan_id[0]].distribution.push({ analytic_account_id: record.id, percentage: data[record.id], id: this.nextId++, @@ -111,11 +120,7 @@ export class AnalyticDistribution extends Component { }); }); - this.state.list = res; - } - - patched() { - this.focusToSelector(); + this.state.list = widgetData; } // ORM @@ -158,6 +163,10 @@ export class AnalyticDistribution extends Component { if (limit) { args['limit'] = limit; } + if (domain.length === 1 && domain[0][0] === "id") { + //batch these orm calls + return await this.props.record.model.orm.read("account.analytic.account", domain[0][2], args.fields, {}); + } return await this.orm.call("account.analytic.account", "search_read", [], args); } @@ -402,17 +411,13 @@ export class AnalyticDistribution extends Component { validate() { for (const group_id in this.list) { if (this.groupStatus(group_id) === 'red') { - this.invalidate(); + this.props.record.setInvalidField(this.props.name); return false; } } return true; } - invalidate() { - this.props.record.setInvalidField(this.props.name); - } - async save() { const currentDistribution = this.listForJson; const dataToSave = currentDistribution; @@ -441,7 +446,11 @@ export class AnalyticDistribution extends Component { this.state.showDropdown = false; } - openAnalyticEditor() { + async openAnalyticEditor() { + if (!this.allPlans.length) { + await this.fetchAllPlans(this.props); + await this.formatData(this.props); + } this.autoFill(); const incompletePlan = this.firstIncompletePlanId; this.setFocusSelector(incompletePlan ? `#plan_${incompletePlan} .incomplete`: ".analytic_json_popup"); diff --git a/addons/analytic/static/tests/analytic_distribution_tests.js b/addons/analytic/static/tests/analytic_distribution_tests.js index 8924c1f42b2..e1fd652f259 100644 --- a/addons/analytic/static/tests/analytic_distribution_tests.js +++ b/addons/analytic/static/tests/analytic_distribution_tests.js @@ -2,7 +2,10 @@ import { click, + editInput, getFixture, + nextTick, + triggerHotkey, } from "@web/../tests/helpers/utils"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; @@ -14,18 +17,18 @@ QUnit.module("Analytic", (hooks) => { target = getFixture(); serverData = { models: { - analytic_account: { + "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" }, }, records: [ - {id: 1, color: 1, root_plan_id: 1, plan_id: 1, name: "RD" }, - {id: 2, color: 1, root_plan_id: 1, plan_id: 1, name: "HR" }, - {id: 3, color: 1, root_plan_id: 1, plan_id: 1, name: "FI" }, - {id: 4, color: 2, root_plan_id: 2, plan_id: 2, name: "Time Off" }, - {id: 5, color: 2, root_plan_id: 2, plan_id: 2, name: "Operating Costs" }, + {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" }, @@ -45,14 +48,15 @@ QUnit.module("Analytic", (hooks) => { }, color: { string: "Color", type: "integer" }, all_account_count: { type: "integer" }, + parent_id: { type: "many2one", relation: "plan" }, }, records: [ - { id: 1, name: 'Departments', applicability: "mandatory" }, - { id: 2, name: 'Internal', applicability: "optional", all_account_count: 2 }, + { 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" }, - { id: 5, name: "Country", applicability: "optional"}, - { id: 6, name: "City", 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: { @@ -86,7 +90,7 @@ QUnit.module("Analytic", (hooks) => { QUnit.module("AnalyticDistribution"); - QUnit.test("field in form view should open and close", async function (assert) { + QUnit.test("field in form view basic features", async function (assert) { await makeView({ type: "form", resModel: "aml", @@ -103,59 +107,76 @@ QUnit.module("Analytic", (hooks) => { `, mockRPC(route, { kwargs, method, model }) { - if (method === "search_read" && model === "account.analytic.plan") { - return Promise.resolve(serverData.models['plan'].records); - } else if (method === "search_read" && model === "account.analytic.account") { - if (kwargs.domain[0][0] == "id" && kwargs.domain[0][1] == "in") { - const required_ids = kwargs.domain[0][2].map((id) => parseInt(id)); - const analytic_accs = serverData.models['analytic_account'].records.filter((r) => required_ids.includes(r.id)); - const accs_with_plan = analytic_accs.map( - (r) => ( - { - ...r, - root_plan_id: Object.values(serverData.models['plan'].records.find((p) => p.id === r.root_plan_id)) - } - )); - return Promise.resolve(accs_with_plan); - } - return Promise.resolve(serverData.models['analytic_account'].records); - } else if (method === "get_relevant_plans" && model === "account.analytic.plan") { - return Promise.resolve(serverData.models['plan'].records); + if (method === "get_relevant_plans" && model === "account.analytic.plan") { + return Promise.resolve( + serverData.models['plan'].records.filter((r) => !r.parent_id && r.applicability !== "unavailable") + ); } }, }); assert.containsOnce(target, ".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, - "RD 30.3%", + assert.strictEqual(target.querySelector(".badge .o_tag_badge_text").textContent, "RD 30.3%", "should have rendered 'RD 30.3%'" ); - assert.strictEqual( - target.querySelectorAll(".badge .o_tag_badge_text")[1].textContent, - "FI 69.7%", + assert.strictEqual(target.querySelectorAll(".badge .o_tag_badge_text")[1].textContent, "FI 69.7%", "should have rendered 'FI 69.7%'" ); - assert.containsN( - target, - ".o_delete", - 2, - "tags should contain a delete button" + assert.containsN(target, ".o_delete", 2, "tags should contain a delete button"); + + let badge1 = target.querySelector('.badge'); + await click(badge1, ".o_tag_badge_text"); + assert.containsN(target, ".analytic_distribution_popup", 1, "popup should be visible"); + + let popup = target.querySelector('.analytic_distribution_popup'); + let planTable = popup.querySelectorAll('table')[0]; + assert.strictEqual(planTable.id, "plan_2", "mandatory plan appears first"); + assert.containsN(planTable, 'tr', 4, + "first plan contains 4 rows including: title, 2 tags, add a line" + ); + assert.strictEqual(document.activeElement, planTable.querySelector('input'), + "focus is on the first analytic account" ); - const badge1 = target.querySelector('.badge'); - await click(badge1, ".o_tag_badge_text"); - assert.containsN( - target, - ".analytic_distribution_popup", - 1, - "popup should be visible" + triggerHotkey("Tab"); + const input = document.activeElement; + await editInput(input, null, "19"); + + assert.containsOnce(planTable, '.o_analytic_status_orange', "Mandatory plan has incomplete status"); + + let incompleteInputName = planTable.querySelector('tr.incomplete .o_analytic_account_name input'); + assert.strictEqual(document.activeElement, incompleteInputName, + "focus is on the first incomplete tag" ); + triggerHotkey("Escape"); + await nextTick(); + assert.containsNone(target, '.analytic_distribution_popup', "The popup should be closed and invalid"); + assert.containsOnce(target, "div .o_field_analytic_distribution.o_field_invalid", "Field should be invalid"); + + triggerHotkey("arrowdown"); //opens the popup again + await nextTick(); + + popup = target.querySelector('.analytic_distribution_popup'); + planTable = popup.querySelectorAll('table')[0]; + incompleteInputName = planTable.querySelector('tr.incomplete .o_analytic_account_name input'); + assert.strictEqual(document.activeElement, incompleteInputName, + "focus returns to the first incomplete tag" + ); + + triggerHotkey("arrowdown"); + await nextTick(); + + triggerHotkey("Tab"); + await nextTick(); + + assert.strictEqual(document.activeElement.value, "11.3%", "remainder percentage is prepopulated"); + await click(target, '.fa-close'); assert.containsNone(target, '.analytic_distribution_popup', "The popup should be closed"); - + assert.containsNone(target, '.o_field_invalid', "Distribution is valid"); + assert.containsN(target, ".badge", 3, "should contain 3 tags"); }); });