feat(fusion_accounting_bank_rec): fusion-only AI suggestion UI components

ai_suggestion_strip (inline confidence badge + accept), ai_alternatives_panel
(expandable other-options), ai_reasoning_tooltip (score breakdown). These
go beyond Enterprise's bank_rec_widget which has no AI suggestions.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 13:02:18 -04:00
parent c9ac4c64fb
commit 99e27cc566
7 changed files with 163 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting — Bank Reconciliation',
'version': '19.0.1.0.15',
'version': '19.0.1.0.16',
'category': 'Accounting/Accounting',
'sequence': 28,
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
@@ -80,6 +80,13 @@ Built by Nexa Systems Inc.
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/quick_create/quick_create.xml',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/chatter/chatter.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/file_uploader/file_uploader.js',
# Fusion-only (Task 34) — AI suggestion UI
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_suggestion_strip.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_suggestion_strip.xml',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_alternatives_panel.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_alternatives_panel.xml',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_reasoning_tooltip.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/ai_suggestion/ai_reasoning_tooltip.xml',
],
},
'installable': True,

View File

@@ -0,0 +1,34 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class AiAlternativesPanel extends Component {
static template = "fusion_accounting_bank_rec.AiAlternativesPanel";
static props = {
suggestions: { type: Array },
onClose: { type: Function, optional: true },
};
setup() {
this.bankRec = useService("fusion_bank_reconciliation");
}
bandFor(c) {
if (c >= 0.85) return "high";
if (c >= 0.6) return "medium";
if (c > 0) return "low";
return "none";
}
pctFor(c) {
return Math.round(c * 100);
}
async onAccept(suggestionId) {
await this.bankRec.acceptSuggestion(suggestionId);
if (this.props.onClose) {
this.props.onClose();
}
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_bank_rec.AiAlternativesPanel">
<div class="o_fusion_alternatives_panel">
<h6>Other AI suggestions</h6>
<div t-foreach="props.suggestions" t-as="sug" t-key="sug.id"
class="o_fusion_alternative">
<div>
<span class="alt_confidence" t-att-class="'band-' + bandFor(sug.confidence)">
<t t-esc="pctFor(sug.confidence)"/>%
</span>
<t t-esc="sug.reasoning"/>
</div>
<button class="btn_fusion" t-on-click="() => onAccept(sug.id)">
Use this
</button>
</div>
<div t-if="props.onClose" class="text-end mt-2">
<button class="btn_fusion" t-on-click="props.onClose">Close</button>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,18 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
export class AiReasoningTooltip extends Component {
static template = "fusion_accounting_bank_rec.AiReasoningTooltip";
static props = {
scores: { type: Object },
reasoning: { type: String, optional: true },
};
pctFor(value) {
if (value === undefined || value === null) {
return "0";
}
return (value * 100).toFixed(0);
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_bank_rec.AiReasoningTooltip">
<div class="o_fusion_reasoning_tooltip" style="font-size: 0.85em; padding: 0.5rem;">
<div t-if="props.reasoning" class="mb-2">
<em><t t-esc="props.reasoning"/></em>
</div>
<div class="text-muted">
<div>Amount match: <t t-esc="pctFor(props.scores.amount_match)"/>%</div>
<div>Partner pattern: <t t-esc="pctFor(props.scores.partner_pattern)"/>%</div>
<div>Precedent similarity: <t t-esc="pctFor(props.scores.precedent_similarity)"/>%</div>
<div t-if="props.scores.ai_rerank">
AI re-rank: <t t-esc="pctFor(props.scores.ai_rerank)"/>%
</div>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,38 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class AiSuggestionStrip extends Component {
static template = "fusion_accounting_bank_rec.AiSuggestionStrip";
static props = {
suggestion: { type: Object },
showAlternatives: { type: Function, optional: true },
};
setup() {
this.bankRec = useService("fusion_bank_reconciliation");
}
get band() {
const c = this.props.suggestion.confidence;
if (c >= 0.85) return "high";
if (c >= 0.6) return "medium";
if (c > 0) return "low";
return "none";
}
get confidencePct() {
return Math.round(this.props.suggestion.confidence * 100);
}
async onAccept() {
await this.bankRec.acceptSuggestion(this.props.suggestion.id);
}
onShowAlternatives() {
if (this.props.showAlternatives) {
this.props.showAlternatives();
}
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_bank_rec.AiSuggestionStrip">
<div class="o_fusion_ai_suggestion" t-att-data-band="band">
<div class="o_fusion_confidence_badge">
<t t-esc="confidencePct"/>%
</div>
<div class="o_fusion_suggestion_text">
<div class="o_fusion_reasoning">
<t t-esc="props.suggestion.reasoning || 'AI suggested match'"/>
</div>
</div>
<div class="o_fusion_suggestion_actions">
<button class="btn_fusion btn_fusion_primary" t-on-click="onAccept">
Accept
</button>
<button t-if="props.showAlternatives" class="btn_fusion"
t-on-click="onShowAlternatives">
Other options
</button>
</div>
</div>
</t>
</templates>