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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Reports',
|
'name': 'Fusion Accounting Reports',
|
||||||
'version': '19.0.1.1.1',
|
'version': '19.0.1.2.0',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
||||||
'description': """
|
'description': """
|
||||||
@@ -57,7 +57,6 @@ menu hides; the engine and AI tools remain available for the chat.
|
|||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'fusion_accounting_reports/static/src/scss/_variables.scss',
|
'fusion_accounting_reports/static/src/scss/_variables.scss',
|
||||||
'fusion_accounting_reports/static/src/scss/reports.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/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.js',
|
||||||
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.xml',
|
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.xml',
|
||||||
@@ -73,6 +72,9 @@ menu hides; the engine and AI tools remain available for the chat.
|
|||||||
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js',
|
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js',
|
||||||
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.xml',
|
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.xml',
|
||||||
],
|
],
|
||||||
|
'web.assets_web_dark': [
|
||||||
|
'fusion_accounting_reports/static/src/scss/reports.dark.scss',
|
||||||
|
],
|
||||||
'web.assets_tests': [
|
'web.assets_tests': [
|
||||||
'fusion_accounting_reports/static/src/tours/reports_tours.js',
|
'fusion_accounting_reports/static/src/tours/reports_tours.js',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""Pre-migration: convert legacy act_window report actions to client actions.
|
||||||
|
|
||||||
|
In 19.0.1.1.1 we shipped 11 ``ir.actions.act_window`` records with
|
||||||
|
``view_mode='fusion_reports'``. Odoo's act_window resolver requires a
|
||||||
|
matching ``ir.ui.view`` record per view_mode, so clicking those menus
|
||||||
|
raised "View types not defined fusion_reports".
|
||||||
|
|
||||||
|
19.0.1.1.2 reissues the same xml_ids as ``ir.actions.client`` records
|
||||||
|
with ``tag='fusion_reports'``. Odoo refuses to update a record across
|
||||||
|
models, so this pre-migration drops the legacy records before the new
|
||||||
|
data file loads. Idempotent: safe to re-run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
LEGACY_XIDS = (
|
||||||
|
'action_fusion_report_pnl',
|
||||||
|
'action_fusion_report_balance_sheet',
|
||||||
|
'action_fusion_report_trial_balance',
|
||||||
|
'action_fusion_report_general_ledger',
|
||||||
|
'action_fusion_report_cash_flow',
|
||||||
|
'action_fusion_report_executive_summary',
|
||||||
|
'action_fusion_report_annual_statements',
|
||||||
|
'action_fusion_report_tax_summary',
|
||||||
|
'action_fusion_report_aged_receivable',
|
||||||
|
'action_fusion_report_aged_payable',
|
||||||
|
'action_fusion_report_partner_ledger',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
if not version:
|
||||||
|
return
|
||||||
|
deleted = 0
|
||||||
|
for name in LEGACY_XIDS:
|
||||||
|
cr.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, model, res_id
|
||||||
|
FROM ir_model_data
|
||||||
|
WHERE module = 'fusion_accounting_reports' AND name = %s
|
||||||
|
""",
|
||||||
|
(name,),
|
||||||
|
)
|
||||||
|
row = cr.fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
ir_md_id, model, res_id = row
|
||||||
|
if model != 'ir.actions.act_window':
|
||||||
|
continue
|
||||||
|
cr.execute(
|
||||||
|
"DELETE FROM ir_act_window WHERE id = %s",
|
||||||
|
(res_id,),
|
||||||
|
)
|
||||||
|
cr.execute(
|
||||||
|
"DELETE FROM ir_actions WHERE id = %s",
|
||||||
|
(res_id,),
|
||||||
|
)
|
||||||
|
cr.execute(
|
||||||
|
"DELETE FROM ir_model_data WHERE id = %s",
|
||||||
|
(ir_md_id,),
|
||||||
|
)
|
||||||
|
deleted += 1
|
||||||
|
_logger.info("Dropped legacy act_window for fusion_accounting_reports.%s", name)
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
_logger.info(
|
||||||
|
"fusion_accounting_reports pre-migration: dropped %d legacy "
|
||||||
|
"act_window records to make way for ir.actions.client variants.",
|
||||||
|
deleted,
|
||||||
|
)
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
<t t-name="fusion_accounting_reports.AiCommentaryPanel">
|
<t t-name="fusion_accounting_reports.AiCommentaryPanel">
|
||||||
<div class="o_fusion_commentary_panel">
|
<div class="o_fusion_commentary_panel">
|
||||||
<h4>📊 AI Commentary</h4>
|
<h4>AI Commentary</h4>
|
||||||
|
|
||||||
<div class="commentary-section" t-if="props.commentary.summary">
|
<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>
|
||||||
|
|
||||||
<div class="commentary-section" t-if="props.commentary.highlights and props.commentary.highlights.length">
|
<div class="commentary-section" t-if="props.commentary.highlights and props.commentary.highlights.length">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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"/>
|
Cached • <t t-esc="props.commentary.generated_at"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,22 @@
|
|||||||
|
|
||||||
import { Component } from "@odoo/owl";
|
import { Component } from "@odoo/owl";
|
||||||
|
|
||||||
|
const SEVERITY_TO_BS = {
|
||||||
|
high: 'danger',
|
||||||
|
medium: 'warning',
|
||||||
|
low: 'info',
|
||||||
|
};
|
||||||
|
|
||||||
export class AnomalyStrip extends Component {
|
export class AnomalyStrip extends Component {
|
||||||
static template = "fusion_accounting_reports.AnomalyStrip";
|
static template = "fusion_accounting_reports.AnomalyStrip";
|
||||||
static props = {
|
static props = {
|
||||||
anomaly: { type: Object },
|
anomaly: { type: Object },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
get alertClass() {
|
||||||
|
return SEVERITY_TO_BS[this.props.anomaly.severity] || 'secondary';
|
||||||
|
}
|
||||||
|
|
||||||
formatAmount(amount) {
|
formatAmount(amount) {
|
||||||
if (amount === null || amount === undefined) return "";
|
if (amount === null || amount === undefined) return "";
|
||||||
return new Intl.NumberFormat(undefined, {
|
return new Intl.NumberFormat(undefined, {
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="fusion_accounting_reports.AnomalyStrip">
|
<t t-name="fusion_accounting_reports.AnomalyStrip">
|
||||||
<div class="o_fusion_anomaly_strip" t-att-data-severity="props.anomaly.severity">
|
<div t-att-class="`o_fusion_anomaly_strip alert-${alertClass}`">
|
||||||
<strong><t t-esc="props.anomaly.label"/></strong>
|
<span class="anomaly_label"><t t-esc="props.anomaly.label"/></span>
|
||||||
<span class="ms-2">
|
<span class="anomaly_delta">
|
||||||
<t t-esc="props.anomaly.direction === 'increase' ? '↑' : '↓'"/>
|
<t t-esc="props.anomaly.direction === 'increase' ? '↑' : '↓'"/>
|
||||||
<t t-esc="props.anomaly.variance_pct.toFixed(1)"/>%
|
<t t-esc="props.anomaly.variance_pct.toFixed(1)"/>%
|
||||||
(<t t-esc="formatAmount(props.anomaly.variance_amount)"/>)
|
(<t t-esc="formatAmount(props.anomaly.variance_amount)"/>)
|
||||||
</span>
|
</span>
|
||||||
<span class="ms-3 text-muted">
|
<span class="anomaly_severity">
|
||||||
severity: <t t-esc="props.anomaly.severity"/>
|
<t t-esc="props.anomaly.severity"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="fusion_accounting_reports.PeriodFilter">
|
<t t-name="fusion_accounting_reports.PeriodFilter">
|
||||||
<div class="o_fusion_reports_filters">
|
<div class="o_fusion_report_filters">
|
||||||
<select t-on-change="onReportTypeChange"
|
<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 value="">— Select report —</option>
|
||||||
<option t-foreach="state.availableReports" t-as="r" t-key="r.id"
|
<option t-foreach="state.availableReports" t-as="r" t-key="r.id"
|
||||||
t-att-value="r.report_type"
|
t-att-value="r.report_type"
|
||||||
@@ -14,17 +14,17 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label>From</label>
|
<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-att-value="state.dateFrom || ''"
|
||||||
t-on-change="(ev) => onDateChange('dateFrom', ev)"/>
|
t-on-change="(ev) => onDateChange('dateFrom', ev)"/>
|
||||||
|
|
||||||
<label>To</label>
|
<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-att-value="state.dateTo || ''"
|
||||||
t-on-change="(ev) => onDateChange('dateTo', ev)"/>
|
t-on-change="(ev) => onDateChange('dateTo', ev)"/>
|
||||||
|
|
||||||
<label>Comparison</label>
|
<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">
|
t-on-change="onComparisonChange">
|
||||||
<option value="none" t-att-selected="state.comparison === 'none'">None</option>
|
<option value="none" t-att-selected="state.comparison === 'none'">None</option>
|
||||||
<option value="previous_period"
|
<option value="previous_period"
|
||||||
@@ -32,8 +32,6 @@
|
|||||||
<option value="previous_year"
|
<option value="previous_year"
|
||||||
t-att-selected="state.comparison === 'previous_year'">Previous Year</option>
|
t-att-selected="state.comparison === 'previous_year'">Previous Year</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<span t-if="state.isLoading" class="text-muted ms-3">Loading...</span>
|
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export class ReportTable extends Component {
|
|||||||
onDrillDown: { type: Function, optional: true },
|
onDrillDown: { type: Function, optional: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
get isPartnerGrouped() {
|
||||||
|
return this.props.result?.report_type === 'partner_grouped';
|
||||||
|
}
|
||||||
|
|
||||||
formatAmount(amount) {
|
formatAmount(amount) {
|
||||||
if (amount === null || amount === undefined) return "";
|
if (amount === null || amount === undefined) return "";
|
||||||
return new Intl.NumberFormat(undefined, {
|
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) {
|
rowClass(row) {
|
||||||
const classes = ['report-row', `level-${row.level || 0}`];
|
const classes = [`line_level_${row.level || 0}`];
|
||||||
if (row.is_subtotal) classes.push('subtotal');
|
if (row.is_subtotal) classes.push('total');
|
||||||
if (row.account_id) classes.push('drillable');
|
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(' ');
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
varianceClass(pct) {
|
varianceClass(pct) {
|
||||||
if (pct === null || pct === undefined) return "";
|
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">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="fusion_accounting_reports.ReportTable">
|
<t t-name="fusion_accounting_reports.ReportTable">
|
||||||
<div class="o_fusion_reports_table">
|
<table class="table table-borderless table-hover w-print-100 mx-auto">
|
||||||
<table>
|
<thead class="sticky">
|
||||||
<thead>
|
<tr t-if="isPartnerGrouped">
|
||||||
<tr>
|
<th class="line_name">Partner</th>
|
||||||
<th>Line</th>
|
<th class="line_cell numeric">Current</th>
|
||||||
<th class="amount">Amount</th>
|
<th class="line_cell numeric">1 - 30</th>
|
||||||
<t t-if="props.result.comparison_period">
|
<th class="line_cell numeric">31 - 60</th>
|
||||||
<th class="amount">
|
<th class="line_cell numeric">61 - 90</th>
|
||||||
<t t-esc="props.result.comparison_period.label"/>
|
<th class="line_cell numeric">90+</th>
|
||||||
</th>
|
<th class="line_cell numeric">Total</th>
|
||||||
<th class="amount">Variance %</th>
|
</tr>
|
||||||
</t>
|
<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>
|
||||||
</thead>
|
<tr t-if="props.result.total !== undefined" class="total line_level_0">
|
||||||
<tbody>
|
<td class="line_name"><div class="wrapper"><div class="content"><span class="name">Total</span></div></div></td>
|
||||||
<tr t-foreach="props.result.rows" t-as="row" t-key="row.id"
|
<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-att-class="rowClass(row)"
|
||||||
t-on-click="() => onRowClick(row)">
|
t-on-click="() => onRowClick(row)">
|
||||||
<td>
|
<td t-att-class="lineNameClass(row)">
|
||||||
<span><t t-esc="row.label"/></span>
|
<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>
|
||||||
<td class="amount">
|
<td class="line_cell numeric">
|
||||||
<t t-esc="formatAmount(row.amount)"/>
|
<div class="wrapper"><div class="content">
|
||||||
|
<t t-esc="formatAmount(row.amount)"/>
|
||||||
|
</div></div>
|
||||||
</td>
|
</td>
|
||||||
<t t-if="props.result.comparison_period">
|
<t t-if="props.result.comparison_period">
|
||||||
<td class="amount">
|
<td class="line_cell numeric">
|
||||||
<t t-esc="formatAmount(row.amount_comparison)"/>
|
<div class="wrapper"><div class="content">
|
||||||
|
<t t-esc="formatAmount(row.comparison_amount)"/>
|
||||||
|
</div></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="amount" t-att-class="varianceClass(row.variance_pct)">
|
<td class="line_cell numeric">
|
||||||
<t t-if="row.variance_pct !== null and row.variance_pct !== undefined">
|
<div class="wrapper"><div class="content">
|
||||||
<t t-esc="row.variance_pct.toFixed(1)"/>%
|
<t t-esc="formatAmount(row.variance_amount)"/>
|
||||||
</t>
|
</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>
|
</td>
|
||||||
</t>
|
</t>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</t>
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -1,29 +1,15 @@
|
|||||||
// Fusion reports design tokens (extends Phase 1's bank_rec tokens for consistency).
|
// 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).
|
||||||
|
|
||||||
// Colors — semantic
|
// Spacing scale (4px increments)
|
||||||
$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-1: 0.25rem;
|
||||||
$report-space-2: 0.5rem;
|
$report-space-2: 0.5rem;
|
||||||
$report-space-3: 0.75rem;
|
$report-space-3: 0.75rem;
|
||||||
@@ -38,12 +24,7 @@ $report-font-size-sm: 0.875rem;
|
|||||||
$report-font-size-base: 1rem;
|
$report-font-size-base: 1rem;
|
||||||
$report-font-size-lg: 1.125rem;
|
$report-font-size-lg: 1.125rem;
|
||||||
$report-font-size-xl: 1.25rem;
|
$report-font-size-xl: 1.25rem;
|
||||||
$report-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
|
|
||||||
// Borders + radii
|
// Border radius
|
||||||
$report-border-radius: 0.375rem;
|
$report-border-radius: 0.25rem;
|
||||||
$report-border-radius-md: 0.5rem;
|
$report-border-radius-md: 0.5rem;
|
||||||
$report-border-radius-lg: 0.75rem;
|
|
||||||
|
|
||||||
// Subtotal indentation
|
|
||||||
$report-indent-per-level: 1.5rem;
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
// Variables come from _variables.scss via manifest concatenation order.
|
|
||||||
|
|
||||||
[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); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
fusion_accounting_reports/static/src/scss/reports.dark.scss
Normal file
80
fusion_accounting_reports/static/src/scss/reports.dark.scss
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,162 +1,344 @@
|
|||||||
// Variables come from _variables.scss via manifest concatenation order.
|
// Faithful adaptation of Enterprise account_reports' look.
|
||||||
// (V19 forbids cross-file SCSS imports; rely on bundle order instead.)
|
// 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.
|
||||||
|
|
||||||
.o_fusion_reports {
|
.account_report {
|
||||||
background: $report-bg-secondary;
|
//--------------------------------------------------------------------
|
||||||
min-height: 100vh;
|
// Header (Fusion-only -- Enterprise uses Odoo ControlPanel; we keep
|
||||||
|
// a lightweight header to host the report title + AI commentary
|
||||||
&_header {
|
// trigger but style it to feel native).
|
||||||
background: $report-bg-primary;
|
//--------------------------------------------------------------------
|
||||||
border-bottom: 1px solid $report-border;
|
.o_fusion_report_header {
|
||||||
padding: $report-space-4 $report-space-6;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
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 {
|
h1 {
|
||||||
font-size: $report-font-size-xl;
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: $o-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fusion_report_period {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $o-gray-600;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_table {
|
//--------------------------------------------------------------------
|
||||||
background: $report-bg-primary;
|
// Scroll container (mirrors Enterprise)
|
||||||
border: 1px solid $report-border;
|
//--------------------------------------------------------------------
|
||||||
border-radius: $report-border-radius-md;
|
.o_account_report_scroll_container {
|
||||||
margin: $report-space-4;
|
margin-inline: 0 !important;
|
||||||
overflow: hidden;
|
}
|
||||||
font-family: $report-font-mono;
|
|
||||||
font-size: $report-font-size-sm;
|
|
||||||
|
|
||||||
table {
|
//--------------------------------------------------------------------
|
||||||
width: 100%;
|
// Table (verbatim from Enterprise, minus chatter/annotation/audit)
|
||||||
border-collapse: collapse;
|
//--------------------------------------------------------------------
|
||||||
|
.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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
> tbody {
|
||||||
background: $report-bg-tertiary;
|
> tr {
|
||||||
padding: $report-space-3 $report-space-4;
|
&.unfolded { font-weight: bold }
|
||||||
text-align: left;
|
> td {
|
||||||
font-weight: 600;
|
a { cursor: pointer }
|
||||||
color: $report-text-secondary;
|
.clickable { color: $o-enterprise-action-color }
|
||||||
border-bottom: 1px solid $report-border;
|
&.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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th.amount, td.amount {
|
@media print {
|
||||||
text-align: right;
|
border: 0;
|
||||||
white-space: nowrap;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
.name { white-space: nowrap }
|
||||||
padding: $report-space-2 $report-space-4;
|
&.draft { color: $o-info; }
|
||||||
border-bottom: 1px solid lighten($report-border, 5%);
|
&.unfoldable:hover { cursor: pointer }
|
||||||
|
}
|
||||||
|
|
||||||
|
.line_cell {
|
||||||
|
vertical-align: middle;
|
||||||
|
> .wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.subtotal {
|
&.date > .wrapper { justify-content: center }
|
||||||
font-weight: 600;
|
&.numeric > .wrapper { justify-content: flex-end }
|
||||||
background: $report-bg-secondary;
|
.name { white-space: nowrap }
|
||||||
border-top: 1px solid $report-text-muted;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tr.subtotal td {
|
//--------------------------------------------------------------------
|
||||||
border-bottom: 1px solid $report-text-muted;
|
// Indentation per level
|
||||||
}
|
//--------------------------------------------------------------------
|
||||||
|
.line_level_0 {
|
||||||
|
color: $o-gray-700;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
tr.drillable {
|
> 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;
|
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 {
|
.btn_foldable { color: $o-gray-500 }
|
||||||
background: $report-bg-primary;
|
.btn_foldable_empty:hover { cursor: default }
|
||||||
padding: $report-space-3 $report-space-4;
|
.btn_more { opacity: 1 }
|
||||||
border-bottom: 1px solid $report-border;
|
.btn_action {
|
||||||
display: flex;
|
opacity: 0;
|
||||||
gap: $report-space-3;
|
background-color: $o-view-background-color;
|
||||||
align-items: center;
|
color: $o-gray-600;
|
||||||
flex-wrap: wrap;
|
width: auto;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
border: 1px solid $o-gray-300;
|
||||||
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn_report {
|
//--------------------------------------------------------------------
|
||||||
padding: $report-space-2 $report-space-4;
|
// Dropdown
|
||||||
border-radius: $report-border-radius;
|
//--------------------------------------------------------------------
|
||||||
background: $report-bg-primary;
|
.dropdown { display: inline }
|
||||||
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; }
|
//--------------------------------------------------------------------
|
||||||
|
// 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;
|
||||||
|
|
||||||
&.primary {
|
label {
|
||||||
background: $report-accent;
|
color: $o-gray-700;
|
||||||
border-color: $report-accent;
|
margin-bottom: 0;
|
||||||
color: white;
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover { background: darken($report-accent, 8%); }
|
.form-select, .form-control {
|
||||||
}
|
font-size: 0.8rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fusion_anomaly_strip {
|
//--------------------------------------------------------------------
|
||||||
margin: $report-space-3;
|
// AI commentary panel (Fusion-only addition)
|
||||||
padding: $report-space-3;
|
//--------------------------------------------------------------------
|
||||||
border-radius: $report-border-radius;
|
.account_report .o_fusion_commentary_panel {
|
||||||
border: 1px solid;
|
background-color: $o-view-background-color;
|
||||||
font-size: $report-font-size-sm;
|
border: 1px solid $o-gray-300;
|
||||||
|
border-radius: 0.25rem;
|
||||||
&[data-severity="high"] {
|
margin: 0 24px 24px;
|
||||||
background: $report-severity-high-bg;
|
padding: 1rem 1.25rem;
|
||||||
border-color: $report-severity-high;
|
font-size: 0.85rem;
|
||||||
}
|
|
||||||
&[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 {
|
h4 {
|
||||||
margin: 0 0 $report-space-3;
|
font-size: 0.95rem;
|
||||||
font-size: $report-font-size-base;
|
font-weight: 600;
|
||||||
color: $report-text-primary;
|
margin: 0 0 0.5rem;
|
||||||
|
color: $o-gray-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentary-section {
|
.commentary-section {
|
||||||
margin-bottom: $report-space-3;
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: $report-font-size-sm;
|
font-size: 0.75rem;
|
||||||
color: $report-text-secondary;
|
font-weight: 600;
|
||||||
|
color: $o-gray-600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
margin-bottom: $report-space-2;
|
margin: 0 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: $report-space-4;
|
padding-left: 1.25rem;
|
||||||
|
li { margin: 0.15rem 0 }
|
||||||
li { margin: $report-space-1 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,22 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="fusion_accounting_reports.ReportViewer">
|
<t t-name="fusion_accounting_reports.ReportViewer">
|
||||||
<div class="o_fusion_reports">
|
<div class="o_action account_report">
|
||||||
<div class="o_fusion_reports_header">
|
<div class="o_fusion_report_header">
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
<t t-esc="state.currentResult?.report_name || 'Financial Reports'"/>
|
<t t-esc="state.currentResult?.report_name || 'Financial Reports'"/>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-muted" t-if="state.currentResult">
|
<div class="o_fusion_report_period" t-if="state.currentResult">
|
||||||
<t t-esc="state.currentResult.period?.label"/>
|
<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>
|
</div>
|
||||||
<div>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn_report primary"
|
<span t-if="state.isLoading" class="text-muted small">Loading...</span>
|
||||||
|
<button class="btn btn-secondary btn-sm"
|
||||||
t-on-click="onGenerateCommentary"
|
t-on-click="onGenerateCommentary"
|
||||||
t-att-disabled="state.isGeneratingCommentary">
|
t-att-disabled="state.isGeneratingCommentary">
|
||||||
<t t-if="state.isGeneratingCommentary">Generating...</t>
|
<t t-if="state.isGeneratingCommentary">Generating...</t>
|
||||||
@@ -24,17 +28,21 @@
|
|||||||
|
|
||||||
<PeriodFilter />
|
<PeriodFilter />
|
||||||
|
|
||||||
<AnomalyStrip t-foreach="state.currentAnomalies" t-as="anomaly"
|
<div t-if="state.currentAnomalies and state.currentAnomalies.length" class="warnings d-print-none">
|
||||||
t-key="anomaly.row_id" anomaly="anomaly"/>
|
<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"/>
|
<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"
|
<DrillDownDialog t-if="state.drillDown and state.drillDown.isOpen"
|
||||||
drill="state.drillDown"
|
drill="state.drillDown"
|
||||||
onClose="onCloseDrill.bind(this)"/>
|
onClose="onCloseDrill.bind(this)"/>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
import { ReportViewer } from "./report_viewer";
|
import { ReportViewer } from "./report_viewer";
|
||||||
|
|
||||||
export const fusionReportsView = {
|
// Register as a CLIENT ACTION (not a view). View types in V19 require an
|
||||||
type: "fusion_reports",
|
// ir.ui.view record per type; client actions don't. The Fusion report
|
||||||
Controller: ReportViewer,
|
// viewer doesn't render records of a model \u2014 it's a custom dashboard \u2014
|
||||||
display_name: "Fusion Financial Reports",
|
// so client-action is the architecturally correct dispatch.
|
||||||
icon: "fa-line-chart",
|
//
|
||||||
multiRecord: true,
|
// 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("views").add("fusion_reports", fusionReportsView);
|
registry.category("actions").add("fusion_reports", ReportViewer);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
sitting alongside Community's PDF-based "Statement Reports" /
|
sitting alongside Community's PDF-based "Statement Reports" /
|
||||||
"Partner Reports" / "Taxes & Fiscal" / "Management" wrappers.
|
"Partner Reports" / "Taxes & Fiscal" / "Management" wrappers.
|
||||||
|
|
||||||
Each menu opens an act_window with view_mode='fusion_reports'
|
Each menu opens an ir.actions.client with tag='fusion_reports'
|
||||||
(the OWL ReportViewer). report_actions.xml defines the actions.
|
(the OWL ReportViewer). report_actions.xml defines the actions.
|
||||||
|
|
||||||
All gated to the coexistence group so they only appear when
|
All gated to the coexistence group so they only appear when
|
||||||
|
|||||||
@@ -1,44 +1,47 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!--
|
<!--
|
||||||
One ir.actions.act_window per built-in report, each opens the OWL
|
One ir.actions.client per built-in report. tag="fusion_reports"
|
||||||
ReportViewer (view_mode='fusion_reports'). The viewer reads
|
dispatches to the OWL ReportViewer (registered in
|
||||||
``default_report_type`` and ``default_report_code`` from the action
|
static/src/views/report_viewer/report_viewer_view.js).
|
||||||
context to pick which fusion.report to render.
|
|
||||||
|
|
||||||
New menus per-report live below; users no longer need to go through
|
The viewer reads ``default_report_type``, ``default_report_code``,
|
||||||
the period-picker wizard for the standard reports.
|
and ``default_comparison`` from the action context to pick which
|
||||||
|
fusion.report to render with which line_specs.
|
||||||
|
|
||||||
|
Why ir.actions.client and not ir.actions.act_window:
|
||||||
|
a custom OWL view type (view_mode='fusion_reports') would require
|
||||||
|
a placeholder ir.ui.view record per action plus a registered "view"
|
||||||
|
in the views registry; the standard act_window resolver also rejects
|
||||||
|
view_modes that have no corresponding ir.ui.view record. Client
|
||||||
|
actions are the V19-canonical pattern for record-less dashboards.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
CORE REPORTS (one per Fusion engine method)
|
CORE REPORTS (one per Fusion engine method)
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
|
|
||||||
<record id="action_fusion_report_pnl" model="ir.actions.act_window">
|
<record id="action_fusion_report_pnl" model="ir.actions.client">
|
||||||
<field name="name">Profit and Loss</field>
|
<field name="name">Profit and Loss</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'pnl'}</field>
|
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'pnl'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_balance_sheet" model="ir.actions.act_window">
|
<record id="action_fusion_report_balance_sheet" model="ir.actions.client">
|
||||||
<field name="name">Balance Sheet</field>
|
<field name="name">Balance Sheet</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'balance_sheet', 'default_report_code': 'balance_sheet'}</field>
|
<field name="context">{'default_report_type': 'balance_sheet', 'default_report_code': 'balance_sheet'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_trial_balance" model="ir.actions.act_window">
|
<record id="action_fusion_report_trial_balance" model="ir.actions.client">
|
||||||
<field name="name">Trial Balance</field>
|
<field name="name">Trial Balance</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'trial_balance'}</field>
|
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'trial_balance'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_general_ledger" model="ir.actions.act_window">
|
<record id="action_fusion_report_general_ledger" model="ir.actions.client">
|
||||||
<field name="name">General Ledger</field>
|
<field name="name">General Ledger</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'general_ledger', 'default_report_code': 'general_ledger'}</field>
|
<field name="context">{'default_report_type': 'general_ledger', 'default_report_code': 'general_ledger'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
@@ -46,31 +49,27 @@
|
|||||||
SECONDARY PnL VARIANTS (engine.compute_pnl with code)
|
SECONDARY PnL VARIANTS (engine.compute_pnl with code)
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
|
|
||||||
<record id="action_fusion_report_cash_flow" model="ir.actions.act_window">
|
<record id="action_fusion_report_cash_flow" model="ir.actions.client">
|
||||||
<field name="name">Cash Flow Statement</field>
|
<field name="name">Cash Flow Statement</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'cash_flow', 'default_comparison': 'previous_year'}</field>
|
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'cash_flow', 'default_comparison': 'previous_year'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_executive_summary" model="ir.actions.act_window">
|
<record id="action_fusion_report_executive_summary" model="ir.actions.client">
|
||||||
<field name="name">Executive Summary</field>
|
<field name="name">Executive Summary</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'executive_summary', 'default_comparison': 'previous_year'}</field>
|
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'executive_summary', 'default_comparison': 'previous_year'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_annual_statements" model="ir.actions.act_window">
|
<record id="action_fusion_report_annual_statements" model="ir.actions.client">
|
||||||
<field name="name">Annual Statements</field>
|
<field name="name">Annual Statements</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'annual_statements', 'default_comparison': 'previous_year'}</field>
|
<field name="context">{'default_report_type': 'pnl', 'default_report_code': 'annual_statements', 'default_comparison': 'previous_year'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_tax_summary" model="ir.actions.act_window">
|
<record id="action_fusion_report_tax_summary" model="ir.actions.client">
|
||||||
<field name="name">Tax Summary</field>
|
<field name="name">Tax Summary</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'tax_summary'}</field>
|
<field name="context">{'default_report_type': 'trial_balance', 'default_report_code': 'tax_summary'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
@@ -78,24 +77,21 @@
|
|||||||
PARTNER-GROUPED REPORTS
|
PARTNER-GROUPED REPORTS
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
|
|
||||||
<record id="action_fusion_report_aged_receivable" model="ir.actions.act_window">
|
<record id="action_fusion_report_aged_receivable" model="ir.actions.client">
|
||||||
<field name="name">Aged Receivable</field>
|
<field name="name">Aged Receivable</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'aged_receivable', 'default_report_code': 'aged_receivable'}</field>
|
<field name="context">{'default_report_type': 'aged_receivable', 'default_report_code': 'aged_receivable'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_aged_payable" model="ir.actions.act_window">
|
<record id="action_fusion_report_aged_payable" model="ir.actions.client">
|
||||||
<field name="name">Aged Payable</field>
|
<field name="name">Aged Payable</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'aged_payable', 'default_report_code': 'aged_payable'}</field>
|
<field name="context">{'default_report_type': 'aged_payable', 'default_report_code': 'aged_payable'}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_fusion_report_partner_ledger" model="ir.actions.act_window">
|
<record id="action_fusion_report_partner_ledger" model="ir.actions.client">
|
||||||
<field name="name">Partner Ledger</field>
|
<field name="name">Partner Ledger</field>
|
||||||
<field name="res_model">fusion.report</field>
|
<field name="tag">fusion_reports</field>
|
||||||
<field name="view_mode">fusion_reports</field>
|
|
||||||
<field name="context">{'default_report_type': 'partner_ledger', 'default_report_code': 'partner_ledger'}</field>
|
<field name="context">{'default_report_type': 'partner_ledger', 'default_report_code': 'partner_ledger'}</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Reference in New Issue
Block a user