mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-19 21:12:02 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,11 @@
|
|||
.o_field_analytic_distribution {
|
||||
|
||||
height: auto;
|
||||
|
||||
.analytic_distribution_placeholder {
|
||||
height: 1.5em;
|
||||
width: 20px;
|
||||
color: grey;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_input_dropdown {
|
||||
|
|
@ -41,8 +44,11 @@
|
|||
}
|
||||
|
||||
.analytic_distribution_popup {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
max-width: $o-form-sheet-min-width;
|
||||
max-height: 50vh;
|
||||
// this ensures that analytic distribution widget does not cover "new model" or "search more" modals
|
||||
z-index: $zindex-modal - 1;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
|
||||
|
|
@ -67,5 +73,12 @@
|
|||
tr:hover {
|
||||
outline: none !important;
|
||||
}
|
||||
.numeric_column_width {
|
||||
max-width: 105px;
|
||||
// width: 88px;
|
||||
}
|
||||
.w-20px {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,84 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="analytic.AnalyticDistribution" owl="1">
|
||||
<t t-name="analytic.AnalyticDistribution">
|
||||
<div class="o_field_tags d-inline-flex flex-wrap mw-100" t-att-class="{'o_tags_input o_input': !props.readonly}" t-ref="analyticDistribution" t-on-keydown="onWidgetKeydown">
|
||||
<TagsList tags="this.tags"/>
|
||||
<TagsList tags="planSummaryTags()"/>
|
||||
<div t-if="!props.readonly" class="o_input_dropdown d-inline-flex w-100" tabindex="0" t-ref="mainElement" t-on-focus="onMainElementFocus" t-on-click="onMainElementFocus">
|
||||
<span class="analytic_distribution_placeholder"/>
|
||||
<a role="button" class="o_dropdown_button" draggable="false"/>
|
||||
<span class="analytic_distribution_placeholder"><t t-if="!state.formattedData.length" t-esc="props.placeholder"/></span>
|
||||
<span class="o_dropdown_button" />
|
||||
<t t-call="analytic.AnalyticDistributionPopup"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<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">
|
||||
<t t-name="analytic.AnalyticDistributionPopup">
|
||||
<div class="popover analytic_distribution_popup dropdown-menu o-dropdown--menu show rounded py-0 overflow-x-hidden" t-if="state.showDropdown" t-ref="analyticDropdown">
|
||||
<div class="popover-header sticky-top">
|
||||
<div class="d-flex">
|
||||
<div class="h5 mt-2 me-auto">
|
||||
Analytic
|
||||
<span t-if="tags.length and allowSave" class="btn btn-link" t-on-click="onSaveNew">New Model</span>
|
||||
<span t-if="allowSave" class="btn btn-link" t-on-click="onSaveNew" title="Save as new analytic distribution model">New Model</span>
|
||||
</div>
|
||||
<div class="popupButtons">
|
||||
<span class="o_button ms-2 cursor-pointer" t-on-click.stop="() => this.closeAnalyticEditor()"><span class="fa fa-close"/></span>
|
||||
<span class="btn o_button" t-on-click.stop="() => this.closeAnalyticEditor()" title="Close"><span class="fa fa-close"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<span t-if="!sortedList.length">No plans available</span>
|
||||
<t t-foreach="sortedList" t-as="plan" t-key="plan.id">
|
||||
<table class="o_list_table table table-sm table-hover o_analytic_table mb-2 table-borderless" t-attf-id="plan_{{plan.id}}">
|
||||
<div class="popover-body p-2 table-responsive" style="max-width: 100vw;">
|
||||
<span t-if="!allPlans.length">No analytic plans found</span>
|
||||
<table t-else="" class="o_list_table table table-sm table-hover o_analytic_table mb-2 table-striped">
|
||||
<t t-set="totals" t-value="planTotals()"/>
|
||||
<thead>
|
||||
<tr class="border-bottom">
|
||||
<th class="o_analytic_account_name">
|
||||
<t t-esc="plan.name"/>
|
||||
<t t-if="plan.account_count === 0"> (no accounts)</t>
|
||||
<span t-if="plan.applicability === 'mandatory'" t-attf-class="o_status d-inline-block o_analytic_status_{{groupStatus(plan.id)}}" t-att-title="statusDescription(plan.id)"/>
|
||||
<th t-foreach="allPlans" t-as="plan" t-key="plan.id">
|
||||
<t t-if="props.multi_edit">
|
||||
<a t-if="state.update_plan[plan.column_name]" t-on-click="() => state.update_plan[plan.column_name] = false" href="#">Don't update</a>
|
||||
<a t-else="" t-on-click="() => state.update_plan[plan.column_name] = true" href="#">Update</a>
|
||||
<br/>
|
||||
</t>
|
||||
<span t-out="plan.name"/>
|
||||
<t t-if="state.update_plan[plan.column_name]">(<span t-att-class="totals[plan.id].class" t-out="totals[plan.id].formattedValue"/>)</t>
|
||||
</th>
|
||||
<th class="numeric_column_width">Percentage</th>
|
||||
<th t-if="valueColumnEnabled" class="numeric_column_width" t-out="props.record.fields[props.amount_field].string"/>
|
||||
<th class="deleteColumn w-20px"/>
|
||||
</tr>
|
||||
<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">
|
||||
<AutoComplete
|
||||
id="dist_tag.id.toString()"
|
||||
placeholder="'Search Analytic Account'"
|
||||
value="dist_tag.analytic_account_name"
|
||||
sources="sourcesAnalyticAccount(plan.id)"
|
||||
autoSelect="true"
|
||||
onSelect.alike="(option, params) => this.onSelect(option, params, dist_tag)"
|
||||
onChange.alike="({inputValue}) => this.autoCompleteInputChanged(dist_tag, inputValue)"/>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.formattedData" t-as="line" t-key="line.id" t-att-id="line_index" t-att-name="'line_' + line_index">
|
||||
<Record t-props="recordProps(line)" t-slot-scope="data">
|
||||
<t t-foreach="Object.keys(data.record.fields).filter((f) => f.startsWith('x_plan') || f == 'account_id')" t-as="field" t-key="field">
|
||||
<td t-att-style="state.update_plan[field] ? '' : '--table-bg-state: var(--table-hover-bg);'">
|
||||
<Field t-if="state.update_plan[field]"
|
||||
id="field"
|
||||
name="field"
|
||||
record="data.record"
|
||||
domain="data.record.fields[field].domain"
|
||||
canOpen="false"
|
||||
canCreate="false"
|
||||
canCreateEdit="false"
|
||||
canQuickCreate="false"/>
|
||||
</td>
|
||||
</t>
|
||||
<td class="numeric_column_width">
|
||||
<Field id="'percentage'" name="'percentage'" record="data.record"/>
|
||||
</td>
|
||||
<td class="o_analytic_percentage">
|
||||
<input
|
||||
class="o_input"
|
||||
inputmode="numeric"
|
||||
type="text"
|
||||
t-att-value="formatPercentage(dist_tag.percentage)"
|
||||
t-on-click.stop=""
|
||||
t-on-change="(ev) => this.percentageChanged(dist_tag, ev)"/>
|
||||
<td t-if="valueColumnEnabled" class="numeric_column_width">
|
||||
<Field id="props.amount_field" name="props.amount_field" record="data.record"/>
|
||||
</td>
|
||||
<td>
|
||||
<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 class="w-20px">
|
||||
<span class="fa fa-trash-o cursor-pointer" t-on-click.stop="() => this.deleteLine(line_index)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</t>
|
||||
<div tabindex="0" class="hidden-focus"/>
|
||||
</Record>
|
||||
</tr>
|
||||
<tr>
|
||||
<td t-on-click.stop.prevent="addLine" class="o_field_x2many_list_row_add" t-att-colspan="allPlans.length + 2 + valueColumnEnabled">
|
||||
<a href="#" t-ref="addLineButton" tabindex="0">Add a Line</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { ORM } from "@web/core/orm_service";
|
||||
import { unique } from "@web/core/utils/arrays";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
|
||||
class RequestBatcherORM extends ORM {
|
||||
constructor() {
|
||||
super();
|
||||
this.searchReadBatches = {};
|
||||
this.searchReadBatchId = 1;
|
||||
this.batches = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @param {any[]} keys
|
||||
* @param {Function} callback
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async batch(ids, keys, callback) {
|
||||
const key = JSON.stringify(keys);
|
||||
let batch = this.batches[key];
|
||||
if (!batch) {
|
||||
batch = {
|
||||
deferred: new Deferred(),
|
||||
scheduled: false,
|
||||
ids: [],
|
||||
};
|
||||
this.batches[key] = batch;
|
||||
}
|
||||
batch.ids = unique([...batch.ids, ...ids]);
|
||||
|
||||
if (!batch.scheduled) {
|
||||
batch.scheduled = true;
|
||||
Promise.resolve().then(async () => {
|
||||
delete this.batches[key];
|
||||
let result;
|
||||
try {
|
||||
result = await callback(batch.ids);
|
||||
} catch (e) {
|
||||
return batch.deferred.reject(e);
|
||||
}
|
||||
batch.deferred.resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
return batch.deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to batch "read" calls. If the `fields` and `resModel`
|
||||
* arguments have already been called, the given ids are added to the
|
||||
* previous list of ids to perform a single read call. Once the server
|
||||
* responds, records are then dispatched to the callees based on the
|
||||
* given ids arguments (kept in the closure).
|
||||
*
|
||||
* @param {string} resModel
|
||||
* @param {number[]} resIds
|
||||
* @param {string[]} fields
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
async read(resModel, resIds, fields, kwargs) {
|
||||
const records = await this.batch(resIds, ["read", resModel, fields, kwargs], (resIds) =>
|
||||
super.read(resModel, resIds, fields, kwargs)
|
||||
);
|
||||
return records.filter((r) => resIds.includes(r.id));
|
||||
}
|
||||
}
|
||||
|
||||
export const batchedOrmService = {
|
||||
async: [
|
||||
"call",
|
||||
"create",
|
||||
"nameGet",
|
||||
"read",
|
||||
"formattedReadGroup",
|
||||
"search",
|
||||
"searchRead",
|
||||
"unlink",
|
||||
"webSearchRead",
|
||||
"write",
|
||||
],
|
||||
start() {
|
||||
return new RequestBatcherORM();
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("batchedOrm", batchedOrmService);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { SearchModel } from "@web/search/search_model";
|
||||
|
||||
const PLAN_REGEX = /^(?:x_)?(x_plan\d+_id|account_id)(_\d+)?$/;
|
||||
|
||||
export class AnalyticSearchModel extends SearchModel {
|
||||
getSearchItems(predicate) {
|
||||
let searchItems = super.getSearchItems(predicate);
|
||||
const mapped = Map.groupBy(
|
||||
searchItems.filter((f) => f.fieldName?.match(PLAN_REGEX)),
|
||||
(f) => f.fieldName.match(PLAN_REGEX)[1],
|
||||
);
|
||||
searchItems = searchItems.filter(
|
||||
(f) => !f.fieldName?.match(PLAN_REGEX) || mapped.has(f.fieldName)
|
||||
);
|
||||
searchItems.forEach((f) => {
|
||||
if (f.fieldName && mapped.has(f.fieldName) && mapped.get(f.fieldName).length > 1) {
|
||||
f.options = mapped.get(f.fieldName);
|
||||
}
|
||||
});
|
||||
return searchItems;
|
||||
}
|
||||
|
||||
toggleDateGroupBy(searchItemId, intervalId) {
|
||||
if (typeof(intervalId) === "number") {
|
||||
this.toggleSearchItem(intervalId);
|
||||
} else {
|
||||
super.toggleDateGroupBy(searchItemId, intervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { graphView } from "@web/views/graph/graph_view";
|
||||
import { AnalyticSearchModel } from "@analytic/views/analytic_search_model";
|
||||
|
||||
export const analyticGraphView = {
|
||||
...graphView,
|
||||
SearchModel: AnalyticSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("analytic_graph", analyticGraphView);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { AnalyticSearchModel } from "@analytic/views/analytic_search_model";
|
||||
|
||||
export const analyticKanbanView = {
|
||||
...kanbanView,
|
||||
SearchModel: AnalyticSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("analytic_kanban", analyticKanbanView);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { AnalyticSearchModel } from "@analytic/views/analytic_search_model";
|
||||
|
||||
export const analyticListView = {
|
||||
...listView,
|
||||
SearchModel: AnalyticSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("analytic_list", analyticListView);
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { PivotRenderer } from "@web/views/pivot/pivot_renderer";
|
||||
|
||||
|
||||
export class AnalyticPivotRenderer extends PivotRenderer {
|
||||
|
||||
/*
|
||||
* Override to avoid using incomplete groupByItems
|
||||
*/
|
||||
onGroupBySelected({ itemId, optionId }) {
|
||||
if (typeof(optionId) === "number") {
|
||||
itemId = optionId;
|
||||
}
|
||||
let searchItems = this.env.searchModel.getSearchItems(
|
||||
(searchItem) =>
|
||||
["groupBy", "dateGroupBy"].includes(searchItem.type) && !searchItem.custom
|
||||
)
|
||||
searchItems = [...searchItems, ...searchItems.flatMap((f) => f.options).filter((f) => typeof(f?.id) === "number")]
|
||||
const { fieldName } = searchItems.find(({ id }) => id === itemId);
|
||||
this.model.addGroupBy({ ...this.dropdown.cellInfo, fieldName, interval: optionId });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { pivotView } from "@web/views/pivot/pivot_view";
|
||||
import { AnalyticSearchModel } from "@analytic/views/analytic_search_model";
|
||||
import { AnalyticPivotRenderer } from "@analytic/views/pivot/pivot_renderer";
|
||||
|
||||
export const analyticPivotView = {
|
||||
...pivotView,
|
||||
Renderer: AnalyticPivotRenderer,
|
||||
SearchModel: AnalyticSearchModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("analytic_pivot", analyticPivotView);
|
||||
Loading…
Add table
Add a link
Reference in a new issue