feat(fusion_accounting_assets): asset creation wizard with AI useful-life suggest

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 20:02:46 -04:00
parent 9794970429
commit a6ef7e0c2a
8 changed files with 255 additions and 1 deletions

View File

@@ -1,3 +1,4 @@
from . import models
from . import services
from . import controllers
from . import wizards

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Assets',
'version': '19.0.1.0.26',
'version': '19.0.1.0.27',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented asset management with depreciation schedules.',
'description': """
@@ -34,6 +34,7 @@ menu hides; the engine + AI tools remain available for the chat.
'data': [
'security/ir.model.access.csv',
'data/cron.xml',
'wizards/create_asset_wizard_views.xml',
],
'assets': {
'web.assets_backend': [

View File

@@ -9,3 +9,4 @@ access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_
access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
access_fusion_asset_anomaly_user,fusion.asset.anomaly.user,model_fusion_asset_anomaly,base.group_user,1,0,0,0
access_fusion_asset_anomaly_admin,fusion.asset.anomaly.admin,model_fusion_asset_anomaly,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
access_fusion_create_asset_wizard_user,fusion.create.asset.wizard.user,model_fusion_create_asset_wizard,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
9 access_fusion_asset_disposal_admin fusion.asset.disposal.admin model_fusion_asset_disposal fusion_accounting_core.group_fusion_accounting_admin 1 1 1 1
10 access_fusion_asset_anomaly_user fusion.asset.anomaly.user model_fusion_asset_anomaly base.group_user 1 0 0 0
11 access_fusion_asset_anomaly_admin fusion.asset.anomaly.admin model_fusion_asset_anomaly fusion_accounting_core.group_fusion_accounting_admin 1 1 1 1
12 access_fusion_create_asset_wizard_user fusion.create.asset.wizard.user model_fusion_create_asset_wizard base.group_user 1 1 1 0

View File

@@ -19,3 +19,4 @@ from . import test_engine_property
from . import test_method_integration
from . import test_asset_book_values_mv
from . import test_performance_benchmarks
from . import test_create_asset_wizard

View File

@@ -0,0 +1,62 @@
from datetime import date
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class TestCreateAssetWizard(TransactionCase):
def setUp(self):
super().setUp()
self.env['ir.config_parameter'].sudo().search([
('key', 'in', ['fusion_accounting.provider.asset_useful_life',
'fusion_accounting.provider.default'])
]).unlink()
def test_create_minimal_asset(self):
wizard = self.env['fusion.create.asset.wizard'].create({
'name': 'Test Asset',
'cost': 5000,
'method': 'straight_line',
'useful_life_years': 5,
'acquisition_date': date(2026, 1, 1),
'source_invoice_line_id': False,
})
action = wizard.action_create_asset()
self.assertEqual(action['res_model'], 'fusion.asset')
asset = self.env['fusion.asset'].browse(action['res_id'])
self.assertEqual(asset.name, 'Test Asset')
self.assertEqual(asset.cost, 5000)
def test_ai_suggest_fills_fields(self):
wizard = self.env['fusion.create.asset.wizard'].create({
'name': 'Dell laptop',
'cost': 2000,
'method': 'straight_line',
'useful_life_years': 5,
'acquisition_date': date(2026, 1, 1),
})
wizard.action_ai_suggest()
self.assertEqual(wizard.ai_suggested_years, 4)
self.assertEqual(wizard.useful_life_years, 4)
def test_category_onchange_pre_fills(self):
category = self.env['fusion.asset.category'].create({
'name': 'Test Category',
'method': 'declining_balance',
'useful_life_years': 7,
'declining_rate_pct': 25.0,
'salvage_value_pct': 10.0,
})
wizard = self.env['fusion.create.asset.wizard'].new({
'name': 'Test', 'cost': 10000,
'method': 'straight_line', 'useful_life_years': 5,
'acquisition_date': date(2026, 1, 1),
'category_id': category.id,
})
wizard._onchange_category_id()
self.assertEqual(wizard.method, 'declining_balance')
self.assertEqual(wizard.useful_life_years, 7)
self.assertEqual(wizard.declining_rate_pct, 25.0)
self.assertAlmostEqual(wizard.salvage_value, 1000, places=2)

View File

@@ -0,0 +1 @@
from . import create_asset_wizard

View File

@@ -0,0 +1,133 @@
"""Create-asset-from-invoice-line wizard.
Reads an account.move.line as the source, pre-fills name/cost/category,
and optionally calls the AI useful-life predictor for suggestions."""
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from ..services.useful_life_predictor import predict_useful_life
class FusionCreateAssetWizard(models.TransientModel):
_name = "fusion.create.asset.wizard"
_description = "Create Fusion Asset from Invoice Line"
source_invoice_line_id = fields.Many2one(
'account.move.line', string='Source Invoice Line',
default=lambda self: self._default_source_line(),
)
name = fields.Char(required=True)
cost = fields.Monetary(required=True)
salvage_value = fields.Monetary(default=0.0)
currency_id = fields.Many2one(
'res.currency', required=True,
default=lambda self: self.env.company.currency_id,
)
category_id = fields.Many2one('fusion.asset.category')
method = fields.Selection([
('straight_line', 'Straight Line'),
('declining_balance', 'Declining Balance'),
('units_of_production', 'Units of Production'),
], required=True, default='straight_line')
useful_life_years = fields.Integer(default=5)
declining_rate_pct = fields.Float(default=20.0)
acquisition_date = fields.Date(required=True, default=fields.Date.today)
in_service_date = fields.Date(default=fields.Date.today)
ai_suggested_years = fields.Integer(readonly=True)
ai_suggested_method = fields.Char(readonly=True)
ai_rationale = fields.Text(readonly=True)
ai_confidence = fields.Float(readonly=True)
@api.model
def _default_source_line(self):
ctx = self.env.context
if ctx.get('active_model') == 'account.move.line':
return ctx.get('active_id')
return False
@api.onchange('source_invoice_line_id')
def _onchange_source_invoice_line_id(self):
if not self.source_invoice_line_id:
return
line = self.source_invoice_line_id
if not self.name:
self.name = line.name or line.move_id.name or 'New Asset'
if not self.cost:
self.cost = abs(line.balance) if line.balance else (line.price_unit * line.quantity)
if line.currency_id and not self.currency_id:
self.currency_id = line.currency_id
@api.onchange('category_id')
def _onchange_category_id(self):
if self.category_id:
self.method = self.category_id.method
self.useful_life_years = self.category_id.useful_life_years
self.declining_rate_pct = self.category_id.declining_rate_pct
if self.category_id.salvage_value_pct and self.cost:
self.salvage_value = round(
self.cost * self.category_id.salvage_value_pct / 100, 2)
def action_ai_suggest(self):
"""Call AI useful-life predictor."""
self.ensure_one()
if not self.name and not self.source_invoice_line_id:
raise UserError(_("Need a name or source invoice line first."))
description = self.name
if self.source_invoice_line_id and self.source_invoice_line_id.name:
description = self.source_invoice_line_id.name
partner_name = None
if self.source_invoice_line_id and self.source_invoice_line_id.partner_id:
partner_name = self.source_invoice_line_id.partner_id.name
suggestion = predict_useful_life(
self.env, description=description,
amount=self.cost, partner_name=partner_name,
)
self.write({
'ai_suggested_years': suggestion.get('useful_life_years'),
'ai_suggested_method': suggestion.get('depreciation_method'),
'ai_rationale': suggestion.get('rationale'),
'ai_confidence': suggestion.get('confidence'),
'useful_life_years': suggestion.get('useful_life_years'),
'method': suggestion.get('depreciation_method'),
})
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
def action_create_asset(self):
"""Create the fusion.asset record + link to source invoice line."""
self.ensure_one()
if not self.cost:
raise UserError(_("Cost is required."))
Asset = self.env['fusion.asset']
asset = Asset.create({
'name': self.name,
'cost': self.cost,
'salvage_value': self.salvage_value,
'currency_id': self.currency_id.id,
'category_id': self.category_id.id if self.category_id else False,
'method': self.method,
'useful_life_years': self.useful_life_years,
'declining_rate_pct': self.declining_rate_pct,
'acquisition_date': self.acquisition_date,
'in_service_date': self.in_service_date,
'source_invoice_line_id': self.source_invoice_line_id.id if self.source_invoice_line_id else False,
'company_id': self.env.company.id,
})
if self.source_invoice_line_id:
self.source_invoice_line_id.fusion_asset_id = asset.id
return {
'type': 'ir.actions.act_window',
'res_model': 'fusion.asset',
'res_id': asset.id,
'view_mode': 'form',
'target': 'current',
}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_fusion_create_asset_wizard_form" model="ir.ui.view">
<field name="name">fusion.create.asset.wizard.form</field>
<field name="model">fusion.create.asset.wizard</field>
<field name="arch" type="xml">
<form string="Create Fixed Asset">
<group>
<group string="Basics">
<field name="name"/>
<field name="source_invoice_line_id"
options="{'no_create': True}"
readonly="source_invoice_line_id"/>
<field name="cost"/>
<field name="currency_id" invisible="1"/>
<field name="salvage_value"/>
<field name="category_id" options="{'no_create': True}"/>
</group>
<group string="Depreciation">
<field name="method"/>
<field name="useful_life_years"
invisible="method == 'units_of_production'"/>
<field name="declining_rate_pct"
invisible="method != 'declining_balance'"/>
<field name="acquisition_date"/>
<field name="in_service_date"/>
</group>
</group>
<group string="AI Suggestion" invisible="not ai_suggested_years">
<field name="ai_suggested_years" readonly="1"/>
<field name="ai_suggested_method" readonly="1"/>
<field name="ai_rationale" readonly="1"/>
<field name="ai_confidence" readonly="1"/>
</group>
<footer>
<button name="action_ai_suggest" type="object"
string="AI Suggest" class="btn-secondary"/>
<button name="action_create_asset" type="object"
string="Create Asset" class="btn-primary"/>
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_fusion_create_asset_wizard" model="ir.actions.act_window">
<field name="name">Create Asset from Invoice</field>
<field name="res_model">fusion.create.asset.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="account.model_account_move_line"/>
<field name="binding_view_types">list</field>
</record>
</odoo>