diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index b71d07e5..ac94d55a 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.28', + 'version': '19.0.1.0.29', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ @@ -36,6 +36,7 @@ menu hides; the engine + AI tools remain available for the chat. 'data/cron.xml', 'wizards/create_asset_wizard_views.xml', 'wizards/disposal_wizard_views.xml', + 'wizards/partial_sale_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 7448bb30..14b5dcd5 100644 --- a/fusion_accounting_assets/security/ir.model.access.csv +++ b/fusion_accounting_assets/security/ir.model.access.csv @@ -11,3 +11,4 @@ access_fusion_asset_anomaly_user,fusion.asset.anomaly.user,model_fusion_asset_an 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 access_fusion_disposal_wizard_user,fusion.disposal.wizard.user,model_fusion_disposal_wizard,base.group_user,1,1,1,0 +access_fusion_partial_sale_wizard_user,fusion.partial.sale.wizard.user,model_fusion_partial_sale_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 0a56cdc0..ee130246 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -21,3 +21,4 @@ from . import test_asset_book_values_mv from . import test_performance_benchmarks from . import test_create_asset_wizard from . import test_disposal_wizard +from . import test_partial_sale_wizard diff --git a/fusion_accounting_assets/tests/test_partial_sale_wizard.py b/fusion_accounting_assets/tests/test_partial_sale_wizard.py new file mode 100644 index 00000000..5c16117d --- /dev/null +++ b/fusion_accounting_assets/tests/test_partial_sale_wizard.py @@ -0,0 +1,48 @@ +from datetime import date + +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged('post_install', '-at_install') +class TestPartialSaleWizard(TransactionCase): + + def setUp(self): + super().setUp() + self.asset = self.env['fusion.asset'].create({ + 'name': 'Partial Sale Test', + 'cost': 10000, + 'acquisition_date': date(2026, 1, 1), + 'in_service_date': date(2026, 1, 1), + 'method': 'straight_line', 'useful_life_years': 5, + }) + self.env['fusion.asset.engine'].compute_depreciation_schedule(self.asset) + self.asset.action_set_running() + + def test_partial_sell_30pct_creates_child(self): + wizard = self.env['fusion.partial.sale.wizard'].create({ + 'asset_id': self.asset.id, + 'sold_pct': 30.0, 'sold_amount': 4000, + 'sale_date': date(2026, 6, 1), + }) + wizard.action_partial_sell() + self.asset.invalidate_recordset(['cost']) + self.assertAlmostEqual(self.asset.cost, 7000, places=2) + + def test_invalid_pct_raises(self): + wizard = self.env['fusion.partial.sale.wizard'].create({ + 'asset_id': self.asset.id, + 'sold_pct': 0, 'sold_amount': 100, + }) + with self.assertRaises(UserError): + wizard.action_partial_sell() + + def test_compute_estimated_gain_loss(self): + wizard = self.env['fusion.partial.sale.wizard'].new({ + 'asset_id': self.asset.id, + 'sold_pct': 30.0, 'sold_amount': 4000, + }) + wizard._compute_sold_cost() + self.assertAlmostEqual(wizard.estimated_sold_cost, 3000, places=2) + self.assertAlmostEqual(wizard.estimated_gain_loss, 1000, places=2) diff --git a/fusion_accounting_assets/wizards/__init__.py b/fusion_accounting_assets/wizards/__init__.py index a4f73d87..f55dc348 100644 --- a/fusion_accounting_assets/wizards/__init__.py +++ b/fusion_accounting_assets/wizards/__init__.py @@ -1,2 +1,3 @@ from . import create_asset_wizard from . import disposal_wizard +from . import partial_sale_wizard diff --git a/fusion_accounting_assets/wizards/partial_sale_wizard.py b/fusion_accounting_assets/wizards/partial_sale_wizard.py new file mode 100644 index 00000000..4524b4c7 --- /dev/null +++ b/fusion_accounting_assets/wizards/partial_sale_wizard.py @@ -0,0 +1,67 @@ +"""Partial sale wizard (sell a portion of an asset). + +Splits the asset into a child (the sold portion) and disposes the child; +parent retains remaining cost + salvage.""" + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class FusionPartialSaleWizard(models.TransientModel): + _name = "fusion.partial.sale.wizard" + _description = "Asset Partial Sale Wizard" + + asset_id = fields.Many2one( + 'fusion.asset', required=True, + default=lambda self: self._default_asset(), + ) + company_id = fields.Many2one(related='asset_id.company_id') + currency_id = fields.Many2one(related='asset_id.currency_id') + cost = fields.Monetary(related='asset_id.cost', readonly=True) + book_value = fields.Monetary(related='asset_id.book_value', readonly=True) + + sold_pct = fields.Float( + string='% of cost being sold', default=30.0, + help="Percentage of original cost attributed to the sold portion.", + ) + sold_amount = fields.Monetary(string='Sale Amount', required=True) + sale_date = fields.Date(required=True, default=fields.Date.today) + sale_partner_id = fields.Many2one('res.partner') + + estimated_sold_cost = fields.Monetary(compute='_compute_sold_cost') + estimated_gain_loss = fields.Monetary(compute='_compute_sold_cost') + + @api.model + def _default_asset(self): + ctx = self.env.context + if ctx.get('active_model') == 'fusion.asset': + return ctx.get('active_id') + return False + + @api.depends('sold_pct', 'sold_amount', 'cost') + def _compute_sold_cost(self): + for w in self: + w.estimated_sold_cost = round(w.cost * (w.sold_pct or 0) / 100, 2) + w.estimated_gain_loss = w.sold_amount - w.estimated_sold_cost + + def action_partial_sell(self): + self.ensure_one() + if not (0 < self.sold_pct < 100): + raise UserError(_("sold_pct must be strictly between 0 and 100.")) + if self.asset_id.state == 'disposed': + raise UserError(_("Asset already disposed.")) + + result = self.env['fusion.asset.engine'].partial_sale( + self.asset_id, + sold_amount=self.sold_amount, + sold_qty=self.sold_pct / 100, + sale_date=self.sale_date, + sale_partner=self.sale_partner_id, + ) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'fusion.asset', + 'res_id': result['parent_asset_id'], + 'view_mode': 'form', + 'target': 'current', + } diff --git a/fusion_accounting_assets/wizards/partial_sale_wizard_views.xml b/fusion_accounting_assets/wizards/partial_sale_wizard_views.xml new file mode 100644 index 00000000..8848d5b1 --- /dev/null +++ b/fusion_accounting_assets/wizards/partial_sale_wizard_views.xml @@ -0,0 +1,38 @@ + + + + fusion.partial.sale.wizard.form + fusion.partial.sale.wizard + +
+ + + + + + + + + + + + + + + +
+
+
+
+
+ + + Partial Sale + fusion.partial.sale.wizard + form + new + +