refactor(fusion_accounting): move AI module code into fusion_accounting_ai sub-module

git mv preserves history. fusion_accounting/ retains only __manifest__.py,
__init__.py, CLAUDE.md, and docs/ — the meta-module shell. All Python,
data, views, security, services, static, tests, wizards, report move to
fusion_accounting_ai/. Manifest data list updated; security.xml move to
_core deferred to Task 12.

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-18 21:45:06 -04:00
parent b7483d5177
commit 6c72f2ab49
74 changed files with 76 additions and 60 deletions

View File

@@ -0,0 +1,108 @@
/** @odoo-module **/
import { Component, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { rpc } from "@web/core/network/rpc";
import { FusionHealthCard } from "./health_card";
import { FusionChatPanel } from "../chat/chat_panel";
export class FusionDashboard extends Component {
static template = "fusion_accounting.Dashboard";
static components = { FusionHealthCard, FusionChatPanel };
static props = ["*"];
setup() {
this.action = useService("action");
this.state = useState({
data: null,
loading: true,
chatSessionId: null,
});
onWillStart(async () => {
await this.loadDashboard();
});
}
async loadDashboard() {
this.state.loading = true;
try {
this.state.data = await rpc("/fusion_accounting/dashboard/data");
} catch (e) {
console.error("Dashboard load error:", e);
this.state.data = null;
}
this.state.loading = false;
}
async onAttentionClick(domain, prompt) {
// Type the prompt into the chat input and send
if (!prompt) return;
const textarea = this.el?.querySelector('.fusion_chat_input textarea');
if (textarea) {
// Set value and trigger OWL's model update via input event
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype, 'value'
).set;
nativeInputValueSetter.call(textarea, prompt);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
// Click send button
setTimeout(() => {
const sendBtn = this.el?.querySelector('.fusion_chat_input .btn-primary');
if (sendBtn) sendBtn.click();
}, 50);
}
}
get cards() {
if (!this.state.data) return [];
const d = this.state.data;
return [
{
title: "Bank Reconciliation",
metric: `${d.bank_recon.count} unmatched`,
subtext: `$${(d.bank_recon.amount || 0).toLocaleString('en-CA', {minimumFractionDigits: 0})} total`,
domain: "bank_reconciliation",
status: d.bank_recon.count === 0 ? "green" : d.bank_recon.count < 10 ? "yellow" : "red",
},
{
title: "AR Outstanding",
metric: `$${Math.abs(d.ar.total || 0).toLocaleString('en-CA', {minimumFractionDigits: 0})}`,
subtext: `${d.ar.overdue_count} overdue`,
domain: "accounts_receivable",
status: d.ar.overdue_count === 0 ? "green" : d.ar.overdue_count < 5 ? "yellow" : "red",
},
{
title: "AP Due",
metric: `$${(d.ap.total || 0).toLocaleString('en-CA', {minimumFractionDigits: 0})}`,
subtext: `${d.ap.due_this_week} due this week`,
domain: "accounts_payable",
status: d.ap.due_this_week === 0 ? "green" : "yellow",
},
{
title: "HST Balance",
metric: `$${(d.hst.balance || 0).toLocaleString('en-CA', {minimumFractionDigits: 0})}`,
subtext: d.hst.balance > 0 ? "Owing to CRA" : "Refund expected",
domain: "hst_management",
status: "blue",
},
{
title: "Audit Score",
metric: `${d.audit.score}/100`,
subtext: `${d.audit.flags} flags`,
domain: "audit",
status: d.audit.score >= 80 ? "green" : d.audit.score >= 60 ? "yellow" : "red",
},
{
title: "Month-End",
metric: d.month_end.status,
subtext: `${d.month_end.open_items} open items`,
domain: "month_end",
status: d.month_end.open_items === 0 ? "green" : "yellow",
},
];
}
}
registry.category("actions").add("fusion_accounting.dashboard", FusionDashboard);

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting.Dashboard">
<div class="o_action fusion_accounting_dashboard">
<div class="fusion_dashboard_header d-flex justify-content-between align-items-center px-3 py-2">
<h4 class="mb-0"><i class="fa fa-bolt me-2"/>Fusion AI</h4>
<button class="btn btn-outline-secondary btn-sm" t-on-click="loadDashboard">
<i class="fa fa-refresh me-1"/>Refresh
</button>
</div>
<t t-if="state.loading">
<div class="text-center p-5">
<i class="fa fa-spinner fa-spin fa-2x"/>
<p class="mt-2 text-muted">Loading dashboard...</p>
</div>
</t>
<t t-else="">
<div class="fusion_main_layout d-flex">
<!-- LEFT: Cards + Needs Attention -->
<div class="fusion_left_panel d-flex flex-column p-3 gap-3">
<div class="fusion_health_cards">
<t t-foreach="cards" t-as="card" t-key="card.domain">
<FusionHealthCard
title="card.title"
metric="card.metric"
subtext="card.subtext"
status="card.status"
domain="card.domain"
onCardClick.bind="onAttentionClick"/>
</t>
</div>
<!-- Needs Attention -->
<div class="fusion_attention_panel flex-grow-1 d-flex flex-column">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="fa fa-bell text-warning"/>
<span class="fw-semibold small">Needs Attention</span>
<t t-if="state.data and state.data.needs_attention">
<span class="badge bg-warning text-dark" t-esc="state.data.needs_attention.length"/>
</t>
</div>
<div class="fusion_attention_list flex-grow-1 overflow-auto">
<t t-if="state.data and state.data.needs_attention and state.data.needs_attention.length">
<t t-foreach="state.data.needs_attention" t-as="item" t-key="item_index">
<div class="fusion_attention_item d-flex align-items-start gap-2 p-2 rounded cursor-pointer"
t-on-click="() => this.onAttentionClick(item.domain, item.prompt)">
<div t-attf-class="fusion_attn_dot fusion_attn_{{item.severity || 'warning'}}"/>
<div class="flex-grow-1 small">
<div class="fw-semibold" t-esc="item.title"/>
<div class="text-muted" style="font-size: 0.75rem;" t-esc="item.action"/>
</div>
<i class="fa fa-chevron-right text-muted mt-1" style="font-size: 0.6rem;"/>
</div>
</t>
</t>
<t t-else="">
<div class="text-center text-muted small py-3">
<i class="fa fa-check-circle fa-2x mb-2 d-block text-success"/>
All clear! No items need attention.
</div>
</t>
</div>
</div>
</div>
<!-- RIGHT: Chat -->
<div class="fusion_right_panel">
<FusionChatPanel sessionId="state.chatSessionId"/>
</div>
</div>
</t>
</div>
</t>
</templates>

View File

@@ -0,0 +1,24 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
export class FusionHealthCard extends Component {
static template = "fusion_accounting.HealthCard";
static props = ["title", "metric", "subtext", "status", "domain", "onCardClick"];
get icon() {
const icons = {
bank_reconciliation: "fa-bank",
accounts_receivable: "fa-file-text-o",
accounts_payable: "fa-credit-card",
hst_management: "fa-percent",
audit: "fa-shield",
month_end: "fa-calendar-check-o",
};
return icons[this.props.domain] || "fa-bar-chart";
}
onClick() {
this.props.onCardClick(this.props.domain);
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_accounting.HealthCard">
<div class="fusion_health_card cursor-pointer"
t-attf-class="fusion_card_{{props.status}}"
t-on-click="onClick">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="fusion_card_icon">
<i t-attf-class="fa {{icon}}"/>
</div>
<span class="fusion_card_title" t-esc="props.title"/>
</div>
<div class="fusion_card_metric" t-esc="props.metric"/>
<div class="fusion_card_sub" t-esc="props.subtext"/>
</div>
</t>
</templates>