- fusion.asset: lifecycle (draft -> running -> paused -> disposed) - mail.thread + mail.activity.mixin tracking - 3 depreciation methods + 3 prorate conventions selections - monetary cost / salvage with check constraints (models.Constraint) - computed book_value, total_depreciated, last_posted_date - action_set_running / pause / resume / set_draft transitions - minimal stubs for fusion.asset.category and fusion.asset.depreciation.line so the One2many / Many2one comodels resolve at registry build time; expanded in Tasks 9 + 10 - 7 new tests (47 total) Made-with: Cursor
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""Fusion Asset model.
|
|
|
|
Lifecycle: draft -> running -> (paused -> running)* -> disposed.
|
|
- draft: created, not yet running depreciation
|
|
- running: depreciation board active, periodic posts happen
|
|
- paused: depreciation suspended (e.g. asset out for repair)
|
|
- disposed: sold/scrapped/donated; no further depreciation
|
|
"""
|
|
|
|
import logging
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
METHOD_SELECTION = [
|
|
('straight_line', 'Straight Line'),
|
|
('declining_balance', 'Declining Balance'),
|
|
('units_of_production', 'Units of Production'),
|
|
]
|
|
|
|
PRORATE_SELECTION = [
|
|
('full_month', 'Full Month'),
|
|
('days_365', 'Days / 365'),
|
|
('days_period', 'Days in Period'),
|
|
]
|
|
|
|
STATE_SELECTION = [
|
|
('draft', 'Draft'),
|
|
('running', 'Running'),
|
|
('paused', 'Paused'),
|
|
('disposed', 'Disposed'),
|
|
]
|
|
|
|
|
|
class FusionAsset(models.Model):
|
|
_name = "fusion.asset"
|
|
_description = "Fusion Fixed Asset"
|
|
_order = "acquisition_date desc, id desc"
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
|
|
name = fields.Char(required=True, tracking=True)
|
|
code = fields.Char(help="Internal asset code (e.g. tag number).")
|
|
company_id = fields.Many2one(
|
|
'res.company', required=True,
|
|
default=lambda self: self.env.company,
|
|
)
|
|
category_id = fields.Many2one('fusion.asset.category', tracking=True)
|
|
state = fields.Selection(
|
|
STATE_SELECTION, default='draft', required=True, tracking=True,
|
|
)
|
|
|
|
cost = fields.Monetary(
|
|
required=True, tracking=True,
|
|
help="Original acquisition cost.",
|
|
)
|
|
salvage_value = fields.Monetary(
|
|
default=0.0, tracking=True,
|
|
help="Estimated end-of-life value.",
|
|
)
|
|
acquisition_date = fields.Date(
|
|
required=True, default=fields.Date.today, tracking=True,
|
|
)
|
|
in_service_date = fields.Date(
|
|
tracking=True,
|
|
help="Date depreciation actually begins.",
|
|
)
|
|
disposed_date = fields.Date(readonly=True, tracking=True)
|
|
currency_id = fields.Many2one(
|
|
'res.currency', required=True,
|
|
default=lambda self: self.env.company.currency_id,
|
|
)
|
|
|
|
method = fields.Selection(
|
|
METHOD_SELECTION, required=True, default='straight_line', tracking=True,
|
|
)
|
|
useful_life_years = fields.Integer(
|
|
default=5, tracking=True,
|
|
help="For straight_line / declining_balance.",
|
|
)
|
|
declining_rate_pct = fields.Float(
|
|
default=20.0,
|
|
help="For declining_balance method, e.g. 20.0 = 20%/year.",
|
|
)
|
|
total_units_expected = fields.Float(
|
|
help="For units_of_production method.",
|
|
)
|
|
units_used_to_date = fields.Float(
|
|
default=0.0,
|
|
help="For units_of_production: track usage.",
|
|
)
|
|
prorate_convention = fields.Selection(
|
|
PRORATE_SELECTION, default='days_period', required=True,
|
|
)
|
|
|
|
source_invoice_line_id = fields.Many2one(
|
|
'account.move.line', string='Source Invoice Line',
|
|
help="The invoice line that originated this asset.",
|
|
)
|
|
parent_id = fields.Many2one(
|
|
'fusion.asset', help='For partial-sale child assets.',
|
|
)
|
|
|
|
depreciation_line_ids = fields.One2many(
|
|
'fusion.asset.depreciation.line', 'asset_id',
|
|
string='Depreciation Lines',
|
|
)
|
|
book_value = fields.Monetary(compute='_compute_book_value', store=True)
|
|
total_depreciated = fields.Monetary(compute='_compute_book_value', store=True)
|
|
last_posted_date = fields.Date(compute='_compute_last_posted_date', store=True)
|
|
|
|
@api.depends('cost', 'depreciation_line_ids.amount', 'depreciation_line_ids.is_posted')
|
|
def _compute_book_value(self):
|
|
for asset in self:
|
|
posted = sum(l.amount for l in asset.depreciation_line_ids if l.is_posted)
|
|
asset.total_depreciated = posted
|
|
asset.book_value = asset.cost - posted
|
|
|
|
@api.depends('depreciation_line_ids.is_posted', 'depreciation_line_ids.scheduled_date')
|
|
def _compute_last_posted_date(self):
|
|
for asset in self:
|
|
posted_dates = [
|
|
l.scheduled_date for l in asset.depreciation_line_ids if l.is_posted
|
|
]
|
|
asset.last_posted_date = max(posted_dates) if posted_dates else False
|
|
|
|
def action_set_running(self):
|
|
for asset in self:
|
|
if asset.state != 'draft':
|
|
raise ValidationError(_("Only draft assets can be set running."))
|
|
if not asset.in_service_date:
|
|
asset.in_service_date = fields.Date.today()
|
|
asset.state = 'running'
|
|
|
|
def action_pause(self):
|
|
for asset in self:
|
|
if asset.state != 'running':
|
|
raise ValidationError(_("Only running assets can be paused."))
|
|
asset.state = 'paused'
|
|
|
|
def action_resume(self):
|
|
for asset in self:
|
|
if asset.state != 'paused':
|
|
raise ValidationError(_("Only paused assets can be resumed."))
|
|
asset.state = 'running'
|
|
|
|
def action_set_draft(self):
|
|
for asset in self:
|
|
if asset.state not in ('draft', 'paused'):
|
|
raise ValidationError(
|
|
_("Cannot reset to draft from %s.") % asset.state,
|
|
)
|
|
asset.state = 'draft'
|
|
|
|
_check_cost_positive = models.Constraint(
|
|
'CHECK(cost >= 0)',
|
|
'Asset cost must be non-negative.',
|
|
)
|
|
_check_salvage_lte_cost = models.Constraint(
|
|
'CHECK(salvage_value >= 0 AND salvage_value <= cost)',
|
|
'Salvage value must be between 0 and cost.',
|
|
)
|