feat(fusion_accounting_followup): followup_service.js reactive frontend service
Made-with: Cursor
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user