From 6051ef22a016e7944f8080f6a11d03caf1d5bf6f Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 17:36:52 -0400 Subject: [PATCH] feat(fusion_accounting_assets): assets_service.js reactive frontend service Made-with: Cursor --- fusion_accounting_assets/__manifest__.py | 3 +- .../static/src/services/assets_service.js | 149 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_assets/static/src/services/assets_service.js diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index 5522b92d..7bc8ffa0 100644 --- a/fusion_accounting_assets/__manifest__.py +++ b/fusion_accounting_assets/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Assets', - 'version': '19.0.1.0.19', + 'version': '19.0.1.0.20', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ @@ -40,6 +40,7 @@ menu hides; the engine + AI tools remain available for the chat. 'fusion_accounting_assets/static/src/scss/_variables.scss', 'fusion_accounting_assets/static/src/scss/assets.scss', 'fusion_accounting_assets/static/src/scss/dark_mode.scss', + 'fusion_accounting_assets/static/src/services/assets_service.js', ], }, 'installable': True, diff --git a/fusion_accounting_assets/static/src/services/assets_service.js b/fusion_accounting_assets/static/src/services/assets_service.js new file mode 100644 index 00000000..ceab62c0 --- /dev/null +++ b/fusion_accounting_assets/static/src/services/assets_service.js @@ -0,0 +1,149 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const ENDPOINT_BASE = "/fusion/assets"; + +export class AssetsService { + constructor(env, services) { + this.env = env; + this.rpc = services.rpc; + this.notification = services.notification; + + this.state = reactive({ + assets: [], + count: 0, + total: 0, + stateFilter: null, + categoryFilter: null, + isLoading: false, + isProcessing: false, + selectedAssetId: null, + selectedDetail: null, + companyId: null, + limit: 50, + offset: 0, + anomalies: [], + }); + } + + async loadAssets(companyId = null) { + this.state.companyId = companyId; + this.state.isLoading = true; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/list`, { + state: this.state.stateFilter, + category_id: this.state.categoryFilter, + limit: this.state.limit, + offset: this.state.offset, + company_id: companyId, + }); + this.state.assets = result.assets; + this.state.count = result.count; + this.state.total = result.total; + } finally { + this.state.isLoading = false; + } + } + + async selectAsset(assetId) { + this.state.selectedAssetId = assetId; + this.state.selectedDetail = null; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/get_detail`, { + asset_id: assetId, + }); + this.state.selectedDetail = result; + } catch (err) { + this.notification.add(`Failed to load asset detail: ${err.message || err}`, { type: "danger" }); + } + } + + async computeSchedule(assetId, recompute = false) { + this.state.isProcessing = true; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/compute_schedule`, { + asset_id: assetId, recompute: recompute, + }); + this.notification.add(`Schedule computed (${result.lines_created} lines)`, { type: "success" }); + if (this.state.selectedAssetId === assetId) { + await this.selectAsset(assetId); + } + return result; + } catch (err) { + this.notification.add(`Compute failed: ${err.message || err}`, { type: "danger" }); + throw err; + } finally { + this.state.isProcessing = false; + } + } + + async postDepreciation(assetId) { + this.state.isProcessing = true; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/post_depreciation`, { + asset_id: assetId, + }); + this.notification.add(`Posted ${result.posted_count} period(s)`, { type: "success" }); + if (this.state.selectedAssetId === assetId) { + await this.selectAsset(assetId); + } + return result; + } catch (err) { + this.notification.add(`Post failed: ${err.message || err}`, { type: "danger" }); + throw err; + } finally { + this.state.isProcessing = false; + } + } + + async disposeAsset(assetId, { saleAmount = 0, saleDate = null, salePartnerId = null, disposalType = "sale" } = {}) { + this.state.isProcessing = true; + try { + const result = await this.rpc(`${ENDPOINT_BASE}/dispose`, { + asset_id: assetId, sale_amount: saleAmount, + sale_date: saleDate, sale_partner_id: salePartnerId, + disposal_type: disposalType, + }); + this.notification.add(`Asset disposed: gain/loss $${result.gain_loss_amount.toFixed(2)}`, { type: "success" }); + await this.loadAssets(this.state.companyId); + return result; + } catch (err) { + this.notification.add(`Dispose failed: ${err.message || err}`, { type: "danger" }); + throw err; + } finally { + this.state.isProcessing = false; + } + } + + async fetchAnomalies(severity = null) { + try { + const result = await this.rpc(`${ENDPOINT_BASE}/get_anomalies`, { + severity: severity, company_id: this.state.companyId, + }); + this.state.anomalies = result.anomalies || []; + } catch (err) { + this.state.anomalies = []; + } + } + + async suggestUsefulLife(description, amount = null, partnerName = null) { + return await this.rpc(`${ENDPOINT_BASE}/suggest_useful_life`, { + description: description, amount: amount, partner_name: partnerName, + }); + } + + setStateFilter(state) { + this.state.stateFilter = state; + this.state.offset = 0; + this.loadAssets(this.state.companyId); + } +} + +export const assetsService = { + dependencies: ["rpc", "notification"], + start(env, services) { return new AssetsService(env, services); }, +}; + +registry.category("services").add("fusion_assets", assetsService);