Compare commits
3 Commits
c9ac4c64fb
...
6cbb5f85fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cbb5f85fe | ||
|
|
596ecb9e03 | ||
|
|
99e27cc566 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting — Bank Reconciliation',
|
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||||
'version': '19.0.1.0.15',
|
'version': '19.0.1.0.18',
|
||||||
'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.',
|
||||||
@@ -80,6 +80,23 @@ 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/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/chatter/chatter.js',
|
||||||
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/file_uploader/file_uploader.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',
|
||||||
|
# Fusion-only (Task 35) — batch action bar + reconcile model picker
|
||||||
|
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/batch_action_bar/batch_action_bar.js',
|
||||||
|
'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,
|
'installable': True,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class BatchActionBar extends Component {
|
||||||
|
static template = "fusion_accounting_bank_rec.BatchActionBar";
|
||||||
|
static props = {
|
||||||
|
selectedIds: { type: Array, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.bankRec = useService("fusion_bank_reconciliation");
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSelection() {
|
||||||
|
return this.props.selectedIds && this.props.selectedIds.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectionCount() {
|
||||||
|
return this.props.selectedIds ? this.props.selectedIds.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onAutoReconcile() {
|
||||||
|
if (!this.hasSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.bankRec.bulkReconcile(this.props.selectedIds, "auto");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSuggestForSelected() {
|
||||||
|
if (!this.hasSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.bankRec.suggestMatches(this.props.selectedIds, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BatchActionBar">
|
||||||
|
<div class="o_fusion_batch_action_bar d-flex"
|
||||||
|
style="gap: 0.5rem; padding: 0.75rem; background: #f3f4f6; border-radius: 0.375rem;">
|
||||||
|
<span class="text-muted">
|
||||||
|
<t t-esc="selectionCount"/> selected
|
||||||
|
</span>
|
||||||
|
<button class="btn_fusion" t-att-disabled="!hasSelection" t-on-click="onSuggestForSelected">
|
||||||
|
Suggest for selected
|
||||||
|
</button>
|
||||||
|
<button class="btn_fusion btn_fusion_primary" t-att-disabled="!hasSelection" t-on-click="onAutoReconcile">
|
||||||
|
Auto-reconcile selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class ReconcileModelPicker extends Component {
|
||||||
|
static template = "fusion_accounting_bank_rec.ReconcileModelPicker";
|
||||||
|
static props = {
|
||||||
|
statementLineId: { type: Number, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.bankRec = useService("fusion_bank_reconciliation");
|
||||||
|
this.state = useState({ models: [], selected: null });
|
||||||
|
|
||||||
|
onWillStart(async () => {
|
||||||
|
const models = await this.orm.searchRead(
|
||||||
|
"account.reconcile.model",
|
||||||
|
[["rule_type", "=", "writeoff_button"]],
|
||||||
|
["id", "name", "fusion_ai_confidence_threshold"],
|
||||||
|
{ limit: 20 }
|
||||||
|
);
|
||||||
|
this.state.models = models;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(ev) {
|
||||||
|
const value = parseInt(ev.target.value, 10);
|
||||||
|
if (Number.isFinite(value)) {
|
||||||
|
this.onApplyModel(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onApplyModel(modelId) {
|
||||||
|
// Phase 1 placeholder: TODO route through dedicated endpoint when Task 38 lands
|
||||||
|
this.state.selected = modelId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="fusion_accounting_bank_rec.ReconcileModelPicker">
|
||||||
|
<div class="o_fusion_reconcile_model_picker">
|
||||||
|
<select class="form-select" style="max-width: 240px;"
|
||||||
|
t-on-change="onChange">
|
||||||
|
<option value="">— Apply reconcile model —</option>
|
||||||
|
<option t-foreach="state.models" t-as="m" t-key="m.id" t-att-value="m.id">
|
||||||
|
<t t-esc="m.name"/>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
Reference in New Issue
Block a user