feat(fusion_accounting_bank_rec): kanban controller + renderer for OWL widget
Top-level OWL component (BankRecKanbanController) hosts the bank reconciliation widget. Reads journal_id + company_id from action context, initializes the fusion_bank_reconciliation service, and renders the layout: header (stats), left column (line cards via BankRecLineCard renderer), right column (detail panel with AI suggestions). Custom view type 'fusion_bank_rec_kanban' registered so window actions can use <field name="view_mode">fusion_bank_rec_kanban</field>. Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||
'version': '19.0.1.0.10',
|
||||
'version': '19.0.1.0.11',
|
||||
'category': 'Accounting/Accounting',
|
||||
'sequence': 28,
|
||||
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
||||
@@ -39,6 +39,10 @@ Built by Nexa Systems Inc.
|
||||
'fusion_accounting_bank_rec/static/src/scss/ai_suggestion.scss',
|
||||
'fusion_accounting_bank_rec/static/src/scss/dark_mode.scss',
|
||||
'fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js',
|
||||
'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban_controller.js',
|
||||
'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban_renderer.js',
|
||||
'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban_view.js',
|
||||
'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_bank_rec.BankRecKanbanController">
|
||||
<div class="o_fusion_bank_rec">
|
||||
<div class="o_fusion_bank_rec_header">
|
||||
<div>
|
||||
<h1>Bank Reconciliation</h1>
|
||||
<div t-if="state.journalId" class="text-muted">
|
||||
Journal #<t t-esc="state.journalId"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fusion_stats">
|
||||
<div>
|
||||
Unreconciled:
|
||||
<span class="stat-value"><t t-esc="state.unreconciledCount"/></span>
|
||||
</div>
|
||||
<div>
|
||||
Total pending:
|
||||
<span class="stat-value">
|
||||
$<t t-esc="formatCurrency(state.totalPendingAmount)"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" style="gap: 1rem; padding: 1rem;">
|
||||
<div style="flex: 1 1 60%; max-width: 60%;">
|
||||
<div t-if="state.isLoading" class="text-center p-4 text-muted">
|
||||
Loading…
|
||||
</div>
|
||||
<div t-elif="state.lines.length === 0" class="text-center p-4 text-muted">
|
||||
Nothing to reconcile.
|
||||
</div>
|
||||
<div t-else="">
|
||||
<BankRecLineCard
|
||||
t-foreach="state.lines"
|
||||
t-as="line"
|
||||
t-key="line.id"
|
||||
line="line"
|
||||
selected="state.selectedLineId === line.id"
|
||||
onSelect="() => onSelectLine(line.id)"
|
||||
formatCurrency="formatCurrency.bind(this)"
|
||||
/>
|
||||
<div t-if="state.lines.length lt state.unreconciledCount"
|
||||
class="text-center mt-3">
|
||||
<button class="btn_fusion" t-on-click="onLoadMore">
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1 1 40%; max-width: 40%;" class="o_fusion_bank_rec_detail">
|
||||
<t t-if="state.selectedLineId">
|
||||
<t t-set="detail" t-value="state.lineCache[state.selectedLineId]"/>
|
||||
<div t-if="!detail" class="text-muted">Loading detail…</div>
|
||||
<div t-else="">
|
||||
<h2>
|
||||
<t t-esc="detail.line.payment_ref || 'No reference'"/>
|
||||
</h2>
|
||||
<div class="text-muted mb-3">
|
||||
<span><t t-esc="detail.line.date"/></span>
|
||||
<span class="ms-2">
|
||||
$<t t-esc="formatCurrency(detail.line.amount)"/>
|
||||
</span>
|
||||
<span t-if="detail.line.partner_name" class="ms-2">
|
||||
· <t t-esc="detail.line.partner_name"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div t-if="detail.suggestions.length === 0">
|
||||
<button class="btn_fusion btn_fusion_primary"
|
||||
t-on-click="() => onSuggestForLine(detail.line.id)">
|
||||
Get AI suggestions
|
||||
</button>
|
||||
</div>
|
||||
<div t-else="">
|
||||
<h5>AI Suggestions</h5>
|
||||
<div t-foreach="detail.suggestions" t-as="sug" t-key="sug.id"
|
||||
class="o_fusion_ai_suggestion"
|
||||
t-att-data-band="confidenceBandLabel(sug.confidence >= 0.85 ? 'high' : sug.confidence >= 0.6 ? 'medium' : sug.confidence > 0 ? 'low' : 'none').toLowerCase()">
|
||||
<div class="o_fusion_confidence_badge">
|
||||
<t t-esc="(sug.confidence * 100).toFixed(0)"/>%
|
||||
</div>
|
||||
<div class="o_fusion_suggestion_text">
|
||||
<div><t t-esc="sug.reasoning"/></div>
|
||||
</div>
|
||||
<div class="o_fusion_suggestion_actions">
|
||||
<button class="btn_fusion btn_fusion_primary"
|
||||
t-on-click="() => onAcceptSuggestion(sug.id)">
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="text-muted">
|
||||
Select a bank line on the left to see details.
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="fusion_accounting_bank_rec.BankRecLineCard">
|
||||
<div class="o_fusion_bank_rec_line"
|
||||
t-att-class="props.selected ? 'o_fusion_selected' : ''"
|
||||
t-on-click="props.onSelect">
|
||||
<div class="o_fusion_bank_rec_line_header">
|
||||
<div class="o_fusion_amount" t-att-class="props.line.amount lt 0 ? 'negative' : ''">
|
||||
$<t t-esc="props.formatCurrency(props.line.amount)"/>
|
||||
</div>
|
||||
<div class="o_fusion_date">
|
||||
<t t-esc="props.line.date"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fusion_bank_rec_line_body">
|
||||
<span t-if="props.line.partner_name" class="o_fusion_partner">
|
||||
<t t-esc="props.line.partner_name"/>
|
||||
</span>
|
||||
<span class="o_fusion_memo">
|
||||
<t t-esc="props.line.payment_ref || 'No memo'"/>
|
||||
</span>
|
||||
</div>
|
||||
<div t-if="props.line.attachment_count" class="o_fusion_attachments_badge">
|
||||
📎 <t t-esc="props.line.attachment_count"/>
|
||||
</div>
|
||||
<div t-if="props.line.fusion_confidence_band and props.line.fusion_confidence_band !== 'none'"
|
||||
t-att-class="'o_fusion_ai_suggestion ' + 'band-' + props.line.fusion_confidence_band"
|
||||
t-att-data-band="props.line.fusion_confidence_band">
|
||||
<div class="o_fusion_confidence_badge">
|
||||
AI Suggestion Available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,81 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Bank reconciliation kanban controller.
|
||||
*
|
||||
* Top-level OWL component for the fusion bank-rec widget. Hosts:
|
||||
* - Header bar (journal name, unreconciled count, total pending amount)
|
||||
* - Left column: list of unreconciled bank line cards
|
||||
* - Right column: detail panel for the selected line
|
||||
*
|
||||
* Reads journal_id + company_id from action context. Wires up the
|
||||
* fusion_bank_reconciliation service for all data + reactivity.
|
||||
*/
|
||||
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { BankRecLineCard } from "./bank_rec_kanban_renderer";
|
||||
|
||||
export class BankRecKanbanController extends Component {
|
||||
static template = "fusion_accounting_bank_rec.BankRecKanbanController";
|
||||
static components = { BankRecLineCard };
|
||||
static props = {
|
||||
action: { type: Object, optional: true },
|
||||
actionId: { type: [Number, String], optional: true },
|
||||
className: { type: String, optional: true },
|
||||
"*": true,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.bankRec = useService("fusion_bank_reconciliation");
|
||||
this.notification = useService("notification");
|
||||
this.state = useState(this.bankRec.state);
|
||||
|
||||
const ctx = this.props.action?.context || {};
|
||||
const journalId = ctx.default_journal_id || ctx.active_id;
|
||||
const companyId = ctx.allowed_company_ids?.[0]
|
||||
|| this.env.services.user?.context?.allowed_company_ids?.[0];
|
||||
|
||||
onWillStart(async () => {
|
||||
if (journalId && companyId) {
|
||||
await this.bankRec.initForJournal(journalId, companyId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSelectLine(lineId) {
|
||||
this.bankRec.selectLine(lineId);
|
||||
}
|
||||
|
||||
async onLoadMore() {
|
||||
await this.bankRec.loadMore();
|
||||
}
|
||||
|
||||
async onSuggestForLine(lineId) {
|
||||
await this.bankRec.suggestMatches([lineId]);
|
||||
}
|
||||
|
||||
async onAcceptSuggestion(suggestionId) {
|
||||
await this.bankRec.acceptSuggestion(suggestionId);
|
||||
}
|
||||
|
||||
async onUnreconcile(partialIds) {
|
||||
await this.bankRec.unreconcile(partialIds);
|
||||
}
|
||||
|
||||
formatCurrency(amount) {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
confidenceBandLabel(band) {
|
||||
return {
|
||||
high: "High",
|
||||
medium: "Medium",
|
||||
low: "Low",
|
||||
none: "None",
|
||||
}[band] || "—";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Bank reconciliation line-card renderer.
|
||||
*
|
||||
* Renders one unreconciled bank line as a card in the kanban list.
|
||||
* Owned by BankRecKanbanController; receives line + selected flag as props.
|
||||
*/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class BankRecLineCard extends Component {
|
||||
static template = "fusion_accounting_bank_rec.BankRecLineCard";
|
||||
static props = {
|
||||
line: { type: Object },
|
||||
selected: { type: Boolean, optional: true },
|
||||
onSelect: { type: Function },
|
||||
formatCurrency: { type: Function },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Custom view type "fusion_bank_rec_kanban" — registers the controller
|
||||
* with the views registry so window actions can specify
|
||||
* <field name="view_mode">fusion_bank_rec_kanban</field>.
|
||||
*/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BankRecKanbanController } from "./bank_rec_kanban_controller";
|
||||
|
||||
export const fusionBankRecKanbanView = {
|
||||
type: "fusion_bank_rec_kanban",
|
||||
Controller: BankRecKanbanController,
|
||||
display_name: "Bank Reconciliation",
|
||||
icon: "fa-exchange",
|
||||
multiRecord: true,
|
||||
};
|
||||
|
||||
registry.category("views").add("fusion_bank_rec_kanban", fusionBankRecKanbanView);
|
||||
Reference in New Issue
Block a user