diff --git a/fusion_accounting_assets/__init__.py b/fusion_accounting_assets/__init__.py index 7fc29f0a..e8f90eb7 100644 --- a/fusion_accounting_assets/__init__.py +++ b/fusion_accounting_assets/__init__.py @@ -1,3 +1,4 @@ from . import models from . import services from . import controllers +from . import wizards diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index 8b3f90fd..4f77df8d 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.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': [ diff --git a/fusion_accounting_assets/security/ir.model.access.csv b/fusion_accounting_assets/security/ir.model.access.csv index aae79f2e..1782a255 100644 --- a/fusion_accounting_assets/security/ir.model.access.csv +++ b/fusion_accounting_assets/security/ir.model.access.csv @@ -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 diff --git a/fusion_accounting_assets/tests/__init__.py b/fusion_accounting_assets/tests/__init__.py index e45db5b4..44936b8e 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -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 diff --git a/fusion_accounting_assets/tests/test_create_asset_wizard.py b/fusion_accounting_assets/tests/test_create_asset_wizard.py new file mode 100644 index 00000000..e6f67fb4 --- /dev/null +++ b/fusion_accounting_assets/tests/test_create_asset_wizard.py @@ -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) diff --git a/fusion_accounting_assets/wizards/__init__.py b/fusion_accounting_assets/wizards/__init__.py index e69de29b..02131f82 100644 --- a/fusion_accounting_assets/wizards/__init__.py +++ b/fusion_accounting_assets/wizards/__init__.py @@ -0,0 +1 @@ +from . import create_asset_wizard diff --git a/fusion_accounting_assets/wizards/create_asset_wizard.py b/fusion_accounting_assets/wizards/create_asset_wizard.py new file mode 100644 index 00000000..20dc5dd6 --- /dev/null +++ b/fusion_accounting_assets/wizards/create_asset_wizard.py @@ -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', + } diff --git a/fusion_accounting_assets/wizards/create_asset_wizard_views.xml b/fusion_accounting_assets/wizards/create_asset_wizard_views.xml new file mode 100644 index 00000000..05bbed19 --- /dev/null +++ b/fusion_accounting_assets/wizards/create_asset_wizard_views.xml @@ -0,0 +1,54 @@ + + + + fusion.create.asset.wizard.form + fusion.create.asset.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Create Asset from Invoice + fusion.create.asset.wizard + form + new + + list + +