Initial commit: OCA Report packages (45 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 2f4db400df
2543 changed files with 469120 additions and 0 deletions

View file

@ -0,0 +1,108 @@
.o_web_client .mis_builder_amount {
text-align: right;
}
.o_web_client .mis_builder_collabel {
text-align: center;
}
.o_web_client .mis_builder_rowlabel {
text-align: left;
}
.o_web_client .mis_builder a {
/* we don't want the link color, to respect user styles */
color: inherit;
}
.o_web_client .mis_builder a:hover {
/* underline links on hover to give a visual cue */
text-decoration: underline;
}
.oe_mis_builder_content {
}
.oe_mis_builder_report_wide_sheet {
max-width: 95% !important;
}
/* style for the control panel (search box and buttons) */
.oe_mis_builder_cp {
display: flex;
flex-direction: row;
padding-bottom: 20px;
}
.oe_mis_builder_cp_left {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.oe_mis_builder_cp_right {
display: flex;
flex-direction: column;
flex-grow: 2;
max-width: 1280px;
}
.oe_mis_builder_cp_right_top_right {
display: flex;
flex-direction: row;
}
.oe_mis_builder_cp_right_top {
display: flex;
flex-direction: row;
}
.oe_mis_builder_cp_right_bottom {
display: flex;
flex-direction: row;
}
.oe_mis_builder_filter_buttons {
display: flex;
flex-grow: 1;
justify-content: flex-start;
}
.oe_mis_builder_action_buttons {
display: flex;
flex-grow: 1;
justify-content: flex-end;
}
.oe_mis_builder_dropdown {
overflow: visible !important;
}
.oe_mis_builder_footnote {
font-size: 80%;
color: red;
position: relative;
bottom: 1ex;
width: 1em;
display: inline-block;
padding-right: 1px;
}
.oe_mis_builder_footnote_table {
list-style: none;
white-space: pre-wrap;
display: inline-block;
td {
vertical-align: top;
}
}
.oe_mis_builder_footnote_div {
padding-top: 1em;
}
.oe_mis_builder_menu_disabled {
color: gainsboro;
}

View file

@ -0,0 +1,287 @@
/** @odoo-module **/
import Dialog from "web.Dialog";
import {Component, onMounted, onWillStart, useState, useSubEnv} from "@odoo/owl";
import {DatePicker} from "@web/core/datepicker/datepicker";
import {FilterMenu} from "@web/search/filter_menu/filter_menu";
import {SearchBar} from "@web/search/search_bar/search_bar";
import {SearchModel} from "@web/search/search_model";
import {parseDate} from "@web/core/l10n/dates";
import {qweb} from "web.core";
import {registry} from "@web/core/registry";
import {useBus, useService} from "@web/core/utils/hooks";
export class MisReportWidget extends Component {
setup() {
super.setup();
this.orm = useService("orm");
this.user = useService("user");
this.action = useService("action");
this.view = useService("view");
this.JSON = JSON;
this.state = useState({
mis_report_data: {header: [], body: [], notes: {}},
pivot_date: null,
can_edit_annotation: false,
can_read_annotation: false,
});
this.searchModel = new SearchModel(this.env, {
user: this.user,
orm: this.orm,
view: this.view,
});
useSubEnv({searchModel: this.searchModel});
useBus(this.env.searchModel, "update", async () => {
await this.env.searchModel.sectionsPromise;
this.refresh();
});
onWillStart(this.willStart);
onMounted(this._onMounted);
}
// Lifecycle
async willStart() {
const [result] = await this.orm.read(
"mis.report.instance",
[this._instanceId()],
[
"source_aml_model_name",
"widget_show_filters",
"widget_show_settings_button",
"widget_search_view_id",
"pivot_date",
"widget_show_pivot_date",
"user_can_read_annotation",
"user_can_edit_annotation",
"wide_display_by_default",
],
{context: this.context}
);
this.source_aml_model_name = result.source_aml_model_name;
this.widget_show_filters = result.widget_show_filters;
this.widget_show_settings_button = result.widget_show_settings_button;
this.widget_search_view_id =
result.widget_search_view_id && result.widget_search_view_id[0];
this.state.pivot_date = parseDate(result.pivot_date);
this.widget_show_pivot_date = result.widget_show_pivot_date;
if (this.showSearchBar) {
// Initialize the search model
await this.searchModel.load({
resModel: this.source_aml_model_name,
searchViewId: this.widget_search_view_id,
});
}
this.wide_display = result.wide_display_by_default;
// Compute the report
this.refresh();
this.state.can_edit_annotation = result.user_can_edit_annotation;
this.state.can_read_annotation = result.user_can_read_annotation;
}
async _onMounted() {
this.resize_sheet();
}
get showSearchBar() {
return (
this.source_aml_model_name &&
this.widget_show_filters &&
this.widget_search_view_id
);
}
get showPivotDate() {
return this.widget_show_pivot_date;
}
/**
* Return the id of the mis.report.instance to which the widget is
* bound.
*
* @returns int
*/
_instanceId() {
if (this.props.value) {
return this.props.value;
}
/*
* This trick is needed because in a dashboard the view does
* not seem to be bound to an instance: it seems to be a limitation
* of Odoo dashboards that are not designed to contain forms but
* rather tree views or charts.
*/
var context = this.props.record.context;
if (context.active_model === "mis.report.instance") {
return context.active_id;
}
}
get context() {
var ctx = super.context;
if (this.showSearchBar) {
ctx = {
...ctx,
mis_analytic_domain: this.searchModel.searchDomain,
};
}
if (this.showPivotDate && this.state.pivot_date) {
ctx = {
...ctx,
mis_pivot_date: this.state.pivot_date,
};
}
return ctx;
}
async drilldown(event) {
const drilldown = JSON.parse(event.target.dataset.drilldown);
const action = await this.orm.call(
"mis.report.instance",
"drilldown",
[this._instanceId(), drilldown],
{context: this.context}
);
this.action.doAction(action);
}
async refresh() {
this.state.mis_report_data = await this.orm.call(
"mis.report.instance",
"compute",
[this._instanceId()],
{context: this.context}
);
}
async refresh_annotation() {
this.state.mis_report_data.notes = await this.orm.call(
"mis.report.instance",
"get_notes_by_cell_id",
[this._instanceId()],
{context: this.context}
);
}
async printPdf() {
const action = await this.orm.call(
"mis.report.instance",
"print_pdf",
[this._instanceId()],
{context: this.context}
);
this.action.doAction(action);
}
async exportXls() {
const action = await this.orm.call(
"mis.report.instance",
"export_xls",
[this._instanceId()],
{context: this.context}
);
this.action.doAction(action);
}
async displaySettings() {
const action = await this.orm.call(
"mis.report.instance",
"display_settings",
[this._instanceId()],
{context: this.context}
);
this.action.doAction(action);
}
async _remove_annotation(cell_id) {
await this.orm.call(
"mis.report.instance.annotation",
"remove_annotation",
[cell_id, this._instanceId()],
{context: this.context}
);
this.refresh_annotation();
}
async _save_annotation(cell_id) {
const text = document.querySelector(".o_mis_builder_annotation_text").value;
await this.orm.call(
"mis.report.instance.annotation",
"set_annotation",
[cell_id, this._instanceId(), text],
{context: this.context}
);
await this.refresh_annotation();
}
async annotate(event) {
const cell_id = event.target.dataset.cellId;
const note = this.state.mis_report_data.notes[cell_id];
const note_text = (note && note.text) || "";
var buttons = [
{
text: this.env._t("Save"),
classes: "btn-primary",
close: true,
click: this._save_annotation.bind(this, cell_id),
},
{
text: this.env._t("Cancel"),
close: true,
},
];
if (typeof note !== "undefined") {
buttons.push({
text: this.env._t("Remove"),
classes: "btn-secondary",
close: true,
click: this._remove_annotation.bind(this, cell_id),
});
}
new Dialog(this, {
title: "Annotate",
size: "medium",
$content: $(
qweb.render("mis_builder.annotation_dialog", {
text: note_text,
})
),
buttons: buttons,
}).open();
}
async remove_annotation(event) {
const cell_id = event.target.dataset.cellId;
this._remove_annotation(cell_id);
}
onDateTimeChanged(ev) {
this.state.pivot_date = ev;
this.refresh();
}
async toggle_wide_display() {
this.wide_display = !this.wide_display;
this.resize_sheet();
}
async resize_sheet() {
var sheet_element = document.getElementsByClassName("o_form_sheet")[0];
sheet_element.classList.toggle(
"oe_mis_builder_report_wide_sheet",
this.wide_display
);
var button_resize_element = document.getElementById("icon_resize");
button_resize_element.classList.toggle("fa-expand", !this.wide_display);
button_resize_element.classList.toggle("fa-compress", this.wide_display);
}
}
MisReportWidget.components = {FilterMenu, SearchBar, DatePicker};
MisReportWidget.template = "mis_builder.MisReportWidget";
registry.category("fields").add("mis_report_widget", MisReportWidget);

View file

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="utf-8" ?>
<templates>
<t t-name="mis_builder.MisReportWidget" owl="1">
<div class="oe_mis_builder_content">
<t t-if="state.mis_report_data">
<t t-set="notes" t-value="state.mis_report_data.notes" />
<div class="oe_mis_builder_cp">
<div class="oe_mis_builder_cp_left">
</div>
<div class="oe_mis_builder_cp_right">
<div class="oe_mis_builder_cp_right_top_right">
<div class="oe_mis_builder_action_buttons">
<button
t-on-click="toggle_wide_display"
class="btn btn-secondary"
>
<i id="icon_resize" class="fa" />
</button>
</div>
</div>
<div class="oe_mis_builder_cp_right_top">
<SearchBar t-if="showSearchBar" />
</div>
<div class="oe_mis_builder_cp_right_bottom">
<div class="oe_mis_builder_filter_buttons">
<FilterMenu t-if="showSearchBar" />
<DatePicker
date="state.pivot_date"
onDateTimeChanged="onDateTimeChanged.bind(this)"
placeholder="'Base date...'"
t-if="showPivotDate"
/>
</div>
<div class="oe_mis_builder_action_buttons">
<button t-on-click="refresh" class="btn">
<span class="fa fa-refresh" /> Refresh </button>
<button t-on-click="printPdf" class="btn">
<span class="fa fa-print" /> Print </button>
<button t-on-click="exportXls" class="btn">
<span class="fa fa-download" /> Export </button>
<button
t-on-click="displaySettings"
t-if="widget_show_settings_button"
class="btn"
>
<span class="fa fa-cog" /> Settings </button>
</div>
</div>
</div>
</div>
<div class="o_list_renderer o_renderer table-responsive">
<table
class="o_list_table table table-sm table-hover table-striped mis_builder"
>
<thead>
<tr
t-foreach="state.mis_report_data.header"
t-as="row"
t-key="row_index"
class="oe_list_header_columns"
>
<th class="oe_list_header_char">
</th>
<th
t-foreach="row.cols"
t-as="col"
t-key="col_index"
class="oe_list_header_char mis_builder_collabel"
t-att-colspan="col.colspan"
>
<t t-esc="col.label" />
<t t-if="col.description">
<br />
<t t-esc="col.description" />
</t>
</th>
</tr>
</thead>
<tbody>
<tr
t-foreach="state.mis_report_data.body"
t-as="row"
t-key="row_index"
>
<td t-att="{'style': row.style}">
<t t-esc="row.label" />
<t t-if="row.description">
<br />
<t t-esc="row.description" />
</t>
</td>
<td
t-foreach="row.cells"
t-as="cell"
t-key="cell_index"
t-att="{'style': cell.style, 'title': cell.val_c}"
class="mis_builder_amount oe_mis_builder_dropdown"
>
<div>
<t t-if="cell.drilldown_arg">
<a
href="javascript:void(0)"
class="mis_builder_drilldown"
t-on-click="drilldown"
t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)"
>
<t t-esc="cell.val_r" />
</a>
</t>
<t t-else="">
<t t-esc="cell.val_r" />
</t>
<span class="oe_mis_builder_footnote">
<div t-if="notes[cell.cell_id]">
<a
t-att-id="'note_'+notes[cell.cell_id].sequence"
t-out="notes[cell.cell_id] and notes[cell.cell_id].sequence"
t-att="{'title': notes[cell.cell_id].text}"
href="#footnotes"
/>
</div>
</span>
<div id="dropdown_menu" class="btn-group">
<div
class="dropdown"
t-if="state.can_edit_annotation and cell.can_be_annotated"
>
<div
data-bs-toggle="dropdown"
t-attf-class="dropdown-toggle"
/>
<div
class="dropdown-menu o_filter_menu"
role="menu"
>
<a
href="javascript:void(0)"
t-on-click="annotate"
t-att-data-cell-id="cell.cell_id"
role="menuitem"
class="dropdown-item js_tag"
>
Annotate
</a>
</div>
</div>
<!-- show menu as disabled -->
<div
t-else=""
class="dropdown-toggle oe_mis_builder_menu_disabled"
/>
</div>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr />
</tfoot>
</table>
</div>
<!-- Adding notes -->
<div class="oe_mis_builder_footnote_div" id="footnotes">
<table class="oe_mis_builder_footnote_table">
<t
t-foreach="state.mis_report_data.notes"
t-as="cell_id"
t-key="cell_id"
>
<tr>
<td><a
t-out="notes[cell_id].sequence"
t-att-href="'#note_'+notes[cell_id].sequence"
/>. </td>
<td><t t-out="notes[cell_id].text" /></td>
<td><i
href="javascript:void(0)"
t-on-click="remove_annotation"
t-att-data-cell-id="cell_id"
class="btn fa fa-trash-o"
t-if="state.can_edit_annotation"
/></td>
</tr>
</t>
</table>
</div>
</t>
</div>
</t>
<t t-name="mis_builder.annotation_dialog">
<form role="form">
<textarea
class="o_mis_builder_annotation_text"
name="note"
rows='4'
placeholder="Insert note here"
><t t-out="text" t-att-data-textnote="text" /></textarea>
</form>
</t>
</templates>

View file

@ -0,0 +1,68 @@
.mis_table {
display: table;
width: 100%;
table-layout: fixed;
}
.mis_row {
display: table-row;
page-break-inside: avoid;
}
.mis_cell {
display: table-cell;
page-break-inside: avoid;
}
.mis_thead {
display: table-header-group;
}
.mis_tbody {
display: table-row-group;
}
.mis_table,
.mis_table .mis_row {
border-left: 0px;
border-right: 0px;
text-align: left;
padding-right: 3px;
padding-left: 3px;
padding-top: 2px;
padding-bottom: 2px;
border-collapse: collapse;
}
.mis_table .mis_row {
border-color: grey;
border-bottom: 1px solid lightGrey;
}
.mis_table .mis_cell.mis_collabel {
font-weight: bold;
background-color: #f0f0f0;
text-align: center;
}
.mis_table .mis_cell.mis_rowlabel {
text-align: left;
/*white-space: nowrap;*/
}
.mis_table .mis_cell.mis_amount {
text-align: right;
}
.oe_mis_builder_footnote {
font-size: 70%;
color: red;
position: relative;
bottom: 1ex;
width: 1em;
display: inline-block;
padding-right: 1px;
}
.oe_mis_builder_footnote_div {
padding-top: 1em;
}
.oe_mis_builder_footnote_table {
list-style: none;
white-space: pre-wrap;
display: inline-block;
td {
vertical-align: top;
}
}