feat(fusion_accounting_bank_rec): fusion-only attachment strip + partner history panel

attachment_strip renders inline mimetype-aware chips linking to /web/content
downloads. partner_history_panel calls bank_reconciliation.getPartnerHistory
to surface the learned reconcile pattern (preferred strategy, typical cadence)
plus the most recent reconciles per partner — context Enterprise's bank-rec
widget cannot show because it has no behavioural-learning layer.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 13:05:23 -04:00
parent 596ecb9e03
commit 6cbb5f85fe
5 changed files with 124 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting — Bank Reconciliation',
'version': '19.0.1.0.17',
'version': '19.0.1.0.18',
'category': 'Accounting/Accounting',
'sequence': 28,
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
@@ -92,6 +92,11 @@ Built by Nexa Systems Inc.
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/batch_action_bar/batch_action_bar.xml',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconcile_model_picker/reconcile_model_picker.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconcile_model_picker/reconcile_model_picker.xml',
# Fusion-only (Task 36) — attachment strip + partner history panel
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/attachment_strip/attachment_strip.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/attachment_strip/attachment_strip.xml',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/partner_history_panel/partner_history_panel.js',
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/partner_history_panel/partner_history_panel.xml',
],
},
'installable': True,

View File

@@ -0,0 +1,27 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
export class AttachmentStrip extends Component {
static template = "fusion_accounting_bank_rec.AttachmentStrip";
static props = {
attachments: { type: Array },
};
iconFor(mimetype) {
if (!mimetype) {
return "fa-file";
}
if (mimetype.startsWith("image/")) {
return "fa-file-image-o";
}
if (mimetype === "application/pdf") {
return "fa-file-pdf-o";
}
return "fa-file-o";
}
urlFor(att) {
return `/web/content/${att.id}?download=true`;
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_bank_rec.AttachmentStrip">
<div class="o_fusion_attachment_strip d-flex flex-wrap"
style="gap: 0.5rem; padding: 0.5rem;">
<div t-if="props.attachments.length === 0" class="text-muted small">
No attachments
</div>
<a t-foreach="props.attachments" t-as="att" t-key="att.id"
t-att-href="urlFor(att)" target="_blank"
class="o_fusion_attachment_chip"
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.5rem; background: #f3f4f6; border-radius: 0.25rem; text-decoration: none; color: inherit; font-size: 0.85em;">
<i class="fa" t-att-class="iconFor(att.mimetype)"/>
<span><t t-esc="att.name"/></span>
</a>
</div>
</t>
</templates>

View File

@@ -0,0 +1,34 @@
/** @odoo-module **/
import { Component, onWillStart, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class PartnerHistoryPanel extends Component {
static template = "fusion_accounting_bank_rec.PartnerHistoryPanel";
static props = {
partnerId: { type: Number },
};
setup() {
this.bankRec = useService("fusion_bank_reconciliation");
this.state = useState({ history: null, loading: true });
onWillStart(async () => {
try {
this.state.history = await this.bankRec.getPartnerHistory(
this.props.partnerId,
20,
);
} finally {
this.state.loading = false;
}
});
}
formatAmount(value) {
if (value === undefined || value === null) {
return "0.00";
}
return Number(value).toFixed(2);
}
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting_bank_rec.PartnerHistoryPanel">
<div class="o_fusion_partner_history_panel" style="padding: 1rem; border-left: 1px solid #e5e7eb;">
<h5 t-if="state.history">
<t t-esc="state.history.partner.name"/> — History
</h5>
<div t-if="state.loading" class="text-muted">Loading…</div>
<div t-elif="state.history">
<div t-if="state.history.pattern" class="mb-3 p-2"
style="background: #eff6ff; border-radius: 0.25rem; font-size: 0.85em;">
<strong>Learned pattern:</strong>
<div>Reconciles: <t t-esc="state.history.pattern.reconcile_count"/></div>
<div t-if="state.history.pattern.pref_strategy">
Preferred strategy: <t t-esc="state.history.pattern.pref_strategy"/>
</div>
<div t-if="state.history.pattern.typical_cadence_days">
Typical cadence: ~<t t-esc="state.history.pattern.typical_cadence_days"/> days
</div>
</div>
<h6>Recent reconciles</h6>
<div t-foreach="state.history.recent_reconciles" t-as="rec" t-key="rec.precedent_id"
style="padding: 0.5rem 0; border-bottom: 1px solid #e5e7eb; font-size: 0.85em;">
<div class="d-flex justify-content-between">
<span><t t-esc="rec.date"/></span>
<span><strong>$<t t-esc="formatAmount(rec.amount)"/></strong></span>
</div>
<div class="text-muted">
<t t-if="rec.memo_tokens"><t t-esc="rec.memo_tokens"/></t>
<span class="ms-2">(<t t-esc="rec.matched_count"/> line<t t-if="rec.matched_count !== 1">s</t>)</span>
</div>
</div>
<div t-if="state.history.recent_reconciles.length === 0" class="text-muted">
No history yet
</div>
</div>
</div>
</t>
</templates>