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',
|
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||||
'version': '19.0.1.0.10',
|
'version': '19.0.1.0.11',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'sequence': 28,
|
'sequence': 28,
|
||||||
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
'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/ai_suggestion.scss',
|
||||||
'fusion_accounting_bank_rec/static/src/scss/dark_mode.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/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,
|
'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