feat(fusion_accounting_assets): ai_useful_life_panel + anomaly_strip components
Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Assets',
|
||||
'version': '19.0.1.0.25',
|
||||
'version': '19.0.1.0.26',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||
'description': """
|
||||
@@ -52,6 +52,10 @@ menu hides; the engine + AI tools remain available for the chat.
|
||||
'fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.xml',
|
||||
'fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js',
|
||||
'fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.xml',
|
||||
'fusion_accounting_assets/static/src/components/ai_useful_life_panel/ai_useful_life_panel.js',
|
||||
'fusion_accounting_assets/static/src/components/ai_useful_life_panel/ai_useful_life_panel.xml',
|
||||
'fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js',
|
||||
'fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user