[IMP] analytic: add search more option
Allow users to find analytic accounts in the analytic widget if there are more than 7 results for their search criteria, by providing a Search More option. Additional Enhancements include: - remove the 'Add a Line' button. replace with an empty tag (no analytic account, remainder of distribution) - remove the custom AutoComplete. this was used to set the active group on autoComplete focus, and to prevent propogation of click events. - no longer keep track of the active group (editing plan). Instead pass the group (plan id) to the relevant functions. - rename variables, for example request > searchTerm - keep the header of the popup in place (no scroll when there are many tags) - test the widget in list view (with the search more feature) - add a new analytic account list view, as users should not multi-edit in the search more dialog Current behavior before PR: Users were forced to type the entire analytic account name if they had many analytic accounts with similar names. forward port of 36391b9aea82091bf7b689658e07149860b01a61 Desired behavior after PR is merged: A "Search More" option is added to the Autocomplete options when there are more than 6 analytic accounts found. The Search More option provides a SelectCreate Dialog where users are able to find the required analytic account easily. Users are able to select multiple accounts by checking the items in the list. Task-3180055 Forward port of bef8e00d78f439936534cef8b2fd8313f03f3b1c X-original-commit: 9f12669bcc1f23198d12a97b31feb6cb82894e11 Part-of: odoo/odoo#122424
This commit is contained in:
+142
-57
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+5
-11
@@ -14,7 +14,7 @@
|
||||
|
||||
<t t-name="analytic.AnalyticDistributionPopup" owl="1">
|
||||
<div class="analytic_distribution_popup o-dropdown-menu show rounded py-0" t-if="state.showDropdown" t-ref="analyticDropdown">
|
||||
<div class="popover-header">
|
||||
<div class="popover-header sticky-top">
|
||||
<div class="d-flex">
|
||||
<div class="h5 mt-2 me-auto">Analytic</div>
|
||||
<div class="popupButtons">
|
||||
@@ -37,15 +37,14 @@
|
||||
<t t-foreach="plan.distribution" t-as="dist_tag" t-key="dist_tag.id">
|
||||
<tr t-attf-class="{{tagIsReady(dist_tag) and 'ready' or !!dist_tag.analytic_account_id and 'to_remove' or 'incomplete'}} tag_{{dist_tag.id}}">
|
||||
<td class="o_analytic_account_name">
|
||||
<AnalyticAutoComplete
|
||||
<AutoComplete
|
||||
id="dist_tag.id.toString()"
|
||||
placeholder="'Search Analytic Account'"
|
||||
value="dist_tag.analytic_account_name"
|
||||
sources="sourcesAnalyticAccount"
|
||||
sources="sourcesAnalyticAccount(plan.id)"
|
||||
autoSelect="true"
|
||||
onSelect.alike="(option, params) => this.onSelect(option, params, dist_tag)"
|
||||
onFocus.alike="() => this.groupAutocompleteFocus(plan.id)"
|
||||
onInput.alike="(inputVal) => this.autoCompleteInput(dist_tag, inputVal)"/>
|
||||
onChange.alike="({inputValue}) => this.autoCompleteInputChanged(dist_tag, inputValue)"/>
|
||||
</td>
|
||||
<td class="o_analytic_percentage">
|
||||
<input
|
||||
@@ -57,15 +56,10 @@
|
||||
t-on-change="(ev) => this.percentageChanged(dist_tag, ev)"/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-trash-o cursor-pointer" t-on-click.stop="() => this.deleteTag(dist_tag.id)"/>
|
||||
<span t-if="dist_tag.analytic_account_id" class="fa fa-trash-o cursor-pointer" t-on-click.stop="() => this.deleteTag(dist_tag.id, dist_tag.group_id)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<tr>
|
||||
<td class="o_field_x2many_list_row_add" colspan="3">
|
||||
<a t-if="plan.all_account_count" href="#" role="button" tabindex="-1" t-on-click.prevent.stop="() => this.addLineToGroup(plan.id)">Add a line</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</t>
|
||||
<div tabindex="0" class="hidden-focus"/>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
|
||||
export class AnalyticAutoComplete extends AutoComplete {}
|
||||
AnalyticAutoComplete.template = "analytic.AutoComplete";
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="analytic.AutoComplete" t-inherit="web.AutoComplete" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//input" position="attributes">
|
||||
<attribute name="t-on-click"></attribute>
|
||||
<attribute name="t-on-click.stop">onInputClick</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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": `<search/>`,
|
||||
"account.analytic.account,analytic.view_account_analytic_account_list_select,list": `
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
`,
|
||||
}
|
||||
};
|
||||
|
||||
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: `
|
||||
<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, '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");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,19 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_account_analytic_account_list_select" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.list.select</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="mode">primary</field>
|
||||
<field eval="18" name="priority"/>
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_list"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree position="attributes">
|
||||
<attribute name="multi_edit">0</attribute>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_account_analytic_account_kanban" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.kanban</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<field name="partner_id" optional="show"/>
|
||||
<field name="partner_category_id" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution" optional="show"/>
|
||||
<field name="analytic_distribution" widget="analytic_distribution" optional="show"
|
||||
options="{'force_applicability': 'optional', 'disable_save': true}"/>
|
||||
<button name="action_read_distribution_model" type="object" string="View" class="float-end btn-secondary"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
Reference in New Issue
Block a user