feat(fusion_accounting_followup): followup_service.js reactive frontend service

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 21:17:57 -04:00
parent 99e4f8e17f
commit 86bead48e1
2 changed files with 147 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Follow-up',
'version': '19.0.1.0.19',
'version': '19.0.1.0.20',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
'description': """
@@ -40,6 +40,7 @@ menu hides; the engine + AI tools remain available for the chat.
'fusion_accounting_followup/static/src/scss/_variables.scss',
'fusion_accounting_followup/static/src/scss/followup.scss',
'fusion_accounting_followup/static/src/scss/dark_mode.scss',
'fusion_accounting_followup/static/src/services/followup_service.js',
],
},
'installable': True,

View File

@@ -0,0 +1,145 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { reactive } from "@odoo/owl";
const ENDPOINT_BASE = "/fusion/followup";
export class FollowupService {
constructor(env, services) {
this.env = env;
this.rpc = services.rpc;
this.notification = services.notification;
this.state = reactive({
partners: [],
count: 0,
total: 0,
statusFilter: null,
isLoading: false,
isProcessing: false,
selectedPartnerId: null,
selectedDetail: null,
companyId: null,
limit: 50,
offset: 0,
generatedText: null,
});
}
async loadOverdue(companyId = null) {
this.state.companyId = companyId;
this.state.isLoading = true;
try {
const result = await this.rpc(`${ENDPOINT_BASE}/list_overdue`, {
status: this.state.statusFilter,
limit: this.state.limit,
offset: this.state.offset,
company_id: companyId,
});
this.state.partners = result.partners;
this.state.count = result.count;
this.state.total = result.total;
} finally {
this.state.isLoading = false;
}
}
async selectPartner(partnerId) {
this.state.selectedPartnerId = partnerId;
this.state.selectedDetail = null;
this.state.generatedText = null;
try {
this.state.selectedDetail = await this.rpc(`${ENDPOINT_BASE}/get_partner_detail`, {
partner_id: partnerId,
});
} catch (err) {
this.notification.add(`Failed to load partner detail: ${err.message || err}`, { type: "danger" });
}
}
async generateText(partnerId, levelId = null, forceRegenerate = false) {
this.state.isProcessing = true;
try {
this.state.generatedText = await this.rpc(`${ENDPOINT_BASE}/generate_text`, {
partner_id: partnerId, level_id: levelId,
force_regenerate: forceRegenerate,
});
return this.state.generatedText;
} catch (err) {
this.notification.add(`Generate failed: ${err.message || err}`, { type: "danger" });
throw err;
} finally {
this.state.isProcessing = false;
}
}
async sendFollowup(partnerId, levelId = null, force = false) {
this.state.isProcessing = true;
try {
const result = await this.rpc(`${ENDPOINT_BASE}/send`, {
partner_id: partnerId, level_id: levelId, force: force,
});
const status = result.status || "unknown";
const type = status === "sent" ? "success" : status.startsWith("paused") ? "warning" : "info";
this.notification.add(`Send result: ${status}`, { type: type });
if (this.state.selectedPartnerId === partnerId) {
await this.selectPartner(partnerId);
}
await this.loadOverdue(this.state.companyId);
return result;
} catch (err) {
this.notification.add(`Send failed: ${err.message || err}`, { type: "danger" });
throw err;
} finally {
this.state.isProcessing = false;
}
}
async pausePartner(partnerId, untilDate = null) {
try {
const result = await this.rpc(`${ENDPOINT_BASE}/pause`, {
partner_id: partnerId, until_date: untilDate,
});
this.notification.add(`Paused until ${result.paused_until}`, { type: "info" });
if (this.state.selectedPartnerId === partnerId) {
await this.selectPartner(partnerId);
}
await this.loadOverdue(this.state.companyId);
return result;
} catch (err) {
this.notification.add(`Pause failed: ${err.message || err}`, { type: "danger" });
throw err;
}
}
async resetPartner(partnerId) {
try {
const result = await this.rpc(`${ENDPOINT_BASE}/reset`, {
partner_id: partnerId,
});
this.notification.add(`Reset`, { type: "info" });
if (this.state.selectedPartnerId === partnerId) {
await this.selectPartner(partnerId);
}
await this.loadOverdue(this.state.companyId);
return result;
} catch (err) {
this.notification.add(`Reset failed: ${err.message || err}`, { type: "danger" });
throw err;
}
}
setStatusFilter(status) {
this.state.statusFilter = status;
this.state.offset = 0;
this.loadOverdue(this.state.companyId);
}
}
export const followupService = {
dependencies: ["rpc", "notification"],
start(env, services) { return new FollowupService(env, services); },
};
registry.category("services").add("fusion_followup", followupService);