This commit is contained in:
gsinghpal
2026-05-16 13:18:52 -04:00
parent 191a9c82be
commit 9ebf89bde2
1080 changed files with 0 additions and 1197 deletions

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

View File

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

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

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

View File

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

View File

@@ -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' : '';
}
}

View File

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