feat(fusion_accounting_assets): top-level asset_dashboard component
Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Assets',
|
||||
'version': '19.0.1.0.20',
|
||||
'version': '19.0.1.0.21',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||
'description': """
|
||||
@@ -41,6 +41,9 @@ menu hides; the engine + AI tools remain available for the chat.
|
||||
'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',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard.js',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard.xml',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard_view.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { AssetCard } from "../../components/asset_card/asset_card";
|
||||
import { AssetDetailPanel } from "../../components/asset_detail_panel/asset_detail_panel";
|
||||
import { AnomalyStrip } from "../../components/anomaly_strip/anomaly_strip";
|
||||
|
||||
export class AssetDashboard extends Component {
|
||||
static template = "fusion_accounting_assets.AssetDashboard";
|
||||
static props = { "*": true };
|
||||
static components = { AssetCard, AssetDetailPanel, AnomalyStrip };
|
||||
|
||||
setup() {
|
||||
this.assets = useService("fusion_assets");
|
||||
this.state = useState(this.assets.state);
|
||||
|
||||
const companyId = this.env.services.user?.context?.allowed_company_ids?.[0];
|
||||
|
||||
onWillStart(async () => {
|
||||
await this.assets.loadAssets(companyId);
|
||||
await this.assets.fetchAnomalies();
|
||||
});
|
||||
}
|
||||
|
||||
onSelectAsset(id) {
|
||||
this.assets.selectAsset(id);
|
||||
}
|
||||
|
||||
onStateFilter(state) {
|
||||
this.assets.setStateFilter(state || null);
|
||||
}
|
||||
|
||||
formatCurrency(amount) {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
}).format(amount || 0);
|
||||
}
|
||||
|
||||
get totalCost() {
|
||||
return this.state.assets.reduce((sum, a) => sum + a.cost, 0);
|
||||
}
|
||||
|
||||
get totalBookValue() {
|
||||
return this.state.assets.reduce((sum, a) => sum + a.book_value, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.AssetDashboard">
|
||||
<div class="o_fusion_assets">
|
||||
<div class="o_fusion_assets_header">
|
||||
<div>
|
||||
<h1>Asset Management</h1>
|
||||
<div class="text-muted">
|
||||
<t t-esc="state.count"/> of <t t-esc="state.total"/> assets
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fusion_assets_summary">
|
||||
<div>Cost: <span class="summary-value">$<t t-esc="formatCurrency(totalCost)"/></span></div>
|
||||
<div>Book Value: <span class="summary-value">$<t t-esc="formatCurrency(totalBookValue)"/></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" style="gap: 0.5rem; padding: 0.75rem;">
|
||||
<button class="btn_asset" t-on-click="() => onStateFilter(null)"
|
||||
t-att-class="state.stateFilter === null ? 'primary' : ''">All</button>
|
||||
<button class="btn_asset" t-on-click="() => onStateFilter('draft')"
|
||||
t-att-class="state.stateFilter === 'draft' ? 'primary' : ''">Draft</button>
|
||||
<button class="btn_asset" t-on-click="() => onStateFilter('running')"
|
||||
t-att-class="state.stateFilter === 'running' ? 'primary' : ''">Running</button>
|
||||
<button class="btn_asset" t-on-click="() => onStateFilter('paused')"
|
||||
t-att-class="state.stateFilter === 'paused' ? 'primary' : ''">Paused</button>
|
||||
<button class="btn_asset" t-on-click="() => onStateFilter('disposed')"
|
||||
t-att-class="state.stateFilter === 'disposed' ? 'primary' : ''">Disposed</button>
|
||||
</div>
|
||||
|
||||
<AnomalyStrip t-foreach="state.anomalies" t-as="anomaly"
|
||||
t-key="anomaly.id" anomaly="anomaly"/>
|
||||
|
||||
<div class="d-flex" style="gap: 1rem; padding: 1rem;">
|
||||
<div style="flex: 1 1 60%;">
|
||||
<div t-if="state.isLoading" class="text-center p-4 text-muted">Loading...</div>
|
||||
<div t-elif="state.assets.length === 0" class="text-center p-4 text-muted">No assets found.</div>
|
||||
<div t-else="">
|
||||
<AssetCard t-foreach="state.assets" t-as="asset" t-key="asset.id"
|
||||
asset="asset" selected="state.selectedAssetId === asset.id"
|
||||
onSelect="() => onSelectAsset(asset.id)"
|
||||
formatCurrency="formatCurrency.bind(this)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1 1 40%;">
|
||||
<AssetDetailPanel t-if="state.selectedDetail"
|
||||
detail="state.selectedDetail"
|
||||
formatCurrency="formatCurrency.bind(this)"/>
|
||||
<div t-else="" class="p-4 text-muted">Select an asset to see details.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { AssetDashboard } from "./asset_dashboard";
|
||||
|
||||
export const fusionAssetDashboardView = {
|
||||
type: "fusion_assets",
|
||||
Controller: AssetDashboard,
|
||||
display_name: "Fusion Asset Management",
|
||||
icon: "fa-cubes",
|
||||
multiRecord: true,
|
||||
};
|
||||
|
||||
registry.category("views").add("fusion_assets", fusionAssetDashboardView);
|
||||
Reference in New Issue
Block a user