diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index da7aa950..fae8c64b 100644 --- a/fusion_accounting_followup/__manifest__.py +++ b/fusion_accounting_followup/__manifest__.py @@ -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, diff --git a/fusion_accounting_followup/static/src/services/followup_service.js b/fusion_accounting_followup/static/src/services/followup_service.js new file mode 100644 index 00000000..03d3c659 --- /dev/null +++ b/fusion_accounting_followup/static/src/services/followup_service.js @@ -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);