changes
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class AiUsefulLifePanel extends Component {
|
||||
static template = "fusion_accounting_assets.AiUsefulLifePanel";
|
||||
static props = {
|
||||
description: { type: String, optional: true },
|
||||
amount: { type: Number, optional: true },
|
||||
onSelect: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.assets = useService("fusion_assets");
|
||||
this.state = useState({
|
||||
suggestion: null,
|
||||
isLoading: false,
|
||||
descInput: this.props.description || '',
|
||||
amountInput: this.props.amount || '',
|
||||
});
|
||||
}
|
||||
|
||||
async onSuggest() {
|
||||
this.state.isLoading = true;
|
||||
try {
|
||||
this.state.suggestion = await this.assets.suggestUsefulLife(
|
||||
this.state.descInput,
|
||||
parseFloat(this.state.amountInput) || null,
|
||||
);
|
||||
} finally {
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onUseSuggestion() {
|
||||
if (this.state.suggestion && this.props.onSelect) {
|
||||
this.props.onSelect(this.state.suggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.AiUsefulLifePanel">
|
||||
<div style="background: white; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 0.5rem;">
|
||||
<h5>AI Suggest Useful Life</h5>
|
||||
<div class="mb-2">
|
||||
<label>Description</label>
|
||||
<input class="form-control" t-att-value="state.descInput"
|
||||
t-on-input="(ev) => state.descInput = ev.target.value"/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label>Amount</label>
|
||||
<input type="number" class="form-control" t-att-value="state.amountInput"
|
||||
t-on-input="(ev) => state.amountInput = ev.target.value"/>
|
||||
</div>
|
||||
<button class="btn_asset primary" t-on-click="onSuggest"
|
||||
t-att-disabled="state.isLoading">
|
||||
<t t-if="state.isLoading">Asking AI...</t>
|
||||
<t t-else="">Suggest</t>
|
||||
</button>
|
||||
|
||||
<div t-if="state.suggestion" class="mt-3 p-2"
|
||||
style="background: #eff6ff; border-radius: 0.25rem;">
|
||||
<div><strong>Suggested life:</strong> <t t-esc="state.suggestion.useful_life_years"/> years</div>
|
||||
<div><strong>Method:</strong> <t t-esc="state.suggestion.depreciation_method"/></div>
|
||||
<div class="text-muted small">
|
||||
<em><t t-esc="state.suggestion.rationale"/></em>
|
||||
(confidence: <t t-esc="(state.suggestion.confidence * 100).toFixed(0)"/>%)
|
||||
</div>
|
||||
<button class="btn_asset mt-2" t-if="props.onSelect" t-on-click="onUseSuggestion">
|
||||
Use This
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,17 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class AnomalyStrip extends Component {
|
||||
static template = "fusion_accounting_assets.AnomalyStrip";
|
||||
static props = {
|
||||
anomaly: { type: Object },
|
||||
};
|
||||
|
||||
formatNumber(n) {
|
||||
if (n === null || n === undefined) return "";
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 0, maximumFractionDigits: 1,
|
||||
}).format(n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.AnomalyStrip">
|
||||
<div class="o_fusion_anomaly_strip" t-att-data-severity="props.anomaly.severity">
|
||||
<strong>
|
||||
<t t-esc="props.anomaly.asset_name || 'Asset'"/>
|
||||
</strong>
|
||||
<span class="ms-2">
|
||||
<t t-esc="props.anomaly.anomaly_type.replace('_', ' ')"/>:
|
||||
<t t-esc="formatNumber(props.anomaly.variance_pct)"/>%
|
||||
</span>
|
||||
<span class="ms-3 text-muted">
|
||||
<t t-esc="props.anomaly.detail"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class AssetCard extends Component {
|
||||
static template = "fusion_accounting_assets.AssetCard";
|
||||
static props = {
|
||||
asset: { type: Object },
|
||||
selected: { type: Boolean, optional: true },
|
||||
onSelect: { type: Function },
|
||||
formatCurrency: { type: Function },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.AssetCard">
|
||||
<div class="o_fusion_assets_card"
|
||||
t-att-class="props.selected ? 'selected' : ''"
|
||||
t-on-click="props.onSelect">
|
||||
<div class="o_fusion_assets_card_header">
|
||||
<div class="asset-name">
|
||||
<t t-esc="props.asset.name"/>
|
||||
<span t-if="props.asset.code" class="text-muted ms-2">
|
||||
[<t t-esc="props.asset.code"/>]
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-state-badge" t-att-data-state="props.asset.state">
|
||||
<t t-esc="props.asset.state"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-numbers">
|
||||
<div>
|
||||
<span class="label">Cost:</span>
|
||||
<span class="value">$<t t-esc="props.formatCurrency(props.asset.cost)"/></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Book Value:</span>
|
||||
<span class="value">$<t t-esc="props.formatCurrency(props.asset.book_value)"/></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Method:</span>
|
||||
<span class="value"><t t-esc="props.asset.method"/></span>
|
||||
</div>
|
||||
<div t-if="props.asset.category_name">
|
||||
<span class="label">Category:</span>
|
||||
<span class="value"><t t-esc="props.asset.category_name"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { DepreciationBoard } from "../depreciation_board/depreciation_board";
|
||||
|
||||
export class AssetDetailPanel extends Component {
|
||||
static template = "fusion_accounting_assets.AssetDetailPanel";
|
||||
static props = {
|
||||
detail: { type: Object },
|
||||
formatCurrency: { type: Function },
|
||||
};
|
||||
static components = { DepreciationBoard };
|
||||
|
||||
setup() {
|
||||
this.assets = useService("fusion_assets");
|
||||
}
|
||||
|
||||
async onComputeSchedule() {
|
||||
await this.assets.computeSchedule(this.props.detail.asset.id, false);
|
||||
}
|
||||
|
||||
async onRecomputeSchedule() {
|
||||
await this.assets.computeSchedule(this.props.detail.asset.id, true);
|
||||
}
|
||||
|
||||
async onPostDepreciation() {
|
||||
await this.assets.postDepreciation(this.props.detail.asset.id);
|
||||
}
|
||||
|
||||
async onDispose() {
|
||||
const saleAmount = parseFloat(prompt("Sale amount (0 for scrap)?", "0"));
|
||||
if (isNaN(saleAmount)) return;
|
||||
await this.assets.disposeAsset(this.props.detail.asset.id, { saleAmount });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.AssetDetailPanel">
|
||||
<div style="background: white; padding: 1rem; border-radius: 0.5rem; border: 1px solid #e5e7eb;">
|
||||
<h3><t t-esc="props.detail.asset.name"/></h3>
|
||||
<div class="text-muted" t-if="props.detail.asset.code">
|
||||
[<t t-esc="props.detail.asset.code"/>]
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div><strong>State:</strong> <t t-esc="props.detail.asset.state"/></div>
|
||||
<div><strong>Cost:</strong> $<t t-esc="props.formatCurrency(props.detail.asset.cost)"/></div>
|
||||
<div><strong>Salvage:</strong> $<t t-esc="props.formatCurrency(props.detail.asset.salvage_value)"/></div>
|
||||
<div><strong>Book Value:</strong> $<t t-esc="props.formatCurrency(props.detail.asset.book_value)"/></div>
|
||||
<div><strong>Total Depreciated:</strong> $<t t-esc="props.formatCurrency(props.detail.asset.total_depreciated)"/></div>
|
||||
<div><strong>Method:</strong> <t t-esc="props.detail.asset.method"/></div>
|
||||
<div><strong>Useful Life:</strong> <t t-esc="props.detail.asset.useful_life_years"/> years</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex mt-3" style="gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button class="btn_asset" t-on-click="onComputeSchedule">Compute Schedule</button>
|
||||
<button class="btn_asset" t-on-click="onRecomputeSchedule">Recompute</button>
|
||||
<button class="btn_asset primary"
|
||||
t-if="props.detail.asset.state === 'running'"
|
||||
t-on-click="onPostDepreciation">Post Next</button>
|
||||
<button class="btn_asset danger"
|
||||
t-if="props.detail.asset.state !== 'disposed'"
|
||||
t-on-click="onDispose">Dispose</button>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Depreciation Schedule</h4>
|
||||
<DepreciationBoard t-if="props.detail.depreciation_lines"
|
||||
lines="props.detail.depreciation_lines"
|
||||
formatCurrency="props.formatCurrency"/>
|
||||
|
||||
<div t-if="props.detail.anomalies and props.detail.anomalies.length" class="mt-3">
|
||||
<h4>Active Anomalies</h4>
|
||||
<div t-foreach="props.detail.anomalies" t-as="a" t-key="a.id"
|
||||
class="o_fusion_anomaly_strip" t-att-data-severity="a.severity">
|
||||
<strong><t t-esc="a.anomaly_type"/></strong>: <t t-esc="a.detail"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class DepreciationBoard extends Component {
|
||||
static template = "fusion_accounting_assets.DepreciationBoard";
|
||||
static props = {
|
||||
lines: { type: Array },
|
||||
formatCurrency: { type: Function },
|
||||
};
|
||||
|
||||
rowClass(line) {
|
||||
if (line.is_posted) return "posted";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.DepreciationBoard">
|
||||
<div class="o_fusion_assets_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Date</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end">Accumulated</th>
|
||||
<th class="text-end">Book Value</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="props.lines" t-as="line" t-key="line.id"
|
||||
t-att-class="rowClass(line)">
|
||||
<td><t t-esc="line.period_index + 1"/></td>
|
||||
<td><t t-esc="line.scheduled_date"/></td>
|
||||
<td class="text-end">$<t t-esc="props.formatCurrency(line.amount)"/></td>
|
||||
<td class="text-end">$<t t-esc="props.formatCurrency(line.accumulated)"/></td>
|
||||
<td class="text-end">$<t t-esc="props.formatCurrency(line.book_value_at_end)"/></td>
|
||||
<td>
|
||||
<t t-if="line.is_posted">Posted</t>
|
||||
<t t-else="">Pending</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class DisposalDialog extends Component {
|
||||
static template = "fusion_accounting_assets.DisposalDialog";
|
||||
static props = {
|
||||
assetId: { type: Number },
|
||||
onClose: { type: Function },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.assets = useService("fusion_assets");
|
||||
this.state = useState({
|
||||
disposalType: 'sale',
|
||||
saleAmount: 0,
|
||||
saleDate: new Date().toISOString().slice(0, 10),
|
||||
});
|
||||
}
|
||||
|
||||
async onConfirm() {
|
||||
try {
|
||||
await this.assets.disposeAsset(this.props.assetId, {
|
||||
disposalType: this.state.disposalType,
|
||||
saleAmount: parseFloat(this.state.saleAmount) || 0,
|
||||
saleDate: this.state.saleDate,
|
||||
});
|
||||
this.props.onClose();
|
||||
} catch (e) {
|
||||
// Error already shown by service
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_accounting_assets.DisposalDialog">
|
||||
<div class="modal" style="display: block; background: rgba(0,0,0,0.5); position: fixed; top:0; left:0; right:0; bottom:0; z-index: 1050;">
|
||||
<div class="modal-dialog" style="margin: 5vh auto; max-width: 500px;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5>Dispose Asset</h5>
|
||||
<button class="btn-close" t-on-click="props.onClose">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label>Disposal Type</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => state.disposalType = ev.target.value">
|
||||
<option value="sale" selected="state.disposalType === 'sale'">Sale</option>
|
||||
<option value="scrap" selected="state.disposalType === 'scrap'">Scrap</option>
|
||||
<option value="donation" selected="state.disposalType === 'donation'">Donation</option>
|
||||
<option value="lost" selected="state.disposalType === 'lost'">Lost</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3" t-if="state.disposalType === 'sale'">
|
||||
<label>Sale Amount ($)</label>
|
||||
<input type="number" class="form-control"
|
||||
t-att-value="state.saleAmount"
|
||||
t-on-change="(ev) => state.saleAmount = ev.target.value"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label>Date</label>
|
||||
<input type="date" class="form-control"
|
||||
t-att-value="state.saleDate"
|
||||
t-on-change="(ev) => state.saleDate = ev.target.value"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn_asset" t-on-click="props.onClose">Cancel</button>
|
||||
<button class="btn_asset primary" t-on-click="onConfirm">Confirm Disposal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user