changes
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class AiCommentaryPanel extends Component {
|
||||
static template = "fusion_accounting_reports.AiCommentaryPanel";
|
||||
static props = {
|
||||
commentary: { type: Object },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.AiCommentaryPanel">
|
||||
<div class="o_fusion_commentary_panel">
|
||||
<h4>AI Commentary</h4>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.summary">
|
||||
<p><t t-esc="props.commentary.summary"/></p>
|
||||
</div>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.highlights and props.commentary.highlights.length">
|
||||
<h5>Highlights</h5>
|
||||
<ul>
|
||||
<li t-foreach="props.commentary.highlights" t-as="h" t-key="h_index">
|
||||
<t t-esc="h"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.concerns and props.commentary.concerns.length">
|
||||
<h5>Concerns</h5>
|
||||
<ul>
|
||||
<li t-foreach="props.commentary.concerns" t-as="c" t-key="c_index">
|
||||
<t t-esc="c"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.next_actions and props.commentary.next_actions.length">
|
||||
<h5>Next Actions</h5>
|
||||
<ul>
|
||||
<li t-foreach="props.commentary.next_actions" t-as="a" t-key="a_index">
|
||||
<t t-esc="a"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="commentary-meta" t-if="props.commentary.cached">
|
||||
Cached • <t t-esc="props.commentary.generated_at"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,28 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const SEVERITY_TO_BS = {
|
||||
high: 'danger',
|
||||
medium: 'warning',
|
||||
low: 'info',
|
||||
};
|
||||
|
||||
export class AnomalyStrip extends Component {
|
||||
static template = "fusion_accounting_reports.AnomalyStrip";
|
||||
static props = {
|
||||
anomaly: { type: Object },
|
||||
};
|
||||
|
||||
get alertClass() {
|
||||
return SEVERITY_TO_BS[this.props.anomaly.severity] || 'secondary';
|
||||
}
|
||||
|
||||
formatAmount(amount) {
|
||||
if (amount === null || amount === undefined) return "";
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
signDisplay: 'always',
|
||||
}).format(amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.AnomalyStrip">
|
||||
<div t-att-class="`o_fusion_anomaly_strip alert-${alertClass}`">
|
||||
<span class="anomaly_label"><t t-esc="props.anomaly.label"/></span>
|
||||
<span class="anomaly_delta">
|
||||
<t t-esc="props.anomaly.direction === 'increase' ? '↑' : '↓'"/>
|
||||
<t t-esc="props.anomaly.variance_pct.toFixed(1)"/>%
|
||||
(<t t-esc="formatAmount(props.anomaly.variance_amount)"/>)
|
||||
</span>
|
||||
<span class="anomaly_severity">
|
||||
<t t-esc="props.anomaly.severity"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,24 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class DrillDownDialog extends Component {
|
||||
static template = "fusion_accounting_reports.DrillDownDialog";
|
||||
static props = {
|
||||
drill: { type: Object },
|
||||
onClose: { type: Function },
|
||||
};
|
||||
|
||||
formatAmount(amount) {
|
||||
if (amount === null || amount === undefined) return "";
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
onBackdropClick(ev) {
|
||||
if (ev.target.classList.contains('modal-backdrop')) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.DrillDownDialog">
|
||||
<div class="modal modal-backdrop"
|
||||
style="display: block; background: rgba(0,0,0,0.5); position: fixed; top:0; left:0; right:0; bottom:0; z-index: 1050;"
|
||||
t-on-click="onBackdropClick">
|
||||
<div class="modal-dialog modal-xl"
|
||||
style="margin: 5vh auto; max-width: 90%;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Drill-down: <t t-esc="props.drill.label || ''"/></h5>
|
||||
<button class="btn-close" t-on-click="props.onClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
|
||||
<div t-if="!props.drill.rows or props.drill.rows.length === 0" class="text-muted">
|
||||
No journal items found.
|
||||
</div>
|
||||
<table t-else="" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Move</th>
|
||||
<th>Account</th>
|
||||
<th>Partner</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Debit</th>
|
||||
<th class="text-end">Credit</th>
|
||||
<th class="text-end">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="props.drill.rows" t-as="row" t-key="row.move_line_id">
|
||||
<td><t t-esc="row.date"/></td>
|
||||
<td><t t-esc="row.move_name"/></td>
|
||||
<td>
|
||||
<span t-att-title="row.account_name">
|
||||
<t t-esc="row.account_code"/>
|
||||
</span>
|
||||
</td>
|
||||
<td><t t-esc="row.partner_name || ''"/></td>
|
||||
<td><t t-esc="row.label"/></td>
|
||||
<td class="text-end"><t t-esc="formatAmount(row.debit)"/></td>
|
||||
<td class="text-end"><t t-esc="formatAmount(row.credit)"/></td>
|
||||
<td class="text-end"><t t-esc="formatAmount(row.balance)"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="text-muted me-auto"><t t-esc="props.drill.count"/> rows</span>
|
||||
<button class="btn_report" t-on-click="props.onClose">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class PeriodFilter extends Component {
|
||||
static template = "fusion_accounting_reports.PeriodFilter";
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
this.reports = useService("fusion_reports");
|
||||
this.state = useState(this.reports.state);
|
||||
}
|
||||
|
||||
async onReportTypeChange(ev) {
|
||||
const reportType = ev.target.value;
|
||||
if (reportType && this.state.dateFrom && this.state.dateTo) {
|
||||
// Switching report type clears the report_code (user is picking
|
||||
// a different category, not a variant).
|
||||
await this.reports.runReport(
|
||||
reportType, this.state.dateFrom, this.state.dateTo,
|
||||
this.state.comparison, null);
|
||||
}
|
||||
}
|
||||
|
||||
async onDateChange(field, ev) {
|
||||
this.state[field] = ev.target.value;
|
||||
if (this.state.currentReportType && this.state.dateFrom && this.state.dateTo) {
|
||||
await this.reports.runReport(
|
||||
this.state.currentReportType,
|
||||
this.state.dateFrom, this.state.dateTo,
|
||||
this.state.comparison,
|
||||
this.state.currentReportCode);
|
||||
}
|
||||
}
|
||||
|
||||
async onComparisonChange(ev) {
|
||||
await this.reports.setComparison(ev.target.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.PeriodFilter">
|
||||
<div class="o_fusion_report_filters">
|
||||
<select t-on-change="onReportTypeChange"
|
||||
class="form-select form-select-sm" style="max-width: 240px;">
|
||||
<option value="">— Select report —</option>
|
||||
<option t-foreach="state.availableReports" t-as="r" t-key="r.id"
|
||||
t-att-value="r.report_type"
|
||||
t-att-selected="r.report_type === state.currentReportType">
|
||||
<t t-esc="r.name"/>
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<label>From</label>
|
||||
<input type="date" class="form-control form-control-sm" style="max-width: 160px;"
|
||||
t-att-value="state.dateFrom || ''"
|
||||
t-on-change="(ev) => onDateChange('dateFrom', ev)"/>
|
||||
|
||||
<label>To</label>
|
||||
<input type="date" class="form-control form-control-sm" style="max-width: 160px;"
|
||||
t-att-value="state.dateTo || ''"
|
||||
t-on-change="(ev) => onDateChange('dateTo', ev)"/>
|
||||
|
||||
<label>Comparison</label>
|
||||
<select class="form-select form-select-sm" style="max-width: 200px;"
|
||||
t-on-change="onComparisonChange">
|
||||
<option value="none" t-att-selected="state.comparison === 'none'">None</option>
|
||||
<option value="previous_period"
|
||||
t-att-selected="state.comparison === 'previous_period'">Previous Period</option>
|
||||
<option value="previous_year"
|
||||
t-att-selected="state.comparison === 'previous_year'">Previous Year</option>
|
||||
</select>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,60 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class ReportTable extends Component {
|
||||
static template = "fusion_accounting_reports.ReportTable";
|
||||
static props = {
|
||||
result: { type: Object },
|
||||
onDrillDown: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
get isPartnerGrouped() {
|
||||
return this.props.result?.report_type === 'partner_grouped';
|
||||
}
|
||||
|
||||
formatAmount(amount) {
|
||||
if (amount === null || amount === undefined) return "";
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
onRowClick(row) {
|
||||
if (row.account_id && this.props.onDrillDown) {
|
||||
this.props.onDrillDown(row.account_id, row.label);
|
||||
}
|
||||
}
|
||||
|
||||
onPartnerRowClick(row) {
|
||||
// Partner-grouped reports are not (yet) drillable through the
|
||||
// dialog; we still expose the click hook for future expansion.
|
||||
if (this.props.onDrillDown && row.partner_id) {
|
||||
// Intentionally no-op until partner drill is wired up.
|
||||
}
|
||||
}
|
||||
|
||||
rowClass(row) {
|
||||
const classes = [`line_level_${row.level || 0}`];
|
||||
if (row.is_subtotal) classes.push('total');
|
||||
if (row.account_id) classes.push('unfoldable');
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
partnerRowClass(row) {
|
||||
const classes = ['line_level_1'];
|
||||
if (row.partner_id) classes.push('unfoldable');
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
lineNameClass(row) {
|
||||
const classes = ['line_name'];
|
||||
if (row.account_id) classes.push('unfoldable');
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
varianceClass(pct) {
|
||||
if (pct === null || pct === undefined) return "";
|
||||
return pct > 0 ? 'variance_pos' : pct < 0 ? 'variance_neg' : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.ReportTable">
|
||||
<table class="table table-borderless table-hover w-print-100 mx-auto">
|
||||
<thead class="sticky">
|
||||
<tr t-if="isPartnerGrouped">
|
||||
<th class="line_name">Partner</th>
|
||||
<th class="line_cell numeric">Current</th>
|
||||
<th class="line_cell numeric">1 - 30</th>
|
||||
<th class="line_cell numeric">31 - 60</th>
|
||||
<th class="line_cell numeric">61 - 90</th>
|
||||
<th class="line_cell numeric">90+</th>
|
||||
<th class="line_cell numeric">Total</th>
|
||||
</tr>
|
||||
<tr t-else="">
|
||||
<th class="line_name"></th>
|
||||
<th class="line_cell numeric">
|
||||
<t t-esc="props.result.period?.label || 'Amount'"/>
|
||||
</th>
|
||||
<t t-if="props.result.comparison_period">
|
||||
<th class="line_cell numeric">
|
||||
<t t-esc="props.result.comparison_period.label"/>
|
||||
</th>
|
||||
<th class="line_cell numeric">Variance</th>
|
||||
<th class="line_cell numeric">%</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Partner-grouped (Aged AR/AP, Partner Ledger) -->
|
||||
<t t-if="isPartnerGrouped">
|
||||
<tr t-foreach="props.result.rows" t-as="row"
|
||||
t-key="row.partner_id || row_index"
|
||||
t-att-class="partnerRowClass(row)"
|
||||
t-on-click="() => onPartnerRowClick(row)">
|
||||
<td class="line_name">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<span class="name"><t t-esc="row.partner_name"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.bucket_current)"/></div></div></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.bucket_1_30)"/></div></div></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.bucket_31_60)"/></div></div></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.bucket_61_90)"/></div></div></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.bucket_90_plus)"/></div></div></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(row.total)"/></div></div></td>
|
||||
</tr>
|
||||
<tr t-if="props.result.total !== undefined" class="total line_level_0">
|
||||
<td class="line_name"><div class="wrapper"><div class="content"><span class="name">Total</span></div></div></td>
|
||||
<td class="line_cell numeric muted"></td>
|
||||
<td class="line_cell numeric muted"></td>
|
||||
<td class="line_cell numeric muted"></td>
|
||||
<td class="line_cell numeric muted"></td>
|
||||
<td class="line_cell numeric muted"></td>
|
||||
<td class="line_cell numeric"><div class="wrapper"><div class="content"><t t-esc="formatAmount(props.result.total)"/></div></div></td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<!-- Standard P&L / Balance Sheet / Trial Balance / GL -->
|
||||
<t t-else="">
|
||||
<tr t-foreach="props.result.rows" t-as="row" t-key="row_index"
|
||||
t-att-class="rowClass(row)"
|
||||
t-on-click="() => onRowClick(row)">
|
||||
<td t-att-class="lineNameClass(row)">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<button t-if="row.account_id" class="btn_foldable">
|
||||
<i class="fa fa-circle-o" aria-hidden="true"/>
|
||||
</button>
|
||||
<span class="name"><t t-esc="row.label"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="line_cell numeric">
|
||||
<div class="wrapper"><div class="content">
|
||||
<t t-esc="formatAmount(row.amount)"/>
|
||||
</div></div>
|
||||
</td>
|
||||
<t t-if="props.result.comparison_period">
|
||||
<td class="line_cell numeric">
|
||||
<div class="wrapper"><div class="content">
|
||||
<t t-esc="formatAmount(row.comparison_amount)"/>
|
||||
</div></div>
|
||||
</td>
|
||||
<td class="line_cell numeric">
|
||||
<div class="wrapper"><div class="content">
|
||||
<t t-esc="formatAmount(row.variance_amount)"/>
|
||||
</div></div>
|
||||
</td>
|
||||
<td t-att-class="`line_cell numeric ${varianceClass(row.variance_pct)}`">
|
||||
<div class="wrapper"><div class="content">
|
||||
<t t-if="row.variance_pct !== null and row.variance_pct !== undefined">
|
||||
<t t-esc="row.variance_pct.toFixed(1)"/>%
|
||||
</t>
|
||||
</div></div>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,30 @@
|
||||
// Fusion reports design tokens.
|
||||
//
|
||||
// COLORS now come from Odoo's own SCSS palette ($o-view-background-color,
|
||||
// $o-gray-100..900, $o-enterprise-action-color). Dark-mode adjustments live
|
||||
// in the separate `reports.dark.scss` bundle (web.assets_web_dark) so they
|
||||
// load automatically when Odoo enters dark mode -- no [data-bs-theme] hack
|
||||
// is needed.
|
||||
//
|
||||
// This file therefore only carries spacing/typography tokens used by the
|
||||
// Fusion-only components (AI commentary panel, anomaly strip).
|
||||
|
||||
// Spacing scale (4px increments)
|
||||
$report-space-1: 0.25rem;
|
||||
$report-space-2: 0.5rem;
|
||||
$report-space-3: 0.75rem;
|
||||
$report-space-4: 1rem;
|
||||
$report-space-5: 1.25rem;
|
||||
$report-space-6: 1.5rem;
|
||||
$report-space-8: 2rem;
|
||||
|
||||
// Typography
|
||||
$report-font-size-xs: 0.75rem;
|
||||
$report-font-size-sm: 0.875rem;
|
||||
$report-font-size-base: 1rem;
|
||||
$report-font-size-lg: 1.125rem;
|
||||
$report-font-size-xl: 1.25rem;
|
||||
|
||||
// Border radius
|
||||
$report-border-radius: 0.25rem;
|
||||
$report-border-radius-md: 0.5rem;
|
||||
@@ -0,0 +1,80 @@
|
||||
// Dark-mode overrides for the Fusion reports viewer.
|
||||
// Loaded only via web.assets_web_dark, mirroring the strategy used by
|
||||
// Enterprise account_reports.dark.scss. The light styles in reports.scss
|
||||
// reference Odoo's $o-* palette so most surfaces flip automatically when
|
||||
// the dark bundle re-derives those palette vars; this file just smooths
|
||||
// over the few spots where Enterprise applies a manual touch-up.
|
||||
|
||||
.account_report {
|
||||
.o_fusion_report_header {
|
||||
background-color: $o-view-background-color;
|
||||
border-bottom-color: $o-gray-700;
|
||||
|
||||
h1 { color: $o-gray-200 }
|
||||
.o_fusion_report_period { color: $o-gray-400 }
|
||||
}
|
||||
|
||||
.o_fusion_report_filters {
|
||||
background-color: $o-view-background-color;
|
||||
border-bottom-color: $o-gray-700;
|
||||
|
||||
label { color: $o-gray-300 }
|
||||
}
|
||||
|
||||
.table {
|
||||
background-color: $o-view-background-color;
|
||||
border-color: $o-gray-700;
|
||||
|
||||
> thead > tr:not(:last-child) > th:not(:first-child) {
|
||||
border-color: $o-gray-700;
|
||||
}
|
||||
|
||||
> tbody > tr {
|
||||
&:not(.empty) > td { border-bottom-color: $o-gray-800 }
|
||||
&.unfolded > td { border-bottom-color: $o-gray-700 }
|
||||
> td.muted { color: $o-gray-600 }
|
||||
&:hover .muted { color: $o-gray-300 }
|
||||
}
|
||||
}
|
||||
|
||||
table.striped {
|
||||
> thead > tr:not(:first-child) > th:nth-child(2n+3) { background: $o-gray-800 }
|
||||
> tbody {
|
||||
> tr:not(.line_level_0):not(.empty) > td:nth-child(2n+3) { background: $o-gray-800 }
|
||||
> tr.line_level_0 > td:nth-child(2n+3) { background: $o-gray-700 }
|
||||
}
|
||||
}
|
||||
|
||||
thead.sticky { background-color: $o-view-background-color }
|
||||
|
||||
.line_level_0 {
|
||||
color: $o-gray-200;
|
||||
|
||||
> td { background-color: $o-gray-700 }
|
||||
.muted { color: $o-gray-500 !important }
|
||||
}
|
||||
|
||||
@for $i from 2 through 16 {
|
||||
.line_level_#{$i} > td { color: $o-gray-300 }
|
||||
}
|
||||
|
||||
.btn_dropdown, .btn_foldable, .btn_foldable_empty,
|
||||
.btn_more, .btn_action {
|
||||
color: $o-gray-600;
|
||||
}
|
||||
.btn_foldable { color: $o-gray-500 }
|
||||
.btn_action {
|
||||
background-color: $o-view-background-color;
|
||||
color: $o-gray-300;
|
||||
border-color: $o-gray-600;
|
||||
}
|
||||
|
||||
.o_fusion_commentary_panel {
|
||||
background-color: $o-view-background-color;
|
||||
border-color: $o-gray-700;
|
||||
|
||||
h4 { color: $o-gray-200 }
|
||||
.commentary-section h5 { color: $o-gray-400 }
|
||||
.commentary-meta { color: $o-gray-500 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// Faithful adaptation of Enterprise account_reports' look.
|
||||
// Source reference:
|
||||
// account_reports/static/src/components/account_report/account_report.scss
|
||||
// We mirror the same root selector (.account_report), the same table
|
||||
// semantics (.line_name + .line_cell + .line_level_N), the same border
|
||||
// treatment (1px gray-300, 0.25rem radius, sticky thead) and the same
|
||||
// button hover behavior (gray-300 -> enterprise action color).
|
||||
//
|
||||
// Trimmed: chatter, annotations, audit-balance and journal-line debug
|
||||
// popovers are Enterprise-only features we do not ship.
|
||||
|
||||
.account_report {
|
||||
//--------------------------------------------------------------------
|
||||
// Header (Fusion-only -- Enterprise uses Odoo ControlPanel; we keep
|
||||
// a lightweight header to host the report title + AI commentary
|
||||
// trigger but style it to feel native).
|
||||
//--------------------------------------------------------------------
|
||||
.o_fusion_report_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
background-color: $o-view-background-color;
|
||||
border-bottom: 1px solid $o-gray-300;
|
||||
|
||||
h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: $o-gray-800;
|
||||
}
|
||||
|
||||
.o_fusion_report_period {
|
||||
font-size: 0.8rem;
|
||||
color: $o-gray-600;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Scroll container (mirrors Enterprise)
|
||||
//--------------------------------------------------------------------
|
||||
.o_account_report_scroll_container {
|
||||
margin-inline: 0 !important;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Table (verbatim from Enterprise, minus chatter/annotation/audit)
|
||||
//--------------------------------------------------------------------
|
||||
.table {
|
||||
background-color: $o-view-background-color;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 0.8rem;
|
||||
margin: 24px 24px;
|
||||
padding: 24px;
|
||||
width: auto;
|
||||
min-width: 800px;
|
||||
border: 1px solid $o-gray-300;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
> :not(caption) > * > * { padding: 0.25rem 0.75rem }
|
||||
|
||||
> thead {
|
||||
> tr {
|
||||
th:first-child {
|
||||
color: lightgrey;
|
||||
}
|
||||
th:not(:first-child) {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
> tr:not(:last-child) > th:not(:first-child) { border: 1px solid $o-gray-300 }
|
||||
}
|
||||
|
||||
> tbody {
|
||||
> tr {
|
||||
&.unfolded { font-weight: bold }
|
||||
> td {
|
||||
a { cursor: pointer }
|
||||
.clickable { color: $o-enterprise-action-color }
|
||||
&.muted { color: $o-gray-300 }
|
||||
&:empty::after{ content: "\00a0" }
|
||||
&:empty { line-height: 1 }
|
||||
}
|
||||
|
||||
&:not(.empty) > td { border-bottom: 1px solid $o-gray-200 }
|
||||
&.total { font-weight: bold }
|
||||
&.o_bold_tr { font-weight: bold }
|
||||
|
||||
&.unfolded {
|
||||
> td { border-bottom: 1px solid $o-gray-300 }
|
||||
.btn_action { opacity: 1 }
|
||||
.btn_more { opacity: 1 }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.empty > * { --table-accent-bg: transparent }
|
||||
.muted { color: $o-gray-800 }
|
||||
.btn_action, .btn_more {
|
||||
opacity: 1;
|
||||
color: $o-enterprise-action-color;
|
||||
}
|
||||
.btn_dropdown { color: $o-enterprise-action-color }
|
||||
.btn_foldable { color: $o-enterprise-action-color }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table.striped {
|
||||
> thead > tr:not(:first-child) > th:nth-child(2n+3) { background: $o-gray-100 }
|
||||
> tbody {
|
||||
> tr:not(.line_level_0):not(.empty) > td:nth-child(2n+3) { background: $o-gray-100 }
|
||||
> tr.line_level_0 > td:nth-child(2n+3) { background: $o-gray-300 }
|
||||
}
|
||||
}
|
||||
|
||||
thead.sticky {
|
||||
background-color: $o-view-background-color;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Line cells
|
||||
//--------------------------------------------------------------------
|
||||
.line_name {
|
||||
vertical-align: middle;
|
||||
> .wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.name { white-space: nowrap }
|
||||
&.draft { color: $o-info; }
|
||||
&.unfoldable:hover { cursor: pointer }
|
||||
}
|
||||
|
||||
.line_cell {
|
||||
vertical-align: middle;
|
||||
> .wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.date > .wrapper { justify-content: center }
|
||||
&.numeric > .wrapper { justify-content: flex-end }
|
||||
.name { white-space: nowrap }
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Indentation per level
|
||||
//--------------------------------------------------------------------
|
||||
.line_level_0 {
|
||||
color: $o-gray-700;
|
||||
font-weight: bold;
|
||||
|
||||
> td {
|
||||
border-bottom: 0 !important;
|
||||
background-color: $o-gray-300;
|
||||
}
|
||||
.muted { color: $o-gray-400 !important }
|
||||
}
|
||||
|
||||
@for $i from 2 through 16 {
|
||||
.line_level_#{$i} {
|
||||
$indentation: (($i + 1) * 8px) - 20px;
|
||||
|
||||
> td {
|
||||
color: $o-gray-700;
|
||||
|
||||
&.line_name.unfoldable .wrapper { column-gap: calc(#{ $indentation }) }
|
||||
&.line_name:not(.unfoldable) .wrapper { padding-left: $indentation }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Variance helpers (Fusion-only -- comparison reports surface signed
|
||||
// deltas right-aligned in their own column).
|
||||
//--------------------------------------------------------------------
|
||||
.variance_pos { color: $o-success }
|
||||
.variance_neg { color: $o-danger }
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Link
|
||||
//--------------------------------------------------------------------
|
||||
.link { color: $o-enterprise-action-color }
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Buttons (foldable / dropdown / action / more)
|
||||
//--------------------------------------------------------------------
|
||||
.btn_dropdown, .btn_foldable, .btn_foldable_empty,
|
||||
.btn_more, .btn_action {
|
||||
border: none;
|
||||
color: $o-gray-300;
|
||||
font-size: inherit;
|
||||
font-weight: normal;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: $o-enterprise-action-color !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.btn_foldable { color: $o-gray-500 }
|
||||
.btn_foldable_empty:hover { cursor: default }
|
||||
.btn_more { opacity: 1 }
|
||||
.btn_action {
|
||||
opacity: 0;
|
||||
background-color: $o-view-background-color;
|
||||
color: $o-gray-600;
|
||||
width: auto;
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 0.25rem;
|
||||
border: 1px solid $o-gray-300;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Dropdown
|
||||
//--------------------------------------------------------------------
|
||||
.dropdown { display: inline }
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Period filter bar (Fusion-only -- Enterprise has a much richer
|
||||
// filter component; we keep our minimal date+comparison+report-type
|
||||
// bar but style it to feel native to .account_report).
|
||||
//--------------------------------------------------------------------
|
||||
.account_report .o_fusion_report_filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 1.5rem;
|
||||
background-color: $o-view-background-color;
|
||||
border-bottom: 1px solid $o-gray-200;
|
||||
font-size: 0.8rem;
|
||||
|
||||
label {
|
||||
color: $o-gray-700;
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.form-select, .form-control {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// AI commentary panel (Fusion-only addition)
|
||||
//--------------------------------------------------------------------
|
||||
.account_report .o_fusion_commentary_panel {
|
||||
background-color: $o-view-background-color;
|
||||
border: 1px solid $o-gray-300;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0 24px 24px;
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
h4 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
color: $o-gray-800;
|
||||
}
|
||||
|
||||
.commentary-section {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h5 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: $o-gray-600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
li { margin: 0.15rem 0 }
|
||||
}
|
||||
|
||||
p { margin: 0 }
|
||||
}
|
||||
|
||||
.commentary-meta {
|
||||
font-size: 0.7rem;
|
||||
color: $o-gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Anomaly strip (Fusion-only) -- now a plain Bootstrap-style alert,
|
||||
// inset to align with the report table padding.
|
||||
//--------------------------------------------------------------------
|
||||
.account_report .o_fusion_anomaly_strip {
|
||||
margin: 0.5rem 24px 0;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.anomaly_label { font-weight: 600 }
|
||||
.anomaly_delta { margin-left: 0.5rem }
|
||||
.anomaly_severity {
|
||||
margin-left: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { reactive } from "@odoo/owl";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
const ENDPOINT_BASE = "/fusion/reports";
|
||||
|
||||
export class ReportsService {
|
||||
constructor(env, services) {
|
||||
this.env = env;
|
||||
// V19: rpc is a standalone import, not a service.
|
||||
this.rpc = rpc;
|
||||
this.notification = services.notification;
|
||||
|
||||
this.state = reactive({
|
||||
availableReports: [],
|
||||
currentReportType: null,
|
||||
currentReportCode: null,
|
||||
currentResult: null,
|
||||
currentAnomalies: [],
|
||||
currentCommentary: null,
|
||||
isLoading: false,
|
||||
isGeneratingCommentary: false,
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
comparison: 'none',
|
||||
companyId: null,
|
||||
drillDown: null,
|
||||
});
|
||||
}
|
||||
|
||||
async loadAvailableReports(companyId = null) {
|
||||
this.state.companyId = companyId;
|
||||
this.state.isLoading = true;
|
||||
try {
|
||||
const result = await this.rpc(`${ENDPOINT_BASE}/list_available`,
|
||||
{ company_id: companyId });
|
||||
this.state.availableReports = result.reports;
|
||||
} finally {
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async runReport(reportType, dateFrom, dateTo, comparison = 'none', reportCode = null) {
|
||||
this.state.isLoading = true;
|
||||
this.state.currentReportType = reportType;
|
||||
this.state.currentReportCode = reportCode;
|
||||
this.state.dateFrom = dateFrom;
|
||||
this.state.dateTo = dateTo;
|
||||
this.state.comparison = comparison;
|
||||
try {
|
||||
this.state.currentResult = await this.rpc(`${ENDPOINT_BASE}/run`, {
|
||||
report_type: reportType,
|
||||
report_code: reportCode,
|
||||
date_from: dateFrom,
|
||||
date_to: dateTo,
|
||||
comparison: comparison,
|
||||
company_id: this.state.companyId,
|
||||
});
|
||||
if (comparison && comparison !== 'none') {
|
||||
this.fetchAnomalies();
|
||||
} else {
|
||||
this.state.currentAnomalies = [];
|
||||
}
|
||||
this.state.currentCommentary = null;
|
||||
return this.state.currentResult;
|
||||
} catch (err) {
|
||||
this.notification.add(`Run failed: ${err.message || err}`, { type: 'danger' });
|
||||
throw err;
|
||||
} finally {
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAnomalies() {
|
||||
if (!this.state.currentReportType) return;
|
||||
try {
|
||||
const result = await this.rpc(`${ENDPOINT_BASE}/get_anomalies`, {
|
||||
report_type: this.state.currentReportType,
|
||||
date_from: this.state.dateFrom,
|
||||
date_to: this.state.dateTo,
|
||||
comparison: this.state.comparison,
|
||||
company_id: this.state.companyId,
|
||||
});
|
||||
this.state.currentAnomalies = result.anomalies || [];
|
||||
} catch (err) {
|
||||
this.state.currentAnomalies = [];
|
||||
}
|
||||
}
|
||||
|
||||
async generateCommentary({ forceRegenerate = false } = {}) {
|
||||
if (!this.state.currentReportType) return;
|
||||
this.state.isGeneratingCommentary = true;
|
||||
try {
|
||||
this.state.currentCommentary = await this.rpc(`${ENDPOINT_BASE}/get_commentary`, {
|
||||
report_type: this.state.currentReportType,
|
||||
date_from: this.state.dateFrom,
|
||||
date_to: this.state.dateTo,
|
||||
comparison: this.state.comparison,
|
||||
company_id: this.state.companyId,
|
||||
force_regenerate: forceRegenerate,
|
||||
});
|
||||
return this.state.currentCommentary;
|
||||
} catch (err) {
|
||||
this.notification.add(`Commentary failed: ${err.message || err}`, { type: 'danger' });
|
||||
throw err;
|
||||
} finally {
|
||||
this.state.isGeneratingCommentary = false;
|
||||
}
|
||||
}
|
||||
|
||||
async drillDown(accountId, label = null) {
|
||||
try {
|
||||
const result = await this.rpc(`${ENDPOINT_BASE}/drill_down`, {
|
||||
account_id: accountId,
|
||||
date_from: this.state.dateFrom,
|
||||
date_to: this.state.dateTo,
|
||||
company_id: this.state.companyId,
|
||||
});
|
||||
this.state.drillDown = {
|
||||
accountId, label, rows: result.rows || [],
|
||||
count: result.count, isOpen: true,
|
||||
};
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.notification.add(`Drill failed: ${err.message || err}`, { type: 'danger' });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
closeDrillDown() {
|
||||
if (this.state.drillDown) {
|
||||
this.state.drillDown.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
setComparison(mode) {
|
||||
this.state.comparison = mode;
|
||||
if (this.state.currentReportType) {
|
||||
return this.runReport(this.state.currentReportType,
|
||||
this.state.dateFrom, this.state.dateTo, mode,
|
||||
this.state.currentReportCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const reportsService = {
|
||||
dependencies: ["notification"],
|
||||
start(env, services) { return new ReportsService(env, services); },
|
||||
};
|
||||
|
||||
registry.category("services").add("fusion_reports", reportsService);
|
||||
@@ -0,0 +1,60 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
* 5 OWL tours for fusion_accounting_reports smoke testing.
|
||||
*
|
||||
* Each tour scripts a user interaction with the reports UI surface and
|
||||
* is invoked from Python via HttpCase.start_tour(). Useful for catching
|
||||
* UI regressions that asset-bundle compilation alone won't catch.
|
||||
*/
|
||||
|
||||
// Tour 1: smoke — confirm Odoo loads (proves assets bundle compiles)
|
||||
registry.category("web_tour.tours").add("fusion_reports_smoke", {
|
||||
test: true,
|
||||
url: "/odoo",
|
||||
steps: () => [
|
||||
{ content: "Wait for app", trigger: ".o_navbar" },
|
||||
],
|
||||
});
|
||||
|
||||
// Tour 2: open the period picker wizard
|
||||
registry.category("web_tour.tours").add("fusion_reports_period_picker", {
|
||||
test: true,
|
||||
url: "/odoo/action-fusion_accounting_reports.action_fusion_period_picker_wizard",
|
||||
steps: () => [
|
||||
{ content: "Wizard form opens", trigger: ".modal-dialog .o_form_view" },
|
||||
{ content: "Report type field exists", trigger: ".modal-dialog [name='report_type']" },
|
||||
{ content: "Close wizard", trigger: ".modal-dialog .btn-secondary", run: "click" },
|
||||
],
|
||||
});
|
||||
|
||||
// Tour 3: open the XLSX export wizard
|
||||
registry.category("web_tour.tours").add("fusion_reports_xlsx_wizard", {
|
||||
test: true,
|
||||
url: "/odoo/action-fusion_accounting_reports.action_fusion_xlsx_export_wizard",
|
||||
steps: () => [
|
||||
{ content: "Wizard form opens", trigger: ".modal-dialog .o_form_view" },
|
||||
{ content: "Report type field exists", trigger: ".modal-dialog [name='report_type']" },
|
||||
{ content: "Close wizard", trigger: ".modal-dialog .btn-secondary", run: "click" },
|
||||
],
|
||||
});
|
||||
|
||||
// Tour 4: anomaly list view loads
|
||||
registry.category("web_tour.tours").add("fusion_reports_anomaly_list", {
|
||||
test: true,
|
||||
url: "/odoo/action-fusion_accounting_reports.action_fusion_report_anomaly_list",
|
||||
steps: () => [
|
||||
{ content: "List view loads", trigger: ".o_list_view, .o_view_nocontent" },
|
||||
],
|
||||
});
|
||||
|
||||
// Tour 5: report viewer mounts (smoke — confirm assets compile cleanly)
|
||||
registry.category("web_tour.tours").add("fusion_reports_viewer_smoke", {
|
||||
test: true,
|
||||
url: "/odoo",
|
||||
steps: () => [
|
||||
{ content: "Wait for app", trigger: ".o_navbar" },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ReportTable } from "../../components/report_table/report_table";
|
||||
import { PeriodFilter } from "../../components/period_filter/period_filter";
|
||||
import { DrillDownDialog } from "../../components/drill_down_dialog/drill_down_dialog";
|
||||
import { AiCommentaryPanel } from "../../components/ai_commentary_panel/ai_commentary_panel";
|
||||
import { AnomalyStrip } from "../../components/anomaly_strip/anomaly_strip";
|
||||
|
||||
export class ReportViewer extends Component {
|
||||
static template = "fusion_accounting_reports.ReportViewer";
|
||||
static props = { "*": true };
|
||||
static components = {
|
||||
ReportTable, PeriodFilter, DrillDownDialog,
|
||||
AiCommentaryPanel, AnomalyStrip,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.reports = useService("fusion_reports");
|
||||
this.state = useState(this.reports.state);
|
||||
|
||||
const ctx = this.props.action?.context || {};
|
||||
const reportType = ctx.default_report_type || 'pnl';
|
||||
// default_report_code lets multiple reports of the same type
|
||||
// (e.g. pnl, cash_flow, executive_summary, annual_statements all
|
||||
// type='pnl') resolve to their own line_specs.
|
||||
const reportCode = ctx.default_report_code || null;
|
||||
const comparison = ctx.default_comparison || 'none';
|
||||
const companyId = this.env.services.user?.context?.allowed_company_ids?.[0];
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.reports.loadAvailableReports(companyId);
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
await this.reports.runReport(
|
||||
reportType, `${year}-01-01`, `${year}-12-31`,
|
||||
comparison, reportCode);
|
||||
});
|
||||
}
|
||||
|
||||
onDrillDown(accountId, label) {
|
||||
this.reports.drillDown(accountId, label);
|
||||
}
|
||||
|
||||
onCloseDrill() {
|
||||
this.reports.closeDrillDown();
|
||||
}
|
||||
|
||||
async onGenerateCommentary() {
|
||||
await this.reports.generateCommentary({ forceRegenerate: false });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.ReportViewer">
|
||||
<div class="o_action account_report">
|
||||
<div class="o_fusion_report_header">
|
||||
<div>
|
||||
<h1>
|
||||
<t t-esc="state.currentResult?.report_name || 'Financial Reports'"/>
|
||||
</h1>
|
||||
<div class="o_fusion_report_period" t-if="state.currentResult">
|
||||
<t t-esc="state.currentResult.period?.label"/>
|
||||
<t t-if="state.currentResult.comparison_period">
|
||||
vs <t t-esc="state.currentResult.comparison_period.label"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span t-if="state.isLoading" class="text-muted small">Loading...</span>
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
t-on-click="onGenerateCommentary"
|
||||
t-att-disabled="state.isGeneratingCommentary">
|
||||
<t t-if="state.isGeneratingCommentary">Generating...</t>
|
||||
<t t-else="">AI Commentary</t>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PeriodFilter />
|
||||
|
||||
<div t-if="state.currentAnomalies and state.currentAnomalies.length" class="warnings d-print-none">
|
||||
<AnomalyStrip t-foreach="state.currentAnomalies" t-as="anomaly"
|
||||
t-key="anomaly_index" anomaly="anomaly"/>
|
||||
</div>
|
||||
|
||||
<div class="o_account_report_scroll_container overflow-x-auto">
|
||||
<ReportTable t-if="state.currentResult" result="state.currentResult"
|
||||
onDrillDown="onDrillDown.bind(this)"/>
|
||||
</div>
|
||||
|
||||
<AiCommentaryPanel t-if="state.currentCommentary" commentary="state.currentCommentary"/>
|
||||
|
||||
<DrillDownDialog t-if="state.drillDown and state.drillDown.isOpen"
|
||||
drill="state.drillDown"
|
||||
onClose="onCloseDrill.bind(this)"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ReportViewer } from "./report_viewer";
|
||||
|
||||
// Register as a CLIENT ACTION (not a view). View types in V19 require an
|
||||
// ir.ui.view record per type; client actions don't. The Fusion report
|
||||
// viewer doesn't render records of a model \u2014 it's a custom dashboard \u2014
|
||||
// so client-action is the architecturally correct dispatch.
|
||||
//
|
||||
// Menu \u2192 ir.actions.client(tag="fusion_reports") \u2192 ReportViewer mounts
|
||||
// with this.props.action.context carrying default_report_type +
|
||||
// default_report_code.
|
||||
registry.category("actions").add("fusion_reports", ReportViewer);
|
||||
Reference in New Issue
Block a user