From 5c897631918ebf906cfc6eb0a9095b8e370a6e17 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 16:58:12 -0400 Subject: [PATCH] feat(fusion_accounting_assets): asset disposal record model - 4 disposal types: sale, scrap, donation, lost - mail.thread tracking on type / date / sale amount / partner - gain_loss_amount computed: - sale: sale_amount - book_value_at_disposal - scrap / donation / lost: -book_value_at_disposal (full loss) - ondelete='restrict' on asset_id (cannot delete an asset with disposal) - move_id placeholder for engine-created journal entry - 4 new tests (59 total) Made-with: Cursor --- fusion_accounting_assets/__manifest__.py | 2 +- fusion_accounting_assets/models/__init__.py | 1 + .../models/fusion_asset_disposal.py | 56 +++++++++++++++++++ .../security/ir.model.access.csv | 2 + fusion_accounting_assets/tests/__init__.py | 1 + .../tests/test_fusion_asset_disposal.py | 56 +++++++++++++++++++ 6 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_assets/models/fusion_asset_disposal.py create mode 100644 fusion_accounting_assets/tests/test_fusion_asset_disposal.py diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index 5131b40b..1af012e4 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.8', + 'version': '19.0.1.0.9', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ diff --git a/fusion_accounting_assets/models/__init__.py b/fusion_accounting_assets/models/__init__.py index 0daeae31..ac027f8f 100644 --- a/fusion_accounting_assets/models/__init__.py +++ b/fusion_accounting_assets/models/__init__.py @@ -1,3 +1,4 @@ from . import fusion_asset_category from . import fusion_asset from . import fusion_asset_depreciation_line +from . import fusion_asset_disposal diff --git a/fusion_accounting_assets/models/fusion_asset_disposal.py b/fusion_accounting_assets/models/fusion_asset_disposal.py new file mode 100644 index 00000000..089d0098 --- /dev/null +++ b/fusion_accounting_assets/models/fusion_asset_disposal.py @@ -0,0 +1,56 @@ +"""Asset disposal records (sale, scrap, donation).""" + +from odoo import api, fields, models + + +DISPOSAL_TYPES = [ + ('sale', 'Sale'), + ('scrap', 'Scrap'), + ('donation', 'Donation'), + ('lost', 'Lost / Stolen'), +] + + +class FusionAssetDisposal(models.Model): + _name = "fusion.asset.disposal" + _description = "Asset Disposal Record" + _order = "disposal_date desc, id desc" + _inherit = ['mail.thread'] + + asset_id = fields.Many2one( + 'fusion.asset', required=True, ondelete='restrict', tracking=True, + ) + company_id = fields.Many2one(related='asset_id.company_id', store=True) + currency_id = fields.Many2one(related='asset_id.currency_id', store=True) + + disposal_type = fields.Selection( + DISPOSAL_TYPES, required=True, default='sale', tracking=True, + ) + disposal_date = fields.Date( + required=True, default=fields.Date.today, tracking=True, + ) + sale_amount = fields.Monetary( + default=0.0, tracking=True, + help="Cash received (for sale disposal type).", + ) + sale_partner_id = fields.Many2one('res.partner', tracking=True) + + book_value_at_disposal = fields.Monetary( + readonly=True, + help="Asset book value at disposal date.", + ) + gain_loss_amount = fields.Monetary(compute='_compute_gain_loss', store=True) + notes = fields.Text() + + move_id = fields.Many2one( + 'account.move', readonly=True, copy=False, + help="Journal entry created for this disposal.", + ) + + @api.depends('sale_amount', 'book_value_at_disposal', 'disposal_type') + def _compute_gain_loss(self): + for d in self: + if d.disposal_type == 'sale': + d.gain_loss_amount = d.sale_amount - d.book_value_at_disposal + else: + d.gain_loss_amount = -d.book_value_at_disposal diff --git a/fusion_accounting_assets/security/ir.model.access.csv b/fusion_accounting_assets/security/ir.model.access.csv index d765dd4e..b4fd5897 100644 --- a/fusion_accounting_assets/security/ir.model.access.csv +++ b/fusion_accounting_assets/security/ir.model.access.csv @@ -5,3 +5,5 @@ access_fusion_asset_depreciation_line_user,fusion.asset.depreciation.line.user,m access_fusion_asset_depreciation_line_admin,fusion.asset.depreciation.line.admin,model_fusion_asset_depreciation_line,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 access_fusion_asset_category_user,fusion.asset.category.user,model_fusion_asset_category,base.group_user,1,0,0,0 access_fusion_asset_category_admin,fusion.asset.category.admin,model_fusion_asset_category,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 +access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_disposal,base.group_user,1,0,0,0 +access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 diff --git a/fusion_accounting_assets/tests/__init__.py b/fusion_accounting_assets/tests/__init__.py index b5ea6f5f..18a546bd 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -6,3 +6,4 @@ from . import test_useful_life_predictor from . import test_fusion_asset from . import test_fusion_asset_depreciation_line from . import test_fusion_asset_category +from . import test_fusion_asset_disposal diff --git a/fusion_accounting_assets/tests/test_fusion_asset_disposal.py b/fusion_accounting_assets/tests/test_fusion_asset_disposal.py new file mode 100644 index 00000000..86bbf99f --- /dev/null +++ b/fusion_accounting_assets/tests/test_fusion_asset_disposal.py @@ -0,0 +1,56 @@ +from datetime import date + +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestFusionAssetDisposal(TransactionCase): + + def setUp(self): + super().setUp() + self.asset = self.env['fusion.asset'].create({ + 'name': 'Disposable Asset', + 'cost': 10000, + 'salvage_value': 0, + 'acquisition_date': date(2026, 1, 1), + 'method': 'straight_line', + 'useful_life_years': 5, + }) + + def test_create_minimal_sale(self): + d = self.env['fusion.asset.disposal'].create({ + 'asset_id': self.asset.id, + 'disposal_type': 'sale', + 'sale_amount': 7000, + 'book_value_at_disposal': 6000, + }) + self.assertEqual(d.gain_loss_amount, 1000) + self.assertEqual(d.company_id, self.asset.company_id) + + def test_sale_at_loss(self): + d = self.env['fusion.asset.disposal'].create({ + 'asset_id': self.asset.id, + 'disposal_type': 'sale', + 'sale_amount': 4000, + 'book_value_at_disposal': 6000, + }) + self.assertEqual(d.gain_loss_amount, -2000) + + def test_scrap_full_loss(self): + d = self.env['fusion.asset.disposal'].create({ + 'asset_id': self.asset.id, + 'disposal_type': 'scrap', + 'sale_amount': 0, + 'book_value_at_disposal': 6000, + }) + self.assertEqual(d.gain_loss_amount, -6000) + + def test_donation_ignores_sale_amount(self): + d = self.env['fusion.asset.disposal'].create({ + 'asset_id': self.asset.id, + 'disposal_type': 'donation', + 'sale_amount': 999, + 'book_value_at_disposal': 6000, + }) + self.assertEqual(d.gain_loss_amount, -6000)