diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js index a1adb8c6afa..ed46c3cb65a 100644 --- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js +++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.js @@ -1,20 +1,22 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; +import { useService, useOwnedDialogs } from "@web/core/utils/hooks"; import { evaluateExpr } from "@web/core/py_js/py"; import { getNextTabableElement, getPreviousTabableElement } from "@web/core/utils/ui"; import { usePosition } from "@web/core/position_hook"; import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service"; import { shallowEqual } from "@web/core/utils/arrays"; +import { sprintf } from "@web/core/utils/strings"; import { _lt } from "@web/core/l10n/translation"; -import { AnalyticAutoComplete } from "../autocomplete/autocomplete"; +import { AutoComplete } from "@web/core/autocomplete/autocomplete"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; import { TagsList } from "@web/core/tags_list/tags_list"; import { useOpenMany2XRecord } from "@web/views/fields/relational_utils"; import { parseFloat as oParseFloat } from "@web/views/fields/parsers"; import { formatPercentage } from "@web/views/fields/formatters"; +import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog"; const { Component, useState, useRef, useExternalListener, onWillUpdateProps, onWillStart, onPatched } = owl; @@ -23,7 +25,6 @@ const PLAN_APPLICABILITY = { optional: _lt("Optional"), } const PLAN_STATUS = { - editing: _lt("Editing"), invalid: _lt("Invalid"), ok: _lt("OK"), } @@ -45,7 +46,6 @@ export class AnalyticDistribution extends Component { this.nextId = 1; this.focusSelector = false; - this.activeGroup = false; this.currentValue = this.props.record.data[this.props.name]; @@ -74,6 +74,10 @@ export class AnalyticDistribution extends Component { this.allPlans = []; this.lastAccount = this.props.account_field && this.props.record.data[this.props.account_field] || false; this.lastProduct = this.props.product_field && this.props.record.data[this.props.product_field] || false; + + this.selectCreateIsOpen = false; + this.addDialog = useOwnedDialogs(); + this.onSearchMore = this._onSearchMore.bind(this); } // Lifecycle @@ -190,18 +194,18 @@ export class AnalyticDistribution extends Component { } // Autocomplete - get sourcesAnalyticAccount() { - return [this.optionsSourceAnalytic]; + sourcesAnalyticAccount(groupId) { + return [this.optionsSourceAnalytic(groupId)]; } - get optionsSourceAnalytic() { + optionsSourceAnalytic(groupId) { return { placeholder: this.env._t("Loading..."), - options: this.loadOptionsSourceAnalytic.bind(this), + options:(searchTerm) => this.loadOptionsSourceAnalytic(groupId, searchTerm), }; } - async loadOptionsSourceAnalytic(request) { + analyticAccountDomain(groupId=null) { let domain = [['id', 'not in', this.existingAnalyticAccountIDs]]; if (this.props.record.data.company_id){ domain.push( @@ -211,11 +215,28 @@ export class AnalyticDistribution extends Component { ); } - if (this.activeGroup) { - domain.push(['root_plan_id', '=', this.activeGroup]); + if (groupId) { + domain.push(['root_plan_id', '=', groupId]); } + return domain; + } - const records = await this.fetchAnalyticAccounts([...domain, '|', ["name", "ilike", request], '|', ['code', 'ilike', request], ['partner_id', 'ilike', request]], 7); + searchAnalyticDomain(searchTerm) { + return [ + '|', + ["name", "ilike", searchTerm], + '|', + ['code', 'ilike', searchTerm], + ['partner_id', 'ilike', searchTerm], + ]; + } + + async loadOptionsSourceAnalytic(groupId, searchTerm) { + const searchLimit = 6; + + const records = await this.fetchAnalyticAccounts([ + ...this.analyticAccountDomain(groupId), + ...this.searchAnalyticDomain(searchTerm)], searchLimit + 1); let options = records.map((result) => ({ value: result.id, @@ -224,6 +245,14 @@ export class AnalyticDistribution extends Component { color: result.color, })); + if (searchLimit < records.length) { + options.push({ + label: this.env._t("Search More..."), + action: (editedTag) => this.onSearchMore(searchTerm, editedTag), + classList: "o_m2o_dropdown_option o_m2o_dropdown_option_search_more", + }); + } + if (!options.length) { options.push({ label: this.env._t("No Analytic Accounts for this plan"), @@ -235,56 +264,98 @@ export class AnalyticDistribution extends Component { return options; } - groupAutocompleteFocus(group_id) { - this.activeGroup = group_id; + async _onSearchMore(searchTerm, editedTag) { + let dynamicFilters = []; + if (searchTerm.length) { + dynamicFilters = [ + { + description: sprintf(this.env._t("Quick search: %s"), searchTerm), + domain: this.searchAnalyticDomain(searchTerm), + }, + ]; + } + this.selectCreateIsOpen = true; + this.addDialog(SelectCreateDialog, { + title: this.env._t("Search: Analytic Account"), + noCreate: true, + multiSelect: true, + resModel: 'account.analytic.account', + context: { + tree_view_ref: "analytic.view_account_analytic_account_list_select", + }, + domain: this.analyticAccountDomain(editedTag.group_id), + dynamicFilters: dynamicFilters, + onSelected: async (resIds) => { + const analytic_accounts = await this.fetchAnalyticAccounts([["id", "in", resIds]]); + // modify the edited tag + editedTag.analytic_account_id = analytic_accounts[0].id; + editedTag.analytic_account_name = analytic_accounts[0].display_name; + this.setFocusSelector(`.tag_${editedTag.id} .o_analytic_percentage`); + if (analytic_accounts.length > 1) { + const planId = editedTag.group_id; + // remove the autofill line + this.list[planId].distribution = this.list[planId].distribution.filter((t) => !!t.analytic_account_id); + for (const account of analytic_accounts.slice(1)) { + // add new tags + const tag = this.newTag(planId); + tag.analytic_account_id = account.id; + tag.analytic_account_name = account.display_name; + this.list[planId].distribution.push(tag); + } + } + this.autoFill(); + }, + onCreateEdit: () => {}, + }, { + onClose: () => { + if (!editedTag.analytic_account_id) { + this.setFocusSelector(`.tag_${editedTag.id} .o_analytic_account_name`); + this.focusToSelector(); + } + this.selectCreateIsOpen = false; + }, + }); } - autoCompleteInput(dist_tag, {inputValue}) { - if (inputValue === "") { - dist_tag.analytic_account_id = ""; + autoCompleteInputChanged(distTag, inputValue) { + if (inputValue === "" && distTag.analytic_account_id) { + this.deleteTag(distTag.id, distTag.group_id); } } // Editing Distributions async onSelect(option, params, tag) { + if (option.action) { + return option.action(tag); + } const selected_option = Object.getPrototypeOf(option); tag.analytic_account_id = parseInt(selected_option.value); tag.analytic_account_name = selected_option.label; tag.color = selected_option.color; this.setFocusSelector(`.tag_${tag.id} .o_analytic_percentage`); + this.autoFill(); } async percentageChanged(dist_tag, ev) { dist_tag.percentage = this.parse(ev.target.value); - - if (this.remainderByGroup(dist_tag.group_id)) { - this.setFocusSelector(`#plan_${dist_tag.group_id} .incomplete .o_analytic_account_name`); + if (dist_tag.percentage == 0) { + this.deleteTag(dist_tag.id, dist_tag.group_id); } this.autoFill(); } - deleteTag(id) { - for (const group_id in this.list) { - this.list[group_id].distribution = this.list[group_id].distribution.filter((dist_tag) => dist_tag.id != id); - if (this.list[group_id].applicability === 'mandatory' && this.remainderByGroup(group_id)) this.addLineToGroup(group_id); - } + deleteTag(id, fromGroup) { + // find the next tag to focus to before deleting the tag + const allTags = this.listFlat; + const currentTagIndex = allTags.findIndex((t) => t.id === id); + const nextTag = allTags[(currentTagIndex + 1) % allTags.length]; + // remove the tag from the groups distribution + this.list[fromGroup].distribution = this.list[fromGroup].distribution.filter((dist_tag) => dist_tag.id != id); if (!this.isDropdownOpen){ this.save(); - } - } - - addLineToGroup(id) { - const tag_cnt_diff = this.list[id].distribution.length - this.listReadyByGroup(id).length; - if (!tag_cnt_diff) { - this.list[id].distribution.push(this.newTag(id)); } else { - //all tags are not ready - update the value of tag without analytic account or add - const tagToUpdate = this.list[id].distribution.filter((t) => !this.tagIsReady(t) && !t.analytic_account_id)[0]; - if (!!tagToUpdate) { - tagToUpdate.percentage = this.remainderByGroup(id); - } else { - this.list[id].distribution.push(this.newTag(id)); - } + this.setFocusSelector(`.tag_${nextTag.id} .o_analytic_account_name`); + this.autoFill(); } } @@ -296,7 +367,7 @@ export class AnalyticDistribution extends Component { colorIndex: dist_tag.color, group_id: dist_tag.group_id, onClick: (ev) => this.tagClicked(ev, dist_tag.id), - onDelete: this.editingRecord ? () => this.deleteTag(dist_tag.id) : undefined + onDelete: this.editingRecord ? () => this.deleteTag(dist_tag.id, dist_tag.group_id) : undefined })); } @@ -310,8 +381,9 @@ export class AnalyticDistribution extends Component { get firstIncompletePlanId() { for (const group_id in this.list) { - const group_status = this.groupStatus(group_id); - if (["editing", "invalid"].includes(group_status)) return group_id; + if (this.groupStatus(group_id) == "invalid") { + return group_id; + } } return 0; } @@ -341,8 +413,8 @@ export class AnalyticDistribution extends Component { } get allowSave() { - for (const group_id in this.list) { - if (['editing', 'invalid'].includes(this.groupStatus(group_id))) return false; + if (this.firstIncompletePlanId > 0) { + return false; } return this.props.allow_save; } @@ -364,11 +436,7 @@ export class AnalyticDistribution extends Component { groupStatus(id) { const group = this.list[id]; - const ready_tags = this.listReadyByGroup(id); if (group.applicability === 'mandatory') { - if (group.distribution.length > ready_tags.length) { - return 'editing' - } const sum = this.sumByGroup(id); if (sum < 99.99 || sum >= 100.01) { return 'invalid'; @@ -405,10 +473,25 @@ export class AnalyticDistribution extends Component { } } + /** + * This method is typically called when opening the popup and after any change to the distribution. + * The remainder, will be placed in the first tag with 0%. + * It adds an empty tag allowing users to continue the distribution (replaced 'Add a Line'). + * Where an empty tag exists, the percentage is updated. + */ autoFill() { - for (const group of this.allPlans.filter((p) => p.all_account_count > 0)){ - if (this.remainderByGroup(group.id)) { - this.addLineToGroup(group.id); + for (const group of this.allPlans.filter((p) => p.all_account_count > 0)) { + // update the first unmodified tag containing 0% + const tagToUpdate = this.list[group.id].distribution.find((t) => t.percentage == 0); + if (tagToUpdate) { + tagToUpdate.percentage = this.remainderByGroup(group.id); + } + // a tag with no analytic account must always be added / updated + const emptyTag = this.list[group.id].distribution.find((t) => !t.analytic_account_id); + if (emptyTag) { + emptyTag.percentage = this.remainderByGroup(group.id); + } else { + this.list[group.id].distribution.push(this.newTag(group.id)); } } } @@ -421,8 +504,7 @@ export class AnalyticDistribution extends Component { async save() { const currentDistribution = this.listForJson; - const dataToSave = currentDistribution; - await this.props.record.update({ [this.props.name]: dataToSave }); + await this.props.record.update({ [this.props.name]: currentDistribution }); } onSaveNew() { @@ -561,8 +643,11 @@ export class AnalyticDistribution extends Component { } onWindowClick(ev) { - if (this.isDropdownOpen && this.dropdownRef.el && !this.dropdownRef.el.contains(ev.target) && !this.widgetRef.el.contains(ev.target)) { - this.closeAnalyticEditor(); + if (this.isDropdownOpen + && this.dropdownRef.el && !this.dropdownRef.el.contains(ev.target) + && !this.widgetRef.el.contains(ev.target) + && !this.selectCreateIsOpen) { + this.forceCloseEditor(); } } @@ -581,7 +666,7 @@ export class AnalyticDistribution extends Component { } AnalyticDistribution.template = "analytic.AnalyticDistribution"; AnalyticDistribution.components = { - AnalyticAutoComplete, + AutoComplete, TagsList, } @@ -592,7 +677,7 @@ AnalyticDistribution.props = { product_field: { type: String, optional: true }, business_domain_compute: { type: String, optional: true }, force_applicability: { type: String, optional: true }, - allow_save: { type: Boolean }, + allow_save: { type: Boolean, optional: true }, } export const analyticDistribution = { diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.scss b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.scss index 63cff225900..db3dcfbd807 100644 --- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.scss +++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.scss @@ -57,10 +57,6 @@ background-color: $o-success; margin-left:8px; } - .o_analytic_status_editing { - background-color: $o-warning; - margin-left:8px; - } tr:hover { outline: none !important; } diff --git a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.xml b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.xml index 0650fdefe6a..e46cbfad624 100644 --- a/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.xml +++ b/addons/analytic/static/src/components/analytic_distribution/analytic_distribution.xml @@ -14,7 +14,7 @@
-
+
Analytic
@@ -37,15 +37,14 @@ - + onChange.alike="({inputValue}) => this.autoCompleteInputChanged(dist_tag, inputValue)"/> - + - - - Add a line - -
diff --git a/addons/analytic/static/src/components/autocomplete/autocomplete.js b/addons/analytic/static/src/components/autocomplete/autocomplete.js deleted file mode 100644 index 7f58e0fc0d5..00000000000 --- a/addons/analytic/static/src/components/autocomplete/autocomplete.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @odoo-module **/ - -import { AutoComplete } from "@web/core/autocomplete/autocomplete"; - -export class AnalyticAutoComplete extends AutoComplete {} -AnalyticAutoComplete.template = "analytic.AutoComplete"; diff --git a/addons/analytic/static/src/components/autocomplete/autocomplete.xml b/addons/analytic/static/src/components/autocomplete/autocomplete.xml deleted file mode 100644 index 48f59d46f9a..00000000000 --- a/addons/analytic/static/src/components/autocomplete/autocomplete.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - onInputClick - - - diff --git a/addons/analytic/static/tests/analytic_distribution_tests.js b/addons/analytic/static/tests/analytic_distribution_tests.js index a71962a78fd..204c2773cab 100644 --- a/addons/analytic/static/tests/analytic_distribution_tests.js +++ b/addons/analytic/static/tests/analytic_distribution_tests.js @@ -26,15 +26,22 @@ QUnit.module("Analytic", (hooks) => { 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: "Namur" }, + {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: { @@ -53,10 +60,10 @@ QUnit.module("Analytic", (hooks) => { 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: 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 }, ], @@ -70,9 +77,9 @@ QUnit.module("Analytic", (hooks) => { }, records: [ { id: 1, label: "Developer Time", amount: 100.00, analytic_distribution: {"1": 30.3, "3": 69.7}}, - { id: 2, label: "Coke", amount: 100.00, analytic_distribution: '{}'}, - { id: 3, label: "Sprite", amount: 100.00, analytic_distribution: '{}'}, - { id: 4, label: "", amount: 100.00, analytic_distribution: '{}'}, + { id: 2, label: "Coke", amount: 100.00, analytic_distribution: {}}, + { id: 3, label: "Sprite", amount: 100.00, analytic_distribution: {}}, + { id: 4, label: "", amount: 100.00, analytic_distribution: {}}, ], }, partner: { @@ -102,6 +109,14 @@ QUnit.module("Analytic", (hooks) => { ] } }, + views: { + "account.analytic.account,false,search": ``, + "account.analytic.account,analytic.view_account_analytic_account_list_select,list": ` + + + + `, + } }; setupViewRegistries(); @@ -109,7 +124,7 @@ QUnit.module("Analytic", (hooks) => { QUnit.module("AnalyticDistribution"); - QUnit.test("field in form view basic features", async function (assert) { + QUnit.test("analytic field in form view basic features", async function (assert) { await makeView({ type: "form", resModel: "aml", @@ -137,10 +152,10 @@ QUnit.module("Analytic", (hooks) => { 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, "RD 30.3%", - "should have rendered 'RD 30.3%'" + "should have rendered tag 'RD 30.3%'" ); assert.strictEqual(target.querySelectorAll(".badge .o_tag_badge_text")[1].textContent, "FI 69.7%", - "should have rendered 'FI 69.7%'" + "should have rendered tag 'FI 69.7%'" ); assert.containsN(target, ".o_delete", 2, "tags should contain a delete button"); @@ -151,9 +166,10 @@ QUnit.module("Analytic", (hooks) => { 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" + "first plan contains 4 rows including: plan title, 2 tags, empty tag" ); assert.strictEqual(document.activeElement, planTable.querySelector('input'), "focus is on the first analytic account" @@ -163,12 +179,7 @@ QUnit.module("Analytic", (hooks) => { const input = document.activeElement; await editInput(input, null, "19"); - assert.containsOnce(planTable, '.o_analytic_status_editing', "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" - ); + assert.containsOnce(planTable, '.o_analytic_status_invalid', "Mandatory plan has invalid status"); triggerHotkey("Escape"); await nextTick(); @@ -179,7 +190,7 @@ QUnit.module("Analytic", (hooks) => { popup = target.querySelector('.analytic_distribution_popup'); planTable = popup.querySelectorAll('table')[0]; - incompleteInputName = planTable.querySelector('tr.incomplete .o_analytic_account_name input'); + let incompleteInputName = planTable.querySelector('tr.incomplete .o_analytic_account_name input'); assert.strictEqual(document.activeElement, incompleteInputName, "focus returns to the first incomplete tag" ); @@ -197,4 +208,133 @@ QUnit.module("Analytic", (hooks) => { assert.containsNone(target, '.o_field_invalid', "Distribution is valid"); assert.containsN(target, ".badge", 3, "should contain 3 tags"); }); + + QUnit.test("analytic field in multi_edit list view + search more", async function (assert) { + await makeView({ + type: "list", + resModel: "aml", + serverData, + arch: ` + + + + + `, + 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, 'th span', "All plans are optional with no status indicator"); + + let incompleteCountryTag = popup.querySelector("table#plan_5 .incomplete .o_analytic_account_name input"); + await click(incompleteCountryTag); + await click(target.querySelector(".o_m2o_dropdown_option_search_more")); + + assert.containsN(target, ".modal-dialog .o_list_renderer", 1, "select create list dialog is visible"); + + // select 2 analytic accounts + let accountRows = [...target.querySelectorAll(".modal-dialog .o_data_row")]; + for (const row of accountRows.slice(0,2)) { + await click(row.querySelector(".o_list_record_selector input")); + } + await click(target.querySelector(".o_select_button")); + + let percentageEls = [...popup.querySelectorAll("table#plan_5 .o_analytic_percentage input")]; + let expectedPercentages = ['100%', '0%', '0%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `1: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + // modify the percentage of tag 1, tag 2 is filled + await editInput(percentageEls[0], null, "50"); + expectedPercentages = ['50%', '50%', '0%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `2: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + // modify the percentage of tag 1, last empty tag (tag 3) is filled + await editInput(percentageEls[0], null, "40"); + expectedPercentages = ['40%', '50%', '10%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `3: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + + // replace the first analytic account with 4 accounts + triggerHotkey("shift+Tab"); + await click(document.activeElement); + await click(target.querySelector(".o_m2o_dropdown_option_search_more")); + accountRows = [...target.querySelectorAll(".modal-dialog .o_data_row")]; + for (const row of accountRows.slice(0,4)) { + await click(row.querySelector(".o_list_record_selector input")); + } + await click(target.querySelector(".o_select_button")); + + percentageEls = [...popup.querySelectorAll("table#plan_5 .o_analytic_percentage input")]; + expectedPercentages = ['40%', '50%', '10%', '0%', '0%', '0%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `4: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + + // modify percentage of the tag 1 (focused), balance goes to the first zero (tag 4) + await editInput(document.activeElement, null, "10"); + expectedPercentages = ['10%', '50%', '10%', '30%', '0%', '0%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `5: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + + // modify percentage of tag 4, balance goes to the first zero (tag 5) + await editInput(percentageEls[3], null, "20"); + expectedPercentages = ['10%', '50%', '10%', '20%', '10%', '0%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `6: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + + // change tag 4 to 0%, tag is removed and balance goes to last tag (tag 5) + let accountEls = [...popup.querySelectorAll("table#plan_5 .o_analytic_account_name input")]; + await editInput(percentageEls[3], null, "0"); + percentageEls = [...popup.querySelectorAll("table#plan_5 .o_analytic_percentage input")]; + expectedPercentages = ['10%', '50%', '10%', '10%', '20%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `7: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + assert.strictEqual(document.activeElement, accountEls[4], "Focus should be on the fifth tag (one was removed)"); + + // delete tag 3, balance goes to last empty tag (tag 4) + let trashIcons = [...document.querySelectorAll("table#plan_5 .fa-trash-o")]; + assert.equal(trashIcons.length, 4, "1 tag should not have a trash icon"); + await click(trashIcons[2]); + percentageEls = [...popup.querySelectorAll("table#plan_5 .o_analytic_percentage input")]; + expectedPercentages = ['10%', '50%', '10%', '30%']; + for (const [i, el] of percentageEls.entries()) { + assert.equal(el.value, expectedPercentages[i], `7: Percentage Element ${i} should be ${expectedPercentages[i]}`); + } + assert.equal(popup.querySelector("table#plan_5 tr:last-of-type .o_analytic_account_name input").value, "", + "Last tag's account is empty"); + + // apply the changes to both move lines + triggerHotkey("Escape"); + await nextTick(); + await click(target.querySelector(".modal-dialog .btn-primary")); + assert.containsN(target, ".badge", 10, "should contain 2 rows of 5 tags each"); + + }); }); diff --git a/addons/analytic/views/analytic_account_views.xml b/addons/analytic/views/analytic_account_views.xml index 3297661fbb1..066bf9c9bc7 100644 --- a/addons/analytic/views/analytic_account_views.xml +++ b/addons/analytic/views/analytic_account_views.xml @@ -66,6 +66,19 @@ + + account.analytic.account.list.select + account.analytic.account + primary + + + + + 0 + + + + account.analytic.account.kanban account.analytic.account diff --git a/addons/analytic/views/analytic_distribution_model_views.xml b/addons/analytic/views/analytic_distribution_model_views.xml index 41d05b6f36a..669f6f9457c 100644 --- a/addons/analytic/views/analytic_distribution_model_views.xml +++ b/addons/analytic/views/analytic_distribution_model_views.xml @@ -8,7 +8,8 @@ - +