From 1ffa86b5322039e2913638d4ece6f0ffcd5462a3 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 16:00:29 -0400 Subject: [PATCH] feat(fusion_accounting_reports): reports_service.js reactive frontend service Made-with: Cursor --- fusion_accounting_reports/__manifest__.py | 3 +- .../static/src/services/reports_service.js | 147 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_reports/static/src/services/reports_service.js diff --git a/fusion_accounting_reports/__manifest__.py b/fusion_accounting_reports/__manifest__.py index 58593bdf..f1a2ba28 100644 --- a/fusion_accounting_reports/__manifest__.py +++ b/fusion_accounting_reports/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Reports', - 'version': '19.0.1.0.22', + 'version': '19.0.1.0.23', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).', 'description': """ @@ -42,6 +42,7 @@ menu hides; the engine and AI tools remain available for the chat. 'fusion_accounting_reports/static/src/scss/_variables.scss', 'fusion_accounting_reports/static/src/scss/reports.scss', 'fusion_accounting_reports/static/src/scss/dark_mode.scss', + 'fusion_accounting_reports/static/src/services/reports_service.js', ], }, 'installable': True, diff --git a/fusion_accounting_reports/static/src/services/reports_service.js b/fusion_accounting_reports/static/src/services/reports_service.js new file mode 100644 index 00000000..2b07bd4d --- /dev/null +++ b/fusion_accounting_reports/static/src/services/reports_service.js @@ -0,0 +1,147 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const ENDPOINT_BASE = "/fusion/reports"; + +export class ReportsService { + constructor(env, services) { + this.env = env; + this.rpc = services.rpc; + this.notification = services.notification; + + this.state = reactive({ + availableReports: [], + currentReportType: null, + currentResult: null, + currentAnomalies: [], + currentCommentary: null, + isLoading: false, + isGeneratingCommentary: false, + dateFrom: null, + dateTo: null, + comparison: 'none', + companyId: null, + drillDown: null, + }); + } + + async loadAvailableReports(companyId = null) { + this.state.companyId = companyId; + this.state.isLoading = true; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/list_available`, + { company_id: companyId }); + this.state.availableReports = result.reports; + } finally { + this.state.isLoading = false; + } + } + + async runReport(reportType, dateFrom, dateTo, comparison = 'none') { + this.state.isLoading = true; + this.state.currentReportType = reportType; + this.state.dateFrom = dateFrom; + this.state.dateTo = dateTo; + this.state.comparison = comparison; + try { + this.state.currentResult = await this.rpc(`${ENDPOINT_BASE}/run`, { + report_type: reportType, + date_from: dateFrom, + date_to: dateTo, + comparison: comparison, + company_id: this.state.companyId, + }); + if (comparison && comparison !== 'none') { + this.fetchAnomalies(); + } else { + this.state.currentAnomalies = []; + } + this.state.currentCommentary = null; + return this.state.currentResult; + } catch (err) { + this.notification.add(`Run failed: ${err.message || err}`, { type: 'danger' }); + throw err; + } finally { + this.state.isLoading = false; + } + } + + async fetchAnomalies() { + if (!this.state.currentReportType) return; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/get_anomalies`, { + report_type: this.state.currentReportType, + date_from: this.state.dateFrom, + date_to: this.state.dateTo, + comparison: this.state.comparison, + company_id: this.state.companyId, + }); + this.state.currentAnomalies = result.anomalies || []; + } catch (err) { + this.state.currentAnomalies = []; + } + } + + async generateCommentary({ forceRegenerate = false } = {}) { + if (!this.state.currentReportType) return; + this.state.isGeneratingCommentary = true; + try { + this.state.currentCommentary = await this.rpc(`${ENDPOINT_BASE}/get_commentary`, { + report_type: this.state.currentReportType, + date_from: this.state.dateFrom, + date_to: this.state.dateTo, + comparison: this.state.comparison, + company_id: this.state.companyId, + force_regenerate: forceRegenerate, + }); + return this.state.currentCommentary; + } catch (err) { + this.notification.add(`Commentary failed: ${err.message || err}`, { type: 'danger' }); + throw err; + } finally { + this.state.isGeneratingCommentary = false; + } + } + + async drillDown(accountId, label = null) { + try { + const result = await this.rpc(`${ENDPOINT_BASE}/drill_down`, { + account_id: accountId, + date_from: this.state.dateFrom, + date_to: this.state.dateTo, + company_id: this.state.companyId, + }); + this.state.drillDown = { + accountId, label, rows: result.rows || [], + count: result.count, isOpen: true, + }; + return result; + } catch (err) { + this.notification.add(`Drill failed: ${err.message || err}`, { type: 'danger' }); + throw err; + } + } + + closeDrillDown() { + if (this.state.drillDown) { + this.state.drillDown.isOpen = false; + } + } + + setComparison(mode) { + this.state.comparison = mode; + if (this.state.currentReportType) { + return this.runReport(this.state.currentReportType, + this.state.dateFrom, this.state.dateTo, mode); + } + } +} + +export const reportsService = { + dependencies: ["rpc", "notification"], + start(env, services) { return new ReportsService(env, services); }, +}; + +registry.category("services").add("fusion_reports", reportsService);