Compare commits

...

8 Commits

Author SHA1 Message Date
gsinghpal
d1661f3a33 feat(fusion_accounting_reports): anomaly_strip OWL component (Fusion-only)
Made-with: Cursor
2026-04-19 16:04:01 -04:00
gsinghpal
8b6dd3aa63 feat(fusion_accounting_reports): ai_commentary_panel OWL component (Fusion-only)
Made-with: Cursor
2026-04-19 16:03:31 -04:00
gsinghpal
4677fae891 feat(fusion_accounting_reports): period_filter component (date range + comparison)
Made-with: Cursor
2026-04-19 16:03:00 -04:00
gsinghpal
1918e03485 feat(fusion_accounting_reports): drill_down_dialog OWL component
Made-with: Cursor
2026-04-19 16:02:21 -04:00
gsinghpal
6d020f6419 feat(fusion_accounting_reports): report_table component with drill chevrons
Made-with: Cursor
2026-04-19 16:01:45 -04:00
gsinghpal
b33e12e587 feat(fusion_accounting_reports): top-level report_viewer OWL component
Made-with: Cursor
2026-04-19 16:01:12 -04:00
gsinghpal
1ffa86b532 feat(fusion_accounting_reports): reports_service.js reactive frontend service
Made-with: Cursor
2026-04-19 16:00:29 -04:00
gsinghpal
1f94927f12 feat(fusion_accounting_reports): SCSS foundation for OWL reports widget
Made-with: Cursor
2026-04-19 15:59:50 -04:00
18 changed files with 843 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Reports',
'version': '19.0.1.0.21',
'version': '19.0.1.0.29',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
'description': """
@@ -39,6 +39,23 @@ menu hides; the engine and AI tools remain available for the chat.
],
'assets': {
'web.assets_backend': [
'fusion_accounting_reports/static/src/scss/_variables.scss',
'fusion_accounting_reports/static/src/scss/reports.scss',
'fusion_accounting_reports/static/src/scss/dark_mode.scss',
'fusion_accounting_reports/static/src/services/reports_service.js',
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.js',
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.xml',
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer_view.js',
'fusion_accounting_reports/static/src/components/report_table/report_table.js',
'fusion_accounting_reports/static/src/components/report_table/report_table.xml',
'fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js',
'fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.xml',
'fusion_accounting_reports/static/src/components/period_filter/period_filter.js',
'fusion_accounting_reports/static/src/components/period_filter/period_filter.xml',
'fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js',
'fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.xml',
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js',
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.xml',
],
},
'installable': True,

View File

@@ -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 },
};
}

View File

@@ -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 style="margin: 0;"><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="text-muted" style="font-size: 0.75rem;" t-if="props.commentary.cached">
Cached • <t t-esc="props.commentary.generated_at"/>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,18 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
export class AnomalyStrip extends Component {
static template = "fusion_accounting_reports.AnomalyStrip";
static props = {
anomaly: { type: Object },
};
formatAmount(amount) {
if (amount === null || amount === undefined) return "";
return new Intl.NumberFormat(undefined, {
minimumFractionDigits: 2, maximumFractionDigits: 2,
signDisplay: 'always',
}).format(amount);
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_reports.AnomalyStrip">
<div class="o_fusion_anomaly_strip" t-att-data-severity="props.anomaly.severity">
<strong><t t-esc="props.anomaly.label"/></strong>
<span class="ms-2">
<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="ms-3 text-muted">
severity: <t t-esc="props.anomaly.severity"/>
</span>
</div>
</t>
</templates>

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,37 @@
/** @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) {
await this.reports.runReport(
reportType, this.state.dateFrom, this.state.dateTo,
this.state.comparison);
}
}
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);
}
}
async onComparisonChange(ev) {
await this.reports.setComparison(ev.target.value);
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_reports.PeriodFilter">
<div class="o_fusion_reports_filters">
<select t-on-change="onReportTypeChange"
class="form-select" 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" 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" style="max-width: 160px;"
t-att-value="state.dateTo || ''"
t-on-change="(ev) => onDateChange('dateTo', ev)"/>
<label>Comparison</label>
<select class="form-select" 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>
<span t-if="state.isLoading" class="text-muted ms-3">Loading...</span>
</div>
</t>
</templates>

View File

@@ -0,0 +1,36 @@
/** @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 },
};
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);
}
}
rowClass(row) {
const classes = ['report-row', `level-${row.level || 0}`];
if (row.is_subtotal) classes.push('subtotal');
if (row.account_id) classes.push('drillable');
return classes.join(' ');
}
varianceClass(pct) {
if (pct === null || pct === undefined) return "";
return pct > 0 ? 'variance-pos' : pct < 0 ? 'variance-neg' : '';
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_reports.ReportTable">
<div class="o_fusion_reports_table">
<table>
<thead>
<tr>
<th>Line</th>
<th class="amount">Amount</th>
<t t-if="props.result.comparison_period">
<th class="amount">
<t t-esc="props.result.comparison_period.label"/>
</th>
<th class="amount">Variance %</th>
</t>
</tr>
</thead>
<tbody>
<tr t-foreach="props.result.rows" t-as="row" t-key="row.id"
t-att-class="rowClass(row)"
t-on-click="() => onRowClick(row)">
<td>
<span><t t-esc="row.label"/></span>
</td>
<td class="amount">
<t t-esc="formatAmount(row.amount)"/>
</td>
<t t-if="props.result.comparison_period">
<td class="amount">
<t t-esc="formatAmount(row.amount_comparison)"/>
</td>
<td class="amount" t-att-class="varianceClass(row.variance_pct)">
<t t-if="row.variance_pct !== null and row.variance_pct !== undefined">
<t t-esc="row.variance_pct.toFixed(1)"/>%
</t>
</td>
</t>
</tr>
</tbody>
</table>
</div>
</t>
</templates>

View File

@@ -0,0 +1,49 @@
// Fusion reports design tokens (extends Phase 1's bank_rec tokens for consistency).
// Colors — semantic
$report-bg-primary: #ffffff;
$report-bg-secondary: #f9fafb;
$report-bg-tertiary: #f3f4f6;
$report-border: #e5e7eb;
$report-text-primary: #111827;
$report-text-secondary: #6b7280;
$report-text-muted: #9ca3af;
$report-accent: #3b82f6;
$report-accent-bg: #eff6ff;
// Severity colors (mirrors bank_rec)
$report-severity-high: #ef4444;
$report-severity-high-bg: #fef2f2;
$report-severity-medium: #f59e0b;
$report-severity-medium-bg: #fffbeb;
$report-severity-low: #10b981;
$report-severity-low-bg: #ecfdf5;
// Variance indicators
$report-variance-positive: #10b981;
$report-variance-negative: #ef4444;
// Spacing
$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;
$report-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
// Borders + radii
$report-border-radius: 0.375rem;
$report-border-radius-md: 0.5rem;
$report-border-radius-lg: 0.75rem;
// Subtotal indentation
$report-indent-per-level: 1.5rem;

View File

@@ -0,0 +1,34 @@
@import "variables";
[data-color-scheme="dark"] .o_fusion_reports {
background: #1f2937;
color: #f9fafb;
&_header, &_table, &_filters, .o_fusion_commentary_panel {
background: #111827;
border-color: #374151;
color: #f9fafb;
}
&_table {
th { background: #1f2937; color: #d1d5db; }
td { border-color: #374151; }
tr.subtotal { background: #1f2937; }
tr.drillable:hover { background: #1e3a8a; }
}
.btn_report {
background: #374151;
border-color: #4b5563;
color: #f9fafb;
&:hover { background: #4b5563; }
&.primary { background: #3b82f6; }
}
.o_fusion_anomaly_strip {
&[data-severity="high"] { background: rgba(239, 68, 68, 0.15); }
&[data-severity="medium"] { background: rgba(245, 158, 11, 0.15); }
&[data-severity="low"] { background: rgba(16, 185, 129, 0.15); }
}
}

View File

@@ -0,0 +1,161 @@
@import "variables";
.o_fusion_reports {
background: $report-bg-secondary;
min-height: 100vh;
&_header {
background: $report-bg-primary;
border-bottom: 1px solid $report-border;
padding: $report-space-4 $report-space-6;
display: flex;
justify-content: space-between;
align-items: center;
h1 {
font-size: $report-font-size-xl;
margin: 0;
}
}
&_table {
background: $report-bg-primary;
border: 1px solid $report-border;
border-radius: $report-border-radius-md;
margin: $report-space-4;
overflow: hidden;
font-family: $report-font-mono;
font-size: $report-font-size-sm;
table {
width: 100%;
border-collapse: collapse;
}
th {
background: $report-bg-tertiary;
padding: $report-space-3 $report-space-4;
text-align: left;
font-weight: 600;
color: $report-text-secondary;
border-bottom: 1px solid $report-border;
}
th.amount, td.amount {
text-align: right;
white-space: nowrap;
}
td {
padding: $report-space-2 $report-space-4;
border-bottom: 1px solid lighten($report-border, 5%);
}
tr.subtotal {
font-weight: 600;
background: $report-bg-secondary;
border-top: 1px solid $report-text-muted;
}
tr.subtotal td {
border-bottom: 1px solid $report-text-muted;
}
tr.drillable {
cursor: pointer;
&:hover { background: $report-accent-bg; }
}
.level-1 { padding-left: $report-space-4 + $report-indent-per-level; }
.level-2 { padding-left: $report-space-4 + $report-indent-per-level * 2; }
.level-3 { padding-left: $report-space-4 + $report-indent-per-level * 3; }
.variance-pos { color: $report-variance-positive; }
.variance-neg { color: $report-variance-negative; }
}
&_filters {
background: $report-bg-primary;
padding: $report-space-3 $report-space-4;
border-bottom: 1px solid $report-border;
display: flex;
gap: $report-space-3;
align-items: center;
flex-wrap: wrap;
}
.btn_report {
padding: $report-space-2 $report-space-4;
border-radius: $report-border-radius;
background: $report-bg-primary;
border: 1px solid $report-border;
color: $report-text-primary;
font-size: $report-font-size-sm;
cursor: pointer;
transition: all 150ms ease-in-out;
&:hover { background: $report-bg-tertiary; }
&.primary {
background: $report-accent;
border-color: $report-accent;
color: white;
&:hover { background: darken($report-accent, 8%); }
}
}
}
.o_fusion_anomaly_strip {
margin: $report-space-3;
padding: $report-space-3;
border-radius: $report-border-radius;
border: 1px solid;
font-size: $report-font-size-sm;
&[data-severity="high"] {
background: $report-severity-high-bg;
border-color: $report-severity-high;
}
&[data-severity="medium"] {
background: $report-severity-medium-bg;
border-color: $report-severity-medium;
}
&[data-severity="low"] {
background: $report-severity-low-bg;
border-color: $report-severity-low;
}
}
.o_fusion_commentary_panel {
background: $report-bg-primary;
border: 1px solid $report-border;
border-radius: $report-border-radius-md;
margin: $report-space-3;
padding: $report-space-4;
h4 {
margin: 0 0 $report-space-3;
font-size: $report-font-size-base;
color: $report-text-primary;
}
.commentary-section {
margin-bottom: $report-space-3;
h5 {
font-size: $report-font-size-sm;
color: $report-text-secondary;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: $report-space-2;
}
ul {
margin: 0;
padding-left: $report-space-4;
li { margin: $report-space-1 0; }
}
}
}

View File

@@ -0,0 +1,147 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { reactive } from "@odoo/owl";
const ENDPOINT_BASE = "/fusion/reports";
export class ReportsService {
constructor(env, services) {
this.env = env;
this.rpc = services.rpc;
this.notification = services.notification;
this.state = reactive({
availableReports: [],
currentReportType: 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') {
this.state.isLoading = true;
this.state.currentReportType = reportType;
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,
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);
}
}
}
export const reportsService = {
dependencies: ["rpc", "notification"],
start(env, services) { return new ReportsService(env, services); },
};
registry.category("services").add("fusion_reports", reportsService);

View File

@@ -0,0 +1,47 @@
/** @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';
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`, 'none');
});
}
onDrillDown(accountId, label) {
this.reports.drillDown(accountId, label);
}
onCloseDrill() {
this.reports.closeDrillDown();
}
async onGenerateCommentary() {
await this.reports.generateCommentary({ forceRegenerate: false });
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_reports.ReportViewer">
<div class="o_fusion_reports">
<div class="o_fusion_reports_header">
<div>
<h1>
<t t-esc="state.currentResult?.report_name || 'Financial Reports'"/>
</h1>
<div class="text-muted" t-if="state.currentResult">
<t t-esc="state.currentResult.period?.label"/>
</div>
</div>
<div>
<button class="btn_report primary"
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 />
<AnomalyStrip t-foreach="state.currentAnomalies" t-as="anomaly"
t-key="anomaly.row_id" anomaly="anomaly"/>
<AiCommentaryPanel t-if="state.currentCommentary" commentary="state.currentCommentary"/>
<ReportTable t-if="state.currentResult" result="state.currentResult"
onDrillDown="onDrillDown.bind(this)"/>
<DrillDownDialog t-if="state.drillDown and state.drillDown.isOpen"
drill="state.drillDown"
onClose="onCloseDrill.bind(this)"/>
</div>
</t>
</templates>

View File

@@ -0,0 +1,14 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { ReportViewer } from "./report_viewer";
export const fusionReportsView = {
type: "fusion_reports",
Controller: ReportViewer,
display_name: "Fusion Financial Reports",
icon: "fa-line-chart",
multiRecord: true,
};
registry.category("views").add("fusion_reports", fusionReportsView);