Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,650 @@
# Fusion Accounting - Asset Modification Wizard
# Enables users to modify, dispose, sell, pause, or resume
# fixed assets with full depreciation board recalculation.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
from odoo.tools import float_is_zero
from odoo.tools.misc import format_date
class AssetModify(models.TransientModel):
"""Wizard for modifying running assets, including value changes,
depreciation parameter adjustments, disposal, and sales."""
_name = 'asset.modify'
_description = 'Modify Asset'
# --- Core Fields ---
name = fields.Text(string='Note')
asset_id = fields.Many2one(
string="Asset",
comodel_name='account.asset',
required=True,
help="The target asset to modify.",
ondelete="cascade",
)
currency_id = fields.Many2one(
related='asset_id.currency_id',
)
company_id = fields.Many2one(
comodel_name='res.company',
related='asset_id.company_id',
)
date = fields.Date(
default=lambda self: fields.Date.today(),
string='Date',
)
# --- Depreciation Parameters ---
method_number = fields.Integer(
string='Duration',
required=True,
)
method_period = fields.Selection(
selection=[('1', 'Months'), ('12', 'Years')],
string='Number of Months in a Period',
help="Interval between successive depreciation entries.",
)
value_residual = fields.Monetary(
string="Depreciable Amount",
help="Updated depreciable amount for the asset.",
compute="_compute_value_residual",
store=True,
readonly=False,
)
salvage_value = fields.Monetary(
string="Not Depreciable Amount",
help="Updated non-depreciable (salvage) amount.",
)
# --- Action Selection ---
modify_action = fields.Selection(
selection="_get_selection_modify_options",
string="Action",
)
# --- Gross Increase Accounts ---
account_asset_id = fields.Many2one(
comodel_name='account.account',
string="Gross Increase Account",
check_company=True,
domain="[]",
)
account_asset_counterpart_id = fields.Many2one(
comodel_name='account.account',
check_company=True,
domain="[]",
string="Asset Counterpart Account",
)
account_depreciation_id = fields.Many2one(
comodel_name='account.account',
check_company=True,
domain="[]",
string="Depreciation Account",
)
account_depreciation_expense_id = fields.Many2one(
comodel_name='account.account',
check_company=True,
domain="[]",
string="Expense Account",
)
# --- Sale / Disposal Fields ---
invoice_ids = fields.Many2many(
comodel_name='account.move',
string="Customer Invoice",
check_company=True,
domain="[('move_type', '=', 'out_invoice'), ('state', '=', 'posted')]",
help="Invoice(s) linked to this disposal or sale.",
)
invoice_line_ids = fields.Many2many(
comodel_name='account.move.line',
check_company=True,
domain="[('move_id', '=', invoice_id), ('display_type', '=', 'product')]",
help="Specific invoice line(s) related to this asset.",
)
select_invoice_line_id = fields.Boolean(
compute="_compute_select_invoice_line_id",
)
gain_value = fields.Boolean(
compute="_compute_gain_value",
)
# --- Gain/Loss Accounts ---
gain_account_id = fields.Many2one(
comodel_name='account.account',
check_company=True,
domain="[]",
compute="_compute_accounts",
inverse="_inverse_gain_account",
readonly=False,
compute_sudo=True,
help="Account for recording gains on asset disposal.",
)
loss_account_id = fields.Many2one(
comodel_name='account.account',
check_company=True,
domain="[]",
compute="_compute_accounts",
inverse="_inverse_loss_account",
readonly=False,
compute_sudo=True,
help="Account for recording losses on asset disposal.",
)
# --- Informational ---
informational_text = fields.Html(
compute='_compute_informational_text',
)
gain_or_loss = fields.Selection(
selection=[('gain', 'Gain'), ('loss', 'Loss'), ('no', 'No')],
compute='_compute_gain_or_loss',
)
# -------------------------------------------------------------------------
# Selection Helpers
# -------------------------------------------------------------------------
def _compute_modify_action(self):
"""Determine the default action based on context."""
if self.env.context.get('resume_after_pause'):
return 'resume'
return 'dispose'
@api.depends('asset_id')
def _get_selection_modify_options(self):
"""Return available modification actions."""
if self.env.context.get('resume_after_pause'):
return [('resume', _('Resume'))]
return [
('dispose', _("Dispose")),
('sell', _("Sell")),
('modify', _("Re-evaluate")),
('pause', _("Pause")),
]
# -------------------------------------------------------------------------
# Compute Methods
# -------------------------------------------------------------------------
@api.depends('company_id')
def _compute_accounts(self):
for rec in self:
rec.gain_account_id = rec.company_id.gain_account_id
rec.loss_account_id = rec.company_id.loss_account_id
@api.depends('date')
def _compute_value_residual(self):
for rec in self:
rec.value_residual = rec.asset_id._get_residual_value_at_date(rec.date)
def _inverse_gain_account(self):
for rec in self:
rec.company_id.sudo().gain_account_id = rec.gain_account_id
def _inverse_loss_account(self):
for rec in self:
rec.company_id.sudo().loss_account_id = rec.loss_account_id
@api.depends('asset_id', 'invoice_ids', 'invoice_line_ids', 'modify_action', 'date')
def _compute_gain_or_loss(self):
"""Determine whether disposing/selling results in a gain or loss."""
for rec in self:
invoice_total = abs(sum(
ln.balance for ln in rec.invoice_line_ids
))
book_val = rec.asset_id._get_own_book_value(rec.date)
cmp_result = rec.company_id.currency_id.compare_amounts(
book_val, invoice_total,
)
if rec.modify_action in ('sell', 'dispose') and cmp_result < 0:
rec.gain_or_loss = 'gain'
elif rec.modify_action in ('sell', 'dispose') and cmp_result > 0:
rec.gain_or_loss = 'loss'
else:
rec.gain_or_loss = 'no'
@api.depends('asset_id', 'value_residual', 'salvage_value')
def _compute_gain_value(self):
"""Check whether the modification increases the asset's book value."""
for rec in self:
rec.gain_value = rec.currency_id.compare_amounts(
rec._get_own_book_value(),
rec.asset_id._get_own_book_value(rec.date),
) > 0
@api.depends('loss_account_id', 'gain_account_id', 'gain_or_loss',
'modify_action', 'date', 'value_residual', 'salvage_value')
def _compute_informational_text(self):
"""Generate user-facing description of what will happen."""
for wiz in self:
formatted_date = format_date(self.env, wiz.date)
if wiz.modify_action == 'dispose':
acct_name, result_label = '', _('gain/loss')
if wiz.gain_or_loss == 'gain':
acct_name = wiz.gain_account_id.display_name or ''
result_label = _('gain')
elif wiz.gain_or_loss == 'loss':
acct_name = wiz.loss_account_id.display_name or ''
result_label = _('loss')
wiz.informational_text = _(
"Depreciation will be posted through %(date)s."
"<br/> A disposal entry will go to the %(account_type)s "
"account <b>%(account)s</b>.",
date=formatted_date,
account_type=result_label,
account=acct_name,
)
elif wiz.modify_action == 'sell':
acct_name = ''
if wiz.gain_or_loss == 'gain':
acct_name = wiz.gain_account_id.display_name or ''
elif wiz.gain_or_loss == 'loss':
acct_name = wiz.loss_account_id.display_name or ''
wiz.informational_text = _(
"Depreciation will be posted through %(date)s."
"<br/> A secondary entry will neutralize the original "
"revenue and post the sale outcome to account "
"<b>%(account)s</b>.",
date=formatted_date,
account=acct_name,
)
elif wiz.modify_action == 'pause':
wiz.informational_text = _(
"Depreciation will be posted through %s.",
formatted_date,
)
elif wiz.modify_action == 'modify':
increase_note = (
_("A child asset will be created for the value increase. <br/>")
if wiz.gain_value else ""
)
wiz.informational_text = _(
"Depreciation will be posted through %(date)s. <br/> "
"%(extra_text)s Future entries will be recalculated to "
"reflect the updated parameters.",
date=formatted_date,
extra_text=increase_note,
)
else:
# Resume or other
increase_note = (
_("A child asset will be created for the value increase. <br/>")
if wiz.gain_value else ""
)
wiz.informational_text = _(
"%s Future entries will be recalculated to reflect "
"the updated parameters.",
increase_note,
)
@api.depends('invoice_ids', 'modify_action')
def _compute_select_invoice_line_id(self):
for rec in self:
rec.select_invoice_line_id = (
rec.modify_action == 'sell'
and len(rec.invoice_ids.invoice_line_ids) > 1
)
# -------------------------------------------------------------------------
# Onchange
# -------------------------------------------------------------------------
@api.onchange('modify_action')
def _onchange_action(self):
if (
self.modify_action == 'sell'
and self.asset_id.children_ids.filtered(
lambda child: child.state in ('draft', 'open')
or child.value_residual > 0
)
):
raise UserError(_(
"Cannot automate the sale journal entry for an asset "
"with active gross increases. Please dispose of the "
"increase(s) first."
))
if self.modify_action not in ('modify', 'resume'):
self.write({
'value_residual': self.asset_id._get_residual_value_at_date(self.date),
'salvage_value': self.asset_id.salvage_value,
})
@api.onchange('invoice_ids')
def _onchange_invoice_ids(self):
"""Keep invoice_line_ids in sync when invoices change."""
valid_line_ids = self.invoice_ids.invoice_line_ids.filtered(
lambda ln: ln._origin.id in self.invoice_line_ids.ids,
)
self.invoice_line_ids = valid_line_ids
# Auto-select lines for single-line invoices
for inv in self.invoice_ids.filtered(lambda i: len(i.invoice_line_ids) == 1):
self.invoice_line_ids += inv.invoice_line_ids
# -------------------------------------------------------------------------
# CRUD Override
# -------------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
"""Populate defaults from the linked asset when not provided."""
AssetModel = self.env['account.asset']
for vals in vals_list:
if 'asset_id' not in vals:
continue
asset = AssetModel.browse(vals['asset_id'])
# Block if future posted depreciation exists
future_posted = asset.depreciation_move_ids.filtered(
lambda mv: (
mv.state == 'posted'
and not mv.reversal_move_ids
and mv.date > fields.Date.today()
)
)
if future_posted:
raise UserError(_(
'Please reverse any future-dated depreciation entries '
'before modifying this asset.'
))
# Fill in missing defaults from the asset
field_defaults = {
'method_number': asset.method_number,
'method_period': asset.method_period,
'salvage_value': asset.salvage_value,
'account_asset_id': asset.account_asset_id.id,
'account_depreciation_id': asset.account_depreciation_id.id,
'account_depreciation_expense_id': asset.account_depreciation_expense_id.id,
}
for fld, default_val in field_defaults.items():
if fld not in vals:
vals[fld] = default_val
return super().create(vals_list)
# -------------------------------------------------------------------------
# Main Business Actions
# -------------------------------------------------------------------------
def modify(self):
"""Re-evaluate the asset: update depreciation parameters,
handle value increases/decreases, and recompute the schedule."""
lock_threshold = self.asset_id.company_id._get_user_fiscal_lock_date(
self.asset_id.journal_id,
)
if self.date <= lock_threshold:
raise UserError(_(
"The selected date is on or before the fiscal lock date. "
"Re-evaluation is not permitted."
))
# Snapshot current values for change tracking
prior_vals = {
'method_number': self.asset_id.method_number,
'method_period': self.asset_id.method_period,
'value_residual': self.asset_id.value_residual,
'salvage_value': self.asset_id.salvage_value,
}
updated_vals = {
'method_number': self.method_number,
'method_period': self.method_period,
'salvage_value': self.salvage_value,
'account_asset_id': self.account_asset_id,
'account_depreciation_id': self.account_depreciation_id,
'account_depreciation_expense_id': self.account_depreciation_expense_id,
}
# Handle resume-after-pause scenario
is_resuming = self.env.context.get('resume_after_pause')
if is_resuming:
latest_depr = self.asset_id.depreciation_move_ids
last_depr_date = (
max(latest_depr, key=lambda m: m.date).date
if latest_depr else self.asset_id.acquisition_date
)
gap_days = self.asset_id._get_delta_days(last_depr_date, self.date) - 1
if self.currency_id.compare_amounts(gap_days, 0) < 0:
raise UserError(_(
"Resume date must be after the pause date."
))
updated_vals['asset_paused_days'] = (
self.asset_id.asset_paused_days + gap_days
)
updated_vals['state'] = 'open'
self.asset_id.message_post(
body=_("Asset resumed. %s", self.name),
)
# Compute value changes
current_book = self.asset_id._get_own_book_value(self.date)
target_book = self._get_own_book_value()
book_increase = target_book - current_book
new_residual, new_salvage = self._get_new_asset_values(current_book)
residual_diff = max(0, self.value_residual - new_residual)
salvage_diff = max(0, self.salvage_value - new_salvage)
# Create depreciation up to modification date (unless resuming)
if not is_resuming:
draft_before = self.env['account.move'].search_count([
('asset_id', '=', self.asset_id.id),
('state', '=', 'draft'),
('date', '<=', self.date),
], limit=1)
if draft_before:
raise UserError(_(
'Unposted depreciation entries exist before the '
'selected date. Please process them first.'
))
self.asset_id._create_move_before_date(self.date)
updated_vals['salvage_value'] = new_salvage
# Detect if child assets need recomputation
schedule_changed = (
updated_vals['method_number'] != self.asset_id.method_number
or updated_vals['method_period'] != self.asset_id.method_period
or (
updated_vals.get('asset_paused_days')
and not float_is_zero(
updated_vals['asset_paused_days']
- self.asset_id.asset_paused_days, 8,
)
)
)
self.asset_id.write(updated_vals)
# Create gross increase asset if value went up
total_increase = residual_diff + salvage_diff
if self.currency_id.compare_amounts(total_increase, 0) > 0:
increase_date = self.date + relativedelta(days=1)
increase_entry = self.env['account.move'].create({
'journal_id': self.asset_id.journal_id.id,
'date': increase_date,
'move_type': 'entry',
'asset_move_type': 'positive_revaluation',
'line_ids': [
Command.create({
'account_id': self.account_asset_id.id,
'debit': total_increase,
'credit': 0,
'name': _(
'Value increase for: %(asset)s',
asset=self.asset_id.name,
),
}),
Command.create({
'account_id': self.account_asset_counterpart_id.id,
'debit': 0,
'credit': total_increase,
'name': _(
'Value increase for: %(asset)s',
asset=self.asset_id.name,
),
}),
],
})
increase_entry._post()
child_asset = self.env['account.asset'].create({
'name': (
self.asset_id.name + ': ' + self.name
if self.name else ""
),
'currency_id': self.asset_id.currency_id.id,
'company_id': self.asset_id.company_id.id,
'method': self.asset_id.method,
'method_number': self.method_number,
'method_period': self.method_period,
'method_progress_factor': self.asset_id.method_progress_factor,
'acquisition_date': increase_date,
'value_residual': residual_diff,
'salvage_value': salvage_diff,
'prorata_date': increase_date,
'prorata_computation_type': (
'daily_computation'
if self.asset_id.prorata_computation_type == 'daily_computation'
else 'constant_periods'
),
'original_value': self._get_increase_original_value(
residual_diff, salvage_diff,
),
'account_asset_id': self.account_asset_id.id,
'account_depreciation_id': self.account_depreciation_id.id,
'account_depreciation_expense_id': self.account_depreciation_expense_id.id,
'journal_id': self.asset_id.journal_id.id,
'parent_id': self.asset_id.id,
'original_move_line_ids': [
(6, 0, increase_entry.line_ids.filtered(
lambda ln: ln.account_id == self.account_asset_id,
).ids),
],
})
child_asset.validate()
link_html = child_asset._get_html_link()
self.asset_id.message_post(
body=_('Gross increase created: %(link)s', link=link_html),
)
# Create negative revaluation entry if value went down
if self.currency_id.compare_amounts(book_increase, 0) < 0:
depr_vals = self.env['account.move']._prepare_move_for_asset_depreciation({
'amount': -book_increase,
'asset_id': self.asset_id,
'move_ref': _(
'Value decrease for: %(asset)s', asset=self.asset_id.name,
),
'depreciation_beginning_date': self.date,
'depreciation_end_date': self.date,
'date': self.date,
'asset_number_days': 0,
'asset_value_change': True,
'asset_move_type': 'negative_revaluation',
})
self.env['account.move'].create(depr_vals)._post()
# Recompute depreciation schedule
board_start = self.date if is_resuming else self.date + relativedelta(days=1)
if self.asset_id.depreciation_move_ids:
self.asset_id.compute_depreciation_board(board_start)
else:
self.asset_id.compute_depreciation_board()
# Propagate changes to child assets if schedule params changed
if schedule_changed:
child_assets = self.asset_id.children_ids
child_assets.write({
'method_number': updated_vals['method_number'],
'method_period': updated_vals['method_period'],
'asset_paused_days': self.asset_id.asset_paused_days,
})
for child in child_assets:
if not is_resuming:
child._create_move_before_date(self.date)
if child.depreciation_move_ids:
child.compute_depreciation_board(board_start)
else:
child.compute_depreciation_board()
child._check_depreciations()
child.depreciation_move_ids.filtered(
lambda mv: mv.state != 'posted',
)._post()
# Log tracked changes in the chatter
tracked_fields = self.env['account.asset'].fields_get(prior_vals.keys())
changes, tracking_vals = self.asset_id._mail_track(
tracked_fields, prior_vals,
)
if changes:
self.asset_id.message_post(
body=_('Depreciation board modified %s', self.name),
tracking_value_ids=tracking_vals,
)
self.asset_id._check_depreciations()
self.asset_id.depreciation_move_ids.filtered(
lambda mv: mv.state != 'posted',
)._post()
return {'type': 'ir.actions.act_window_close'}
def pause(self):
"""Pause depreciation for the asset."""
for rec in self:
rec.asset_id.pause(pause_date=rec.date, message=self.name)
def sell_dispose(self):
"""Dispose of or sell the asset, generating closing entries."""
self.ensure_one()
if (
self.gain_account_id == self.asset_id.account_depreciation_id
or self.loss_account_id == self.asset_id.account_depreciation_id
):
raise UserError(_(
"The gain/loss account cannot be the same as the "
"Depreciation Account."
))
disposal_lines = (
self.env['account.move.line']
if self.modify_action == 'dispose'
else self.invoice_line_ids
)
return self.asset_id.set_to_close(
invoice_line_ids=disposal_lines,
date=self.date,
message=self.name,
)
# -------------------------------------------------------------------------
# Utility Methods
# -------------------------------------------------------------------------
def _get_own_book_value(self):
"""Return the wizard's configured book value (residual + salvage)."""
return self.value_residual + self.salvage_value
def _get_increase_original_value(self, residual_increase, salvage_increase):
"""Compute the original value for a gross increase child asset."""
return residual_increase + salvage_increase
def _get_new_asset_values(self, current_asset_book):
"""Calculate capped residual and salvage values to ensure
they don't exceed the current book value."""
self.ensure_one()
capped_residual = min(
current_asset_book - min(self.salvage_value, self.asset_id.salvage_value),
self.value_residual,
)
capped_salvage = min(
current_asset_book - capped_residual,
self.salvage_value,
)
return capped_residual, capped_salvage