feat(fusion_accounting_reports): adopt Enterprise account_reports look
User feedback: 'i like the odoo enterprise style reports, I hate our style.'
Replaces our custom 'o_fusion_reports' visual with a faithful adaptation
of Enterprise account_reports. Same .account_report root class, same
table semantics (line_name + line_cell + line_level_N), same border
treatment (1px gray-300 borders, 0.25rem radius, sticky thead), same
button hover behavior (gray-300 -> enterprise-action-color), same dense
0.8rem font-size + padded cells.
SCSS layout:
- reports.scss in web.assets_backend bundle (eager light)
- reports.dark.scss in web.assets_web_dark bundle (lazy dark mode)
- _variables.scss reduced to spacing/typography only -- colors use
Odoo's \$o-* SCSS vars so dark mode flips automatically via the
separate dark bundle
- old dark_mode.scss removed (was using non-Odoo [data-color-scheme]
selector that never matched anything)
QWeb templates rewritten to mirror Enterprise's structure:
- report_viewer.xml roots at .account_report with scroll container
- report_table.xml uses Enterprise's td.line_name + td.line_cell with
.wrapper > .content nesting; partner-grouped reports now actually
render their aging buckets (previously showed nothing)
- period_filter.xml is now a clean Bootstrap-styled inline filter bar
Kept Fusion-only components but restyled to fit:
- anomaly_strip uses Bootstrap alert-{danger,warning,info} colors
- ai_commentary_panel is a plain bordered panel, no gradients/emojis
- drill_down_dialog unchanged (already a Bootstrap modal)
Made-with: Cursor
This commit is contained in:
@@ -3,10 +3,10 @@
|
||||
|
||||
<t t-name="fusion_accounting_reports.AiCommentaryPanel">
|
||||
<div class="o_fusion_commentary_panel">
|
||||
<h4>📊 AI Commentary</h4>
|
||||
<h4>AI Commentary</h4>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.summary">
|
||||
<p style="margin: 0;"><t t-esc="props.commentary.summary"/></p>
|
||||
<p><t t-esc="props.commentary.summary"/></p>
|
||||
</div>
|
||||
|
||||
<div class="commentary-section" t-if="props.commentary.highlights and props.commentary.highlights.length">
|
||||
@@ -36,7 +36,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="text-muted" style="font-size: 0.75rem;" t-if="props.commentary.cached">
|
||||
<div class="commentary-meta" t-if="props.commentary.cached">
|
||||
Cached • <t t-esc="props.commentary.generated_at"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
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, {
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<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">
|
||||
<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="ms-3 text-muted">
|
||||
severity: <t t-esc="props.anomaly.severity"/>
|
||||
<span class="anomaly_severity">
|
||||
<t t-esc="props.anomaly.severity"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_reports.PeriodFilter">
|
||||
<div class="o_fusion_reports_filters">
|
||||
<div class="o_fusion_report_filters">
|
||||
<select t-on-change="onReportTypeChange"
|
||||
class="form-select" style="max-width: 240px;">
|
||||
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"
|
||||
@@ -14,17 +14,17 @@
|
||||
</select>
|
||||
|
||||
<label>From</label>
|
||||
<input type="date" class="form-control" style="max-width: 160px;"
|
||||
<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" style="max-width: 160px;"
|
||||
<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" style="max-width: 200px;"
|
||||
<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"
|
||||
@@ -32,8 +32,6 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ export class ReportTable extends Component {
|
||||
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, {
|
||||
@@ -22,15 +26,35 @@ export class ReportTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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 = ['report-row', `level-${row.level || 0}`];
|
||||
if (row.is_subtotal) classes.push('subtotal');
|
||||
if (row.account_id) classes.push('drillable');
|
||||
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' : '';
|
||||
return pct > 0 ? 'variance_pos' : pct < 0 ? 'variance_neg' : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,44 +2,106 @@
|
||||
<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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="props.result.rows" t-as="row" t-key="row.id"
|
||||
<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>
|
||||
<span><t t-esc="row.label"/></span>
|
||||
<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="amount">
|
||||
<t t-esc="formatAmount(row.amount)"/>
|
||||
<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="amount">
|
||||
<t t-esc="formatAmount(row.amount_comparison)"/>
|
||||
<td class="line_cell numeric">
|
||||
<div class="wrapper"><div class="content">
|
||||
<t t-esc="formatAmount(row.comparison_amount)"/>
|
||||
</div></div>
|
||||
</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 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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
Reference in New Issue
Block a user