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
This commit is contained in:
gsinghpal
2026-04-19 16:58:12 -04:00
parent b68d1b1c66
commit 5c89763191
6 changed files with 117 additions and 1 deletions

View File

@@ -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': """

View File

@@ -1,3 +1,4 @@
from . import fusion_asset_category
from . import fusion_asset
from . import fusion_asset_depreciation_line
from . import fusion_asset_disposal

View File

@@ -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

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 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
6 access_fusion_asset_category_user fusion.asset.category.user model_fusion_asset_category base.group_user 1 0 0 0
7 access_fusion_asset_category_admin fusion.asset.category.admin model_fusion_asset_category fusion_accounting_core.group_fusion_accounting_admin 1 1 1 1
8 access_fusion_asset_disposal_user fusion.asset.disposal.user model_fusion_asset_disposal base.group_user 1 0 0 0
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

View File

@@ -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

View File

@@ -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)