# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import psycopg2
import datetime
from dateutil.relativedelta import relativedelta
from markupsafe import Markup
from math import copysign
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero, formatLang, end_of
DAYS_PER_MONTH = 30
DAYS_PER_YEAR = DAYS_PER_MONTH * 12
class AccountAsset(models.Model):
_name = 'account.asset'
_description = 'Asset/Revenue Recognition'
_inherit = ['mail.thread', 'mail.activity.mixin', 'analytic.mixin']
depreciation_entries_count = fields.Integer(compute='_compute_counts', string='# Posted Depreciation Entries')
gross_increase_count = fields.Integer(compute='_compute_counts', string='# Gross Increases', help="Number of assets made to increase the value of the asset")
total_depreciation_entries_count = fields.Integer(compute='_compute_counts', string='# Depreciation Entries', help="Number of depreciation entries (posted or not)")
name = fields.Char(string='Asset Name', compute='_compute_name', store=True, required=True, readonly=False, tracking=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', store=True)
state = fields.Selection(
selection=[('model', 'Model'),
('draft', 'Draft'),
('open', 'Running'),
('paused', 'On Hold'),
('close', 'Closed'),
('cancelled', 'Cancelled')],
string='Status',
copy=False,
default='draft',
readonly=True,
help="When an asset is created, the status is 'Draft'.\n"
"If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n"
"The 'On Hold' status can be set manually when you want to pause the depreciation of an asset for some time.\n"
"You can manually close an asset when the depreciation is over.\n"
"By cancelling an asset, all depreciation entries will be reversed")
active = fields.Boolean(default=True)
# Depreciation params
method = fields.Selection(
selection=[
('linear', 'Straight Line'),
('degressive', 'Declining'),
('degressive_then_linear', 'Declining then Straight Line')
],
string='Method',
default='linear',
help="Choose the method to use to compute the amount of depreciation lines.\n"
" * Straight Line: Calculated on basis of: Gross Value / Duration\n"
" * Declining: Calculated on basis of: Residual Value * Declining Factor\n"
" * Declining then Straight Line: Like Declining but with a minimum depreciation value equal to the straight line value."
)
method_number = fields.Integer(string='Duration', default=5, help="The number of depreciations needed to depreciate your asset")
method_period = fields.Selection([('1', 'Months'), ('12', 'Years')], string='Number of Months in a Period', default='12',
help="The amount of time between two depreciations")
method_progress_factor = fields.Float(string='Declining Factor', default=0.3)
prorata_computation_type = fields.Selection(
selection=[
('none', 'No Prorata'),
('constant_periods', 'Constant Periods'),
('daily_computation', 'Based on days per period'),
],
string="Computation",
required=True, default='constant_periods',
)
prorata_date = fields.Date(
string='Prorata Date',
compute='_compute_prorata_date', store=True, readonly=False,
help='Starting date of the period used in the prorata calculation of the first depreciation',
required=True, precompute=True,
copy=True,
)
paused_prorata_date = fields.Date(compute='_compute_paused_prorata_date') # number of days to shift the computation of future deprecations
account_asset_id = fields.Many2one(
'account.account',
string='Fixed Asset Account',
compute='_compute_account_asset_id',
help="Account used to record the purchase of the asset at its original price.",
store=True, readonly=False,
check_company=True,
domain="[('account_type', '!=', 'off_balance')]",
)
account_depreciation_id = fields.Many2one(
comodel_name='account.account',
string='Depreciation Account',
check_company=True,
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False)]",
help="Account used in the depreciation entries, to decrease the asset value."
)
account_depreciation_expense_id = fields.Many2one(
comodel_name='account.account',
string='Expense Account',
check_company=True,
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False)]",
help="Account used in the periodical entries, to record a part of the asset as expense.",
)
journal_id = fields.Many2one(
'account.journal',
string='Journal',
check_company=True,
domain="[('type', '=', 'general')]",
compute='_compute_journal_id', store=True, readonly=False,
)
# Values
original_value = fields.Monetary(string="Original Value", compute='_compute_value', store=True, readonly=False)
book_value = fields.Monetary(string='Book Value', readonly=True, compute='_compute_book_value', recursive=True, store=True, help="Sum of the depreciable value, the salvage value and the book value of all value increase items")
value_residual = fields.Monetary(string='Depreciable Value', compute='_compute_value_residual')
salvage_value = fields.Monetary(string='Not Depreciable Value',
help="It is the amount you plan to have that you cannot depreciate.")
total_depreciable_value = fields.Monetary(compute='_compute_total_depreciable_value')
gross_increase_value = fields.Monetary(string="Gross Increase Value", compute="_compute_gross_increase_value", compute_sudo=True)
non_deductible_tax_value = fields.Monetary(string="Non Deductible Tax Value", compute="_compute_non_deductible_tax_value", store=True, readonly=True)
related_purchase_value = fields.Monetary(compute='_compute_related_purchase_value')
# Links with entries
depreciation_move_ids = fields.One2many('account.move', 'asset_id', string='Depreciation Lines')
original_move_line_ids = fields.Many2many('account.move.line', 'asset_move_line_rel', 'asset_id', 'line_id', string='Journal Items', copy=False)
# Dates
acquisition_date = fields.Date(
compute='_compute_acquisition_date', store=True, precompute=True,
readonly=False,
copy=True,
)
disposal_date = fields.Date(readonly=False, compute="_compute_disposal_date", store=True)
# model-related fields
model_id = fields.Many2one('account.asset', string='Model', change_default=True, domain="[('company_id', '=', company_id)]")
account_type = fields.Selection(string="Type of the account", related='account_asset_id.account_type')
display_account_asset_id = fields.Boolean(compute="_compute_display_account_asset_id")
# Capital gain
parent_id = fields.Many2one('account.asset', help="An asset has a parent when it is the result of gaining value")
children_ids = fields.One2many('account.asset', 'parent_id', help="The children are the gains in value of this asset")
# Adapt for import fields
already_depreciated_amount_import = fields.Monetary(
help="In case of an import from another software, you might need to use this field to have the right "
"depreciation table report. This is the value that was already depreciated with entries not computed from this model",
)
asset_lifetime_days = fields.Float(compute="_compute_lifetime_days", recursive=True) # total number of days to consider for the computation of an asset depreciation board
asset_paused_days = fields.Float(copy=False)
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('company_id')
def _compute_journal_id(self):
for asset in self:
if asset.journal_id and asset.journal_id.company_id == asset.company_id:
asset.journal_id = asset.journal_id
else:
asset.journal_id = self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(asset.company_id),
('type', '=', 'general'),
], limit=1)
@api.depends('salvage_value', 'original_value')
def _compute_total_depreciable_value(self):
for asset in self:
asset.total_depreciable_value = asset.original_value - asset.salvage_value
@api.depends('depreciation_move_ids.date', 'state')
def _compute_disposal_date(self):
for asset in self:
if asset.state == 'close':
dates = asset.depreciation_move_ids.filtered(lambda m: m.date).mapped('date')
asset.disposal_date = dates and max(dates)
else:
asset.disposal_date = False
@api.depends('original_move_line_ids', 'original_move_line_ids.account_id', 'non_deductible_tax_value')
def _compute_value(self):
for record in self:
if not record.original_move_line_ids:
record.original_value = record.original_value or False
continue
if any(line.move_id.state == 'draft' for line in record.original_move_line_ids):
raise UserError(_("All the lines should be posted"))
record.original_value = record.related_purchase_value
if record.non_deductible_tax_value:
record.original_value += record.non_deductible_tax_value
@api.depends('original_move_line_ids')
@api.depends_context('form_view_ref')
def _compute_display_account_asset_id(self):
for record in self:
# Hide the field when creating an asset model from the CoA. (form_view_ref is set from there)
model_from_coa = self.env.context.get('form_view_ref') and record.state == 'model'
record.display_account_asset_id = not record.original_move_line_ids and not model_from_coa
@api.depends('account_depreciation_id', 'account_depreciation_expense_id', 'original_move_line_ids')
def _compute_account_asset_id(self):
for record in self:
if record.original_move_line_ids:
if len(record.original_move_line_ids.account_id) > 1:
raise UserError(_("All the lines should be from the same account"))
record.account_asset_id = record.original_move_line_ids.account_id
if not record.account_asset_id:
# Only set a default value, do not erase user inputs
record._onchange_account_depreciation_id()
@api.depends('original_move_line_ids')
def _compute_analytic_distribution(self):
for asset in self:
distribution_asset = {}
amount_total = sum(asset.original_move_line_ids.mapped("balance"))
if not float_is_zero(amount_total, precision_rounding=asset.currency_id.rounding):
for line in asset.original_move_line_ids._origin:
if line.analytic_distribution:
for account, distribution in line.analytic_distribution.items():
distribution_asset[account] = distribution_asset.get(account, 0) + distribution * line.balance
for account, distribution_amount in distribution_asset.items():
distribution_asset[account] = distribution_amount / amount_total
asset.analytic_distribution = distribution_asset if distribution_asset else asset.analytic_distribution
@api.depends('method_number', 'method_period', 'prorata_computation_type')
def _compute_lifetime_days(self):
for asset in self:
if not asset.parent_id:
if asset.prorata_computation_type == 'daily_computation':
asset.asset_lifetime_days = (asset.prorata_date + relativedelta(months=int(asset.method_period) * asset.method_number) - asset.prorata_date).days
else:
asset.asset_lifetime_days = int(asset.method_period) * asset.method_number * DAYS_PER_MONTH
else:
# if it has a parent, we want the asset to only depreciate on the remaining days left of the parent
if asset.prorata_computation_type == 'daily_computation':
parent_end_date = asset.parent_id.paused_prorata_date + relativedelta(days=int(asset.parent_id.asset_lifetime_days - 1))
else:
parent_end_date = asset.parent_id.paused_prorata_date + relativedelta(
months=int(asset.parent_id.asset_lifetime_days / DAYS_PER_MONTH),
days=int(asset.parent_id.asset_lifetime_days % DAYS_PER_MONTH) - 1
)
asset.asset_lifetime_days = asset._get_delta_days(asset.prorata_date, parent_end_date)
@api.depends('acquisition_date', 'company_id', 'prorata_computation_type')
def _compute_prorata_date(self):
for asset in self:
if asset.prorata_computation_type == 'none' and asset.acquisition_date:
fiscalyear_date = asset.company_id.compute_fiscalyear_dates(asset.acquisition_date).get('date_from')
asset.prorata_date = fiscalyear_date
else:
asset.prorata_date = asset.acquisition_date
@api.depends('prorata_date', 'prorata_computation_type', 'asset_paused_days')
def _compute_paused_prorata_date(self):
for asset in self:
if asset.prorata_computation_type == 'daily_computation':
asset.paused_prorata_date = asset.prorata_date + relativedelta(days=asset.asset_paused_days)
else:
asset.paused_prorata_date = asset.prorata_date + relativedelta(
months=int(asset.asset_paused_days / DAYS_PER_MONTH),
days=asset.asset_paused_days % DAYS_PER_MONTH
)
@api.depends('original_move_line_ids')
def _compute_related_purchase_value(self):
for asset in self:
related_purchase_value = sum(asset.original_move_line_ids.mapped('balance'))
if asset.account_asset_id.multiple_assets_per_line and len(asset.original_move_line_ids) == 1:
related_purchase_value /= max(1, int(asset.original_move_line_ids.quantity))
asset.related_purchase_value = related_purchase_value
@api.depends('original_move_line_ids')
def _compute_acquisition_date(self):
for asset in self:
asset.acquisition_date = asset.acquisition_date or min(
[(aml.invoice_date or aml.date) for aml in asset.original_move_line_ids] + [fields.Date.today()]
)
@api.depends('original_move_line_ids')
def _compute_name(self):
for record in self:
record.name = record.name or (record.original_move_line_ids and record.original_move_line_ids[0].name or '')
@api.depends(
'original_value', 'salvage_value', 'already_depreciated_amount_import',
'depreciation_move_ids.state',
'depreciation_move_ids.depreciation_value',
'depreciation_move_ids.reversal_move_id'
)
def _compute_value_residual(self):
for record in self:
posted_depreciation_moves = record.depreciation_move_ids.filtered(lambda mv: mv.state == 'posted')
record.value_residual = (
record.original_value
- record.salvage_value
- record.already_depreciated_amount_import
- sum(posted_depreciation_moves.mapped('depreciation_value'))
)
@api.depends('value_residual', 'salvage_value', 'children_ids.book_value')
def _compute_book_value(self):
for record in self:
record.book_value = record.value_residual + record.salvage_value + sum(record.children_ids.mapped('book_value'))
if record.state == 'close' and all(move.state == 'posted' for move in record.depreciation_move_ids):
record.book_value -= record.salvage_value
@api.depends('children_ids.original_value')
def _compute_gross_increase_value(self):
for record in self:
record.gross_increase_value = sum(record.children_ids.mapped('original_value'))
@api.depends('original_move_line_ids')
def _compute_non_deductible_tax_value(self):
for record in self:
record.non_deductible_tax_value = 0.0
for line in record.original_move_line_ids:
if line.non_deductible_tax_value:
account = line.account_id
auto_create_multi = account.create_asset != 'no' and account.multiple_assets_per_line
quantity = line.quantity if auto_create_multi else 1
converted_non_deductible_tax_value = line.currency_id._convert(line.non_deductible_tax_value / quantity, record.currency_id, record.company_id, line.date)
record.non_deductible_tax_value += record.currency_id.round(converted_non_deductible_tax_value)
@api.depends('depreciation_move_ids.state', 'parent_id')
def _compute_counts(self):
depreciation_per_asset = {
group.id: count
for group, count in self.env['account.move']._read_group(
domain=[
('asset_id', 'in', self.ids),
('state', '=', 'posted'),
],
groupby=['asset_id'],
aggregates=['__count'],
)
}
for asset in self:
asset.depreciation_entries_count = depreciation_per_asset.get(asset.id, 0)
asset.total_depreciation_entries_count = len(asset.depreciation_move_ids)
asset.gross_increase_count = len(asset.children_ids)
# -------------------------------------------------------------------------
# ONCHANGE METHODS
# -------------------------------------------------------------------------
@api.onchange('account_depreciation_id')
def _onchange_account_depreciation_id(self):
if not self.original_move_line_ids:
if not self.account_asset_id and self.state != 'model':
# Only set a default value since it is visible in the form
self.account_asset_id = self.account_depreciation_id
@api.onchange('original_value', 'original_move_line_ids')
def _display_original_value_warning(self):
if self.original_move_line_ids:
computed_original_value = self.related_purchase_value + self.non_deductible_tax_value
if self.original_value != computed_original_value:
warning = {
'title': _("Warning for the Original Value of %s", self.name),
'message': _("The amount you have entered (%s) does not match the Related Purchase's value (%s). "
"Please make sure this is what you want.",
formatLang(self.env, self.original_value, currency_obj=self.currency_id),
formatLang(self.env, computed_original_value, currency_obj=self.currency_id))
}
return {'warning': warning}
@api.onchange('original_move_line_ids')
def _onchange_original_move_line_ids(self):
# Force the recompute
self.acquisition_date = False
self._compute_acquisition_date()
@api.onchange('account_asset_id')
def _onchange_account_asset_id(self):
self.account_depreciation_id = self.account_depreciation_id or self.account_asset_id
@api.onchange('model_id')
def _onchange_model_id(self):
model = self.model_id
if model:
self.method = model.method
self.method_number = model.method_number
self.method_period = model.method_period
self.method_progress_factor = model.method_progress_factor
self.prorata_computation_type = model.prorata_computation_type
self.analytic_distribution = model.analytic_distribution or self.analytic_distribution
self.account_asset_id = model.account_asset_id
self.account_depreciation_id = model.account_depreciation_id
self.account_depreciation_expense_id = model.account_depreciation_expense_id
self.journal_id = model.journal_id
@api.onchange('original_value', 'salvage_value', 'acquisition_date', 'method', 'method_progress_factor', 'method_period',
'method_number', 'prorata_computation_type', 'already_depreciated_amount_import', 'prorata_date',)
def onchange_consistent_board(self):
""" When changing the fields that should change the values of the entries, we unlink the entries, so the
depreciation board is not inconsistent with the values of the asset"""
self.write(
{'depreciation_move_ids': [Command.set([])]}
)
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@api.constrains('active', 'state')
def _check_active(self):
for record in self:
if not record.active and record.state not in ('close', 'model'):
raise UserError(_('You cannot archive a record that is not closed'))
@api.constrains('depreciation_move_ids')
def _check_depreciations(self):
for asset in self:
if (
asset.state == 'open'
and asset.depreciation_move_ids
and not asset.currency_id.is_zero(
asset.depreciation_move_ids.sorted(lambda x: (x.date, x.id))[-1].asset_remaining_value
)
):
raise UserError(_("The remaining value on the last depreciation line must be 0"))
@api.constrains('original_move_line_ids')
def _check_related_purchase(self):
for asset in self:
if asset.original_move_line_ids and asset.related_purchase_value == 0:
raise UserError(_("You cannot create an asset from lines containing credit and debit on the account or with a null amount"))
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
@api.ondelete(at_uninstall=True)
def _unlink_if_model_or_draft(self):
for asset in self:
if asset.state in ['open', 'paused', 'close']:
raise UserError(_(
'You cannot delete a document that is in %s state.',
dict(self._fields['state']._description_selection(self.env)).get(asset.state)
))
posted_amount = len(asset.depreciation_move_ids.filtered(lambda x: x.state == 'posted'))
if posted_amount > 0:
raise UserError(_('You cannot delete an asset linked to posted entries.'
'\nYou should either confirm the asset, then, sell or dispose of it,'
' or cancel the linked journal entries.'))
def unlink(self):
for asset in self:
for line in asset.original_move_line_ids:
if line.name:
body = _('A document linked to %s has been deleted: %s',
line.name,
asset._get_html_link(),
)
else:
body = _('A document linked to this move has been deleted: %s',
asset._get_html_link())
line.move_id.message_post(body=body)
return super(AccountAsset, self).unlink()
def copy_data(self, default=None):
if default is None:
default = {}
if self.state == 'model':
default.update(state='model')
default['name'] = self.name + _(' (copy)')
default['account_asset_id'] = self.account_asset_id.id
return super().copy_data(default)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if 'state' in vals and vals['state'] != 'draft' and not (set(vals) - set({'account_depreciation_id', 'account_depreciation_expense_id', 'journal_id'})):
raise UserError(_("Some required values are missing"))
if self._context.get('default_state') != 'model' and vals.get('state') != 'model':
vals['state'] = 'draft'
new_recs = super(AccountAsset, self.with_context(mail_create_nolog=True)).create(vals_list)
# if original_value is passed in vals, make sure the right value is set (as a different original_value may have been computed by _compute_value())
for i, vals in enumerate(vals_list):
if 'original_value' in vals:
new_recs[i].original_value = vals['original_value']
if self.env.context.get('original_asset'):
# When original_asset is set, only one asset is created since its from the form view
original_asset = self.env['account.asset'].browse(self.env.context.get('original_asset'))
original_asset.model_id = new_recs
return new_recs
def write(self, vals):
result = super().write(vals)
for move in self.depreciation_move_ids:
if move.state == 'draft' and 'analytic_distribution' in vals:
# Only draft entries to avoid recreating all the analytic items
move.line_ids.analytic_distribution = vals['analytic_distribution']
lock_date = move.company_id._get_user_fiscal_lock_date()
if move.date > lock_date:
if 'account_depreciation_id' in vals:
# ::2 (0, 2, 4, ...) because we want all first lines of the depreciation entries, which corresponds to the
# lines with account_depreciation_id as account
move.line_ids[::2].account_id = vals['account_depreciation_id']
if 'account_depreciation_expense_id' in vals:
# 1::2 (1, 3, 5, ...) because we want all second lines of the depreciation entries, which corresponds to the
# lines with account_depreciation_expense_id as account
move.line_ids[1::2].account_id = vals['account_depreciation_expense_id']
if 'journal_id' in vals:
move.journal_id = vals['journal_id']
return result
# -------------------------------------------------------------------------
# BOARD COMPUTATION
# -------------------------------------------------------------------------
def _get_linear_amount(self, days_before_period, days_until_period_end, total_depreciable_value):
amount_expected_previous_period = total_depreciable_value * days_before_period / self.asset_lifetime_days
amount_after_expected = total_depreciable_value * days_until_period_end / self.asset_lifetime_days
number_days_for_period = days_until_period_end - days_before_period
# In case of a decrease, we need to lower the amount of the depreciation with the amount of the decrease
# spread over the remaining lifetime
amount_of_decrease_spread_over_period = [
number_days_for_period * mv.depreciation_value / (self.asset_lifetime_days - self._get_delta_days(self.paused_prorata_date, mv.asset_depreciation_beginning_date))
for mv in self.depreciation_move_ids.filtered(lambda mv: mv.asset_value_change)
]
computed_linear_amount = self.currency_id.round(amount_after_expected - self.currency_id.round(amount_expected_previous_period) - sum(amount_of_decrease_spread_over_period))
return computed_linear_amount
def _compute_board_amount(self, residual_amount, period_start_date, period_end_date, days_already_depreciated,
days_left_to_depreciated, residual_declining, start_yearly_period=None, total_lifetime_left=None,
residual_at_compute=None, start_recompute_date=None):
def _get_max_between_linear_and_degressive(linear_amount):
"""
Compute the degressive amount that could be depreciated and returns the biggest between it and linear_amount
The degressive amount corresponds to the difference between what should have been depreciated at the end of
the period and the residual_amount (to deal with rounding issues at the end of each month)
"""
fiscalyear_dates = self.company_id.compute_fiscalyear_dates(period_end_date)
days_in_fiscalyear = self._get_delta_days(fiscalyear_dates['date_from'], fiscalyear_dates['date_to'])
degressive_total_value = residual_declining * (1 - self.method_progress_factor * self._get_delta_days(start_yearly_period, period_end_date) / days_in_fiscalyear)
degressive_amount = residual_amount - degressive_total_value
return self._degressive_linear_amount(residual_amount, degressive_amount, linear_amount)
days_until_period_end = self._get_delta_days(self.paused_prorata_date, period_end_date)
days_before_period = self._get_delta_days(self.paused_prorata_date, period_start_date + relativedelta(days=-1))
days_before_period = max(days_before_period, 0) # if disposed before the beginning of the asset for example
number_days = days_until_period_end - days_before_period
if float_is_zero(self.asset_lifetime_days, 2):
return 0, 0
# The amount to depreciate are computed by computing how much the asset should be depreciated at the end of the
# period minus how much it is actually depreciated. It is done that way to avoid having the last move to take
# every single small difference that could appear over the time with the classic computation method.
if self.method == 'linear':
if total_lifetime_left and float_compare(total_lifetime_left, 0, 2) > 0:
computed_linear_amount = residual_amount - residual_at_compute * (1 - self._get_delta_days(start_recompute_date, period_end_date) / total_lifetime_left)
else:
computed_linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value)
amount = min(computed_linear_amount, residual_amount, key=abs)
elif self.method == 'degressive':
# Linear amount
# We first calculate the total linear amount for the period left from the beginning of the year
# to get the linear amount for the period in order to avoid big delta at the end of the period
days_left_from_beginning_of_year = self._get_delta_days(start_yearly_period, period_start_date - relativedelta(days=1)) + days_left_to_depreciated
expected_remaining_value_with_linear = residual_declining - residual_declining * self._get_delta_days(start_yearly_period, period_end_date) / days_left_from_beginning_of_year
linear_amount = residual_amount - expected_remaining_value_with_linear
amount = _get_max_between_linear_and_degressive(linear_amount)
elif self.method == 'degressive_then_linear':
if not self.parent_id:
linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value)
else:
# we want to know the amount before the reeval for the parent so the child can follow the same curve,
# so it transitions from degressive to linear at the same moment
parent_moves = self.parent_id.depreciation_move_ids.filtered(lambda mv: mv.date <= self.prorata_date).sorted(key=lambda mv: (mv.date, mv.id))
parent_cumulative_depreciation = parent_moves[-1].asset_depreciated_value if parent_moves else self.parent_id.already_depreciated_amount_import
parent_depreciable_value = parent_moves[-1].asset_remaining_value if parent_moves else self.parent_id.total_depreciable_value
if self.currency_id.is_zero(parent_depreciable_value):
linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, self.total_depreciable_value)
else:
# To have the same curve as the parent, we need to have the equivalent amount before the reeval.
# The child's depreciable value corresponds to the amount that is left to depreciate for the parent.
# So, we use the proportion between them to compute the equivalent child's total to depreciate.
# We use it then with the duration of the parent to compute the depreciation amount
depreciable_value = self.total_depreciable_value * (1 + parent_cumulative_depreciation/parent_depreciable_value)
linear_amount = self._get_linear_amount(days_before_period, days_until_period_end, depreciable_value) * self.asset_lifetime_days / self.parent_id.asset_lifetime_days
amount = _get_max_between_linear_and_degressive(linear_amount)
amount = max(amount, 0) if self.currency_id.compare_amounts(residual_amount, 0) > 0 else min(amount, 0)
amount = self._get_depreciation_amount_end_of_lifetime(residual_amount, amount, days_until_period_end)
return number_days, self.currency_id.round(amount)
def compute_depreciation_board(self, date=False):
# Need to unlink draft moves before adding new ones because if we create new moves before, it will cause an error
self.depreciation_move_ids.filtered(lambda mv: mv.state == 'draft' and (mv.date >= date if date else True)).unlink()
new_depreciation_moves_data = []
for asset in self:
new_depreciation_moves_data.extend(asset._recompute_board(date))
new_depreciation_moves = self.env['account.move'].create(new_depreciation_moves_data)
new_depreciation_moves_to_post = new_depreciation_moves.filtered(lambda move: move.asset_id.state == 'open')
# In case of the asset is in running mode, we post in the past and set to auto post move in the future
new_depreciation_moves_to_post._post()
def _recompute_board(self, start_depreciation_date=False):
self.ensure_one()
# All depreciation moves that are posted
posted_depreciation_move_ids = self.depreciation_move_ids.filtered(
lambda mv: mv.state == 'posted' and not mv.asset_value_change
).sorted(key=lambda mv: (mv.date, mv.id))
imported_amount = self.already_depreciated_amount_import
residual_amount = self.value_residual - sum(self.depreciation_move_ids.filtered(lambda mv: mv.state == 'draft').mapped('depreciation_value'))
if not posted_depreciation_move_ids:
residual_amount += imported_amount
residual_declining = residual_at_compute = residual_amount
# start_yearly_period is needed in the 'degressive' and 'degressive_then_linear' methods to compute the amount when the period is monthly
start_recompute_date = start_depreciation_date = start_yearly_period = start_depreciation_date or self.paused_prorata_date
last_day_asset = self._get_last_day_asset()
final_depreciation_date = self._get_end_period_date(last_day_asset)
total_lifetime_left = self._get_delta_days(start_depreciation_date, last_day_asset)
depreciation_move_values = []
if not float_is_zero(self.value_residual, precision_rounding=self.currency_id.rounding):
while not self.currency_id.is_zero(residual_amount) and start_depreciation_date < final_depreciation_date:
period_end_depreciation_date = self._get_end_period_date(start_depreciation_date)
period_end_fiscalyear_date = self.company_id.compute_fiscalyear_dates(period_end_depreciation_date).get('date_to')
lifetime_left = self._get_delta_days(start_depreciation_date, last_day_asset)
days, amount = self._compute_board_amount(residual_amount, start_depreciation_date, period_end_depreciation_date, False, lifetime_left, residual_declining, start_yearly_period, total_lifetime_left, residual_at_compute, start_recompute_date)
residual_amount -= amount
if not posted_depreciation_move_ids:
# self.already_depreciated_amount_import management.
# Subtracts the imported amount from the first depreciation moves until we reach it
# (might skip several depreciation entries)
if abs(imported_amount) <= abs(amount):
amount -= imported_amount
imported_amount = 0
else:
imported_amount -= amount
amount = 0
if self.method == 'degressive_then_linear' and final_depreciation_date < period_end_depreciation_date:
period_end_depreciation_date = final_depreciation_date
if not float_is_zero(amount, precision_rounding=self.currency_id.rounding):
# For deferred revenues, we should invert the amounts.
depreciation_move_values.append(self.env['account.move']._prepare_move_for_asset_depreciation({
'amount': amount,
'asset_id': self,
'depreciation_beginning_date': start_depreciation_date,
'date': period_end_depreciation_date,
'asset_number_days': days,
}))
if period_end_depreciation_date == period_end_fiscalyear_date:
start_yearly_period = self.company_id.compute_fiscalyear_dates(period_end_depreciation_date).get('date_from') + relativedelta(years=1)
residual_declining = residual_amount
start_depreciation_date = period_end_depreciation_date + relativedelta(days=1)
return depreciation_move_values
def _get_end_period_date(self, start_depreciation_date):
"""Get the end of the period in which the depreciation is posted.
Can be the end of the month if the asset is depreciated monthly, or the end of the fiscal year is it is depreciated yearly.
"""
self.ensure_one()
fiscalyear_date = self.company_id.compute_fiscalyear_dates(start_depreciation_date).get('date_to')
period_end_depreciation_date = fiscalyear_date if start_depreciation_date <= fiscalyear_date else fiscalyear_date + relativedelta(years=1)
if self.method_period == '1': # If method period is set to monthly computation
max_day_in_month = end_of(datetime.date(start_depreciation_date.year, start_depreciation_date.month, 1), 'month').day
period_end_depreciation_date = min(start_depreciation_date.replace(day=max_day_in_month), period_end_depreciation_date)
return period_end_depreciation_date
def _get_delta_days(self, start_date, end_date):
"""Compute how many days there are between 2 dates.
The computation is different if the asset is in daily_computation or not.
"""
self.ensure_one()
if self.prorata_computation_type == 'daily_computation':
# Compute how many days there are between 2 dates using a daily_computation method
return (end_date - start_date).days + 1
else:
# Compute how many days there are between 2 dates counting 30 days per month
# Get how many days there are in the start date month
start_date_days_month = end_of(start_date, 'month').day
# Get how many days there are in the start date month (e.g: June 20th: (30 * (30 - 20 + 1)) / 30 = 11)
start_prorata = (start_date_days_month - start_date.day + 1) / start_date_days_month
# Get how many days there are in the end date month (e.g: You're the August 14th: (14 * 30) / 31 = 13.548387096774194)
end_prorata = end_date.day / end_of(end_date, 'month').day
# Compute how many days there are between these 2 dates
# e.g: 13.548387096774194 + 11 + 360 * (2020 - 2020) + 30 * (8 - 6 - 1) = 24.548387096774194 + 360 * 0 + 30 * 1 = 54.548387096774194 day
return sum((
start_prorata * DAYS_PER_MONTH,
end_prorata * DAYS_PER_MONTH,
(end_date.year - start_date.year) * DAYS_PER_YEAR,
(end_date.month - start_date.month - 1) * DAYS_PER_MONTH
))
def _get_last_day_asset(self):
this = self.parent_id if self.parent_id else self
return this.paused_prorata_date + relativedelta(months=int(this.method_period) * this.method_number, days=-1)
# -------------------------------------------------------------------------
# PUBLIC ACTIONS
# -------------------------------------------------------------------------
def action_asset_modify(self):
""" Returns an action opening the asset modification wizard.
"""
self.ensure_one()
new_wizard = self.env['asset.modify'].create({
'asset_id': self.id,
'modify_action': 'resume' if self.env.context.get('resume_after_pause') else 'dispose',
})
return {
'name': _('Modify Asset'),
'view_mode': 'form',
'res_model': 'asset.modify',
'type': 'ir.actions.act_window',
'target': 'new',
'res_id': new_wizard.id,
'context': self.env.context,
}
def action_save_model(self):
return {
'name': _('Save model'),
'views': [[self.env.ref('account_asset.view_account_asset_form').id, "form"]],
'res_model': 'account.asset',
'type': 'ir.actions.act_window',
'context': {
'default_state': 'model',
'default_account_asset_id': self.account_asset_id.id,
'default_account_depreciation_id': self.account_depreciation_id.id,
'default_account_depreciation_expense_id': self.account_depreciation_expense_id.id,
'default_journal_id': self.journal_id.id,
'default_method': self.method,
'default_method_number': self.method_number,
'default_method_period': self.method_period,
'default_method_progress_factor': self.method_progress_factor,
'default_prorata_date': self.prorata_date,
'default_prorata_computation_type': self.prorata_computation_type,
'default_analytic_distribution': self.analytic_distribution,
'original_asset': self.id,
}
}
def open_entries(self):
return {
'name': _('Journal Entries'),
'view_mode': 'tree,form',
'res_model': 'account.move',
'search_view_id': [self.env.ref('account.view_account_move_filter').id, 'search'],
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (False, 'form')],
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.depreciation_move_ids.ids)],
'context': dict(self._context, create=False),
}
def open_related_entries(self):
return {
'name': _('Journal Items'),
'view_mode': 'tree,form',
'res_model': 'account.move.line',
'view_id': False,
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.original_move_line_ids.ids)],
}
def open_increase(self):
result = {
'name': _('Gross Increase'),
'view_mode': 'tree,form',
'res_model': 'account.asset',
'context': {**self.env.context, 'create': False},
'view_id': False,
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.children_ids.ids)],
'views': [(False, 'tree'), (False, 'form')],
}
if len(self.children_ids) == 1:
result['views'] = [(False, 'form')]
result['res_id'] = self.children_ids.id
return result
def open_parent_id(self):
result = {
'name': _('Parent Asset'),
'view_mode': 'form',
'res_model': 'account.asset',
'type': 'ir.actions.act_window',
'res_id': self.parent_id.id,
'views': [(False, 'form')],
}
return result
def validate(self):
fields = [
'method',
'method_number',
'method_period',
'method_progress_factor',
'salvage_value',
'original_move_line_ids',
]
ref_tracked_fields = self.env['account.asset'].fields_get(fields)
self.write({'state': 'open'})
for asset in self:
tracked_fields = ref_tracked_fields.copy()
if asset.method == 'linear':
del tracked_fields['method_progress_factor']
dummy, tracking_value_ids = asset._mail_track(tracked_fields, dict.fromkeys(fields))
asset_name = (_('Asset created'), _('An asset has been created for this move:'))
msg = asset_name[1] + ' ' + asset._get_html_link()
asset.message_post(body=asset_name[0], tracking_value_ids=tracking_value_ids)
for move_id in asset.original_move_line_ids.mapped('move_id'):
move_id.message_post(body=msg)
try:
if not asset.depreciation_move_ids:
asset.compute_depreciation_board()
asset._check_depreciations()
asset.depreciation_move_ids.filtered(lambda move: move.state != 'posted')._post()
except psycopg2.errors.CheckViolation:
raise ValidationError(_("Atleast one asset (%s) couldn't be set as running because it lacks any required information", asset.name))
if asset.account_asset_id.create_asset == 'no':
asset._post_non_deductible_tax_value()
def set_to_close(self, invoice_line_ids, date=None, message=None):
self.ensure_one()
disposal_date = date or fields.Date.today()
if disposal_date <= self.company_id._get_user_fiscal_lock_date():
raise UserError(_("You cannot dispose of an asset before the lock date."))
if invoice_line_ids and self.children_ids.filtered(lambda a: a.state in ('draft', 'open') or a.value_residual > 0):
raise UserError(_("You cannot automate the journal entry for an asset that has a running gross increase. Please use 'Dispose' on the increase(s)."))
full_asset = self + self.children_ids
full_asset.state = 'close'
move_ids = full_asset._get_disposal_moves([invoice_line_ids] * len(full_asset), disposal_date)
for asset in full_asset:
asset.message_post(body=
_('Asset sold. %s', message if message else "")
if invoice_line_ids else
_('Asset disposed. %s', message if message else "")
)
if move_ids:
name = _('Disposal Move')
view_mode = 'form'
if len(move_ids) > 1:
name = _('Disposal Moves')
view_mode = 'tree,form'
return {
'name': name,
'view_mode': view_mode,
'res_model': 'account.move',
'type': 'ir.actions.act_window',
'target': 'current',
'res_id': move_ids[0],
'domain': [('id', 'in', move_ids)]
}
def set_to_cancelled(self):
for asset in self:
posted_moves = asset.depreciation_move_ids.filtered(lambda m: (
not m.reversal_move_id
and not m.reversed_entry_id
and m.state == 'posted'
))
if posted_moves:
depreciation_change = sum(posted_moves.line_ids.mapped(
lambda l: l.debit if l.account_id == asset.account_depreciation_expense_id else 0.0
))
acc_depreciation_change = sum(posted_moves.line_ids.mapped(
lambda l: l.credit if l.account_id == asset.account_depreciation_id else 0.0
))
entries = Markup('
').join(posted_moves.sorted('date').mapped(lambda m:
f'{m.ref} - {m.date} - '
f'{formatLang(self.env, m.depreciation_value, currency_obj=m.currency_id)} - '
f'{m.name}'
))
asset._cancel_future_moves(datetime.date.min)
msg = _('Asset Cancelled') + Markup('
') + \
_('The account %(exp_acc)s has been credited by %(exp_delta)s, '
'while the account %(dep_acc)s has been debited by %(dep_delta)s. '
'This corresponds to %(move_count)s cancelled %(word)s:',
exp_acc=asset.account_depreciation_expense_id.display_name,
exp_delta=formatLang(self.env, depreciation_change, currency_obj=asset.currency_id),
dep_acc=asset.account_depreciation_id.display_name,
dep_delta=formatLang(self.env, acc_depreciation_change, currency_obj=asset.currency_id),
move_count=len(posted_moves),
word=_('entries') if len(posted_moves) > 1 else _('entry'),
) + Markup('
') + entries
asset._message_log(body=msg)
else:
asset._message_log(body=_('Asset Cancelled'))
asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft').with_context(force_delete=True).unlink()
asset.asset_paused_days = 0
asset.write({'state': 'cancelled'})
def set_to_draft(self):
self.write({'state': 'draft'})
def set_to_running(self):
if self.depreciation_move_ids and not max(self.depreciation_move_ids, key=lambda m: (m.date, m.id)).asset_remaining_value == 0:
self.env['asset.modify'].create({'asset_id': self.id, 'name': _('Reset to running')}).modify()
self.write({'state': 'open'})
def resume_after_pause(self):
""" Sets an asset in 'paused' state back to 'open'.
A Depreciation line is created automatically to remove from the
depreciation amount the proportion of time spent
in pause in the current period.
"""
self.ensure_one()
return self.with_context(resume_after_pause=True).action_asset_modify()
def pause(self, pause_date, message=None):
""" Sets an 'open' asset in 'paused' state, generating first a depreciation
line corresponding to the ratio of time spent within the current depreciation
period before putting the asset in pause. This line and all the previous
unposted ones are then posted.
"""
self.ensure_one()
self._create_move_before_date(pause_date)
self.write({'state': 'paused'})
self.message_post(body=_("Asset paused. %s", message if message else ""))
def open_asset(self, view_mode):
if len(self) == 1:
view_mode = ['form']
views = [v for v in [(False, 'tree'), (False, 'form')] if v[1] in view_mode]
ctx = dict(self._context)
ctx.pop('default_move_type', None)
action = {
'name': _('Asset'),
'view_mode': ','.join(view_mode),
'type': 'ir.actions.act_window',
'res_id': self.id if 'tree' not in view_mode else False,
'res_model': 'account.asset',
'views': views,
'domain': [('id', 'in', self.ids)],
'context': ctx
}
return action
# -------------------------------------------------------------------------
# HELPER METHODS
# -------------------------------------------------------------------------
def _insert_depreciation_line(self, amount, beginning_depreciation_date, depreciation_date, days_depreciated):
""" Inserts a new line in the depreciation board, shifting the sequence of
all the following lines from one unit.
:param amount: The depreciation amount of the new line.
:param label: The name to give to the new line.
:param date: The date to give to the new line.
"""
self.ensure_one()
AccountMove = self.env['account.move']
return AccountMove.create(AccountMove._prepare_move_for_asset_depreciation({
'amount': amount,
'asset_id': self,
'depreciation_beginning_date': beginning_depreciation_date,
'date': depreciation_date,
'asset_number_days': days_depreciated,
}))
def _post_non_deductible_tax_value(self):
# If the asset has a non-deductible tax, the value is posted in the chatter to explain why
# the original value does not match the related purchase(s).
if self.non_deductible_tax_value:
currency = self.env.company.currency_id
msg = _('A non deductible tax value of %s was added to %s\'s initial value of %s',
formatLang(self.env, self.non_deductible_tax_value, currency_obj=currency),
self.name,
formatLang(self.env, self.related_purchase_value, currency_obj=currency))
self.message_post(body=msg)
def _create_move_before_date(self, date):
"""Cancel all the moves after the given date and replace them by a new one.
The new depreciation/move is depreciating the residual value.
"""
all_move_dates_before_date = (self.depreciation_move_ids.filtered(
lambda x:
x.date <= date
and not x.reversal_move_id
and not x.reversed_entry_id
and x.state == 'posted'
).sorted('date')).mapped('date')
beginning_fiscal_year = self.company_id.compute_fiscalyear_dates(date).get('date_from') if self.method != 'linear' else False
first_fiscalyear_move = self.env['account.move']
if all_move_dates_before_date:
last_move_date_not_reversed = max(all_move_dates_before_date)
# We don't know when begins the period that the move is supposed to cover
# So, we use the earliest beginning of a move that comes after the last move not cancelled
future_moves_beginning_date = self.depreciation_move_ids.filtered(
lambda m: m.date > last_move_date_not_reversed and (
not m.reversal_move_id and not m.reversed_entry_id and m.state == 'posted'
or m.state == 'draft'
)
).mapped('asset_depreciation_beginning_date')
beginning_depreciation_date = min(future_moves_beginning_date) if future_moves_beginning_date else self.paused_prorata_date
if self.method != 'linear':
# In degressive and degressive_then_linear, we need to find the first move of the fiscal year that comes after the last move not cancelled
# in order to correctly compute the moves just before and after the pause date
first_moves = self.depreciation_move_ids.filtered(
lambda m: m.asset_depreciation_beginning_date >= beginning_fiscal_year and (
not m.reversal_move_id and not m.reversed_entry_id and m.state == 'posted'
or m.state == 'draft'
)
).sorted(lambda m: (m.asset_depreciation_beginning_date, m.id))
first_fiscalyear_move = next(iter(first_moves), first_fiscalyear_move)
else:
beginning_depreciation_date = self.paused_prorata_date
residual_declining = first_fiscalyear_move.asset_remaining_value + first_fiscalyear_move.depreciation_value
self._cancel_future_moves(date)
imported_amount = self.already_depreciated_amount_import if not all_move_dates_before_date else 0
value_residual = self.value_residual + self.already_depreciated_amount_import if not all_move_dates_before_date else self.value_residual
residual_declining = residual_declining or value_residual
last_day_asset = self._get_last_day_asset()
lifetime_left = self._get_delta_days(beginning_depreciation_date, last_day_asset)
days_depreciated, amount = self._compute_board_amount(self.value_residual, beginning_depreciation_date, date, False, lifetime_left, residual_declining, beginning_fiscal_year, lifetime_left, value_residual, beginning_depreciation_date)
if abs(imported_amount) <= abs(amount):
amount -= imported_amount
if not float_is_zero(amount, precision_rounding=self.currency_id.rounding):
new_line = self._insert_depreciation_line(amount, beginning_depreciation_date, date, days_depreciated)
new_line._post()
def _cancel_future_moves(self, date):
"""Cancel all the depreciation entries after the date given as parameter.
When possible, it will reset those to draft before unlinking them, reverse them otherwise.
:param date: date after which the moves are deleted/reversed
"""
for asset in self:
obsolete_moves = asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft' or (
not m.reversal_move_id
and not m.reversed_entry_id
and m.state == 'posted'
and m.date > date
))
obsolete_moves._unlink_or_reverse()
def _get_disposal_moves(self, invoice_lines_list, disposal_date):
"""Create the move for the disposal of an asset.
:param invoice_lines_list: list of recordset of `account.move.line`
Each element of the list corresponds to one record of `self`
These lines are used to generate the disposal move
:param disposal_date: the date of the disposal
"""
def get_line(asset, amount, account):
return (0, 0, {
'name': asset.name,
'account_id': account.id,
'balance': -amount,
'analytic_distribution': analytic_distribution,
'currency_id': asset.currency_id.id,
'amount_currency': -asset.company_id.currency_id._convert(
from_amount=amount,
to_currency=asset.currency_id,
company=asset.company_id,
date=disposal_date,
)
})
move_ids = []
assert len(self) == len(invoice_lines_list)
for asset, invoice_line_ids in zip(self, invoice_lines_list):
asset._create_move_before_date(disposal_date)
analytic_distribution = asset.analytic_distribution
dict_invoice = {}
invoice_amount = 0
initial_amount = asset.original_value
initial_account = asset.original_move_line_ids.account_id if len(asset.original_move_line_ids.account_id) == 1 else asset.account_asset_id
all_lines_before_disposal = asset.depreciation_move_ids.filtered(lambda x: x.date <= disposal_date)
depreciated_amount = asset.currency_id.round(copysign(
sum(all_lines_before_disposal.mapped('depreciation_value')) + asset.already_depreciated_amount_import,
-initial_amount,
))
depreciation_account = asset.account_depreciation_id
for invoice_line in invoice_line_ids:
dict_invoice[invoice_line.account_id] = copysign(invoice_line.balance, -initial_amount) + dict_invoice.get(invoice_line.account_id, 0)
invoice_amount += copysign(invoice_line.balance, -initial_amount)
list_accounts = [(amount, account) for account, amount in dict_invoice.items()]
difference = -initial_amount - depreciated_amount - invoice_amount
difference_account = asset.company_id.gain_account_id if difference > 0 else asset.company_id.loss_account_id
line_datas = [(initial_amount, initial_account), (depreciated_amount, depreciation_account)] + list_accounts + [(difference, difference_account)]
vals = {
'asset_id': asset.id,
'ref': asset.name + ': ' + (_('Disposal') if not invoice_line_ids else _('Sale')),
'asset_depreciation_beginning_date': disposal_date,
'date': disposal_date,
'journal_id': asset.journal_id.id,
'move_type': 'entry',
'line_ids': [get_line(asset, amount, account) for amount, account in line_datas if account],
}
asset.write({'depreciation_move_ids': [(0, 0, vals)]})
move_ids += self.env['account.move'].search([('asset_id', '=', asset.id), ('state', '=', 'draft')]).ids
return move_ids
def _degressive_linear_amount(self, residual_amount, degressive_amount, linear_amount):
if self.currency_id.compare_amounts(residual_amount, 0) > 0:
return max(degressive_amount, linear_amount)
else:
return min(degressive_amount, linear_amount)
def _get_depreciation_amount_end_of_lifetime(self, residual_amount, amount, days_until_period_end):
if abs(residual_amount) < abs(amount) or days_until_period_end >= self.asset_lifetime_days:
# If the residual amount is less than the computed amount, we keep the residual amount
# If total_days is greater or equals to asset lifetime days, it should mean that
# the asset will finish in this period and the value for this period is equal to the residual amount.
amount = residual_amount
return amount
def _get_own_book_value(self, date=None):
self.ensure_one()
return (self._get_residual_value_at_date(date) if date else self.value_residual) + self.salvage_value
def _get_residual_value_at_date(self, date):
""" Computes the theoretical value of the asset at a specific date.
:param date: the date at which we want the asset's value
:return: the value at date of the asset without taking reverse entries into account (as it should be in a "normal" flow of the asset)
"""
current_and_previous_depreciation = self.depreciation_move_ids.filtered(
lambda mv:
mv.asset_depreciation_beginning_date < date
and not mv.reversed_entry_id
).sorted('asset_depreciation_beginning_date', reverse=True)
if not current_and_previous_depreciation:
return 0
if len(current_and_previous_depreciation) > 1:
previous_value_residual = current_and_previous_depreciation[1].asset_remaining_value
else:
# If there is only one depreciation, we take the original depreciation value
previous_value_residual = self.original_value - self.salvage_value - self.already_depreciated_amount_import
# We compare the amount_residuals of the depreciations before and during the given date.
# It applies the ratio of the period (to-given-date / total-days-of-the-period) to the amount of the depreciation.
cur_depr_end_date = self._get_end_period_date(date)
current_depreciation = current_and_previous_depreciation[0]
cur_depr_beg_date = current_depreciation.asset_depreciation_beginning_date
rate = self._get_delta_days(cur_depr_beg_date, date) / self._get_delta_days(cur_depr_beg_date, cur_depr_end_date)
lost_value_at_date = (previous_value_residual - current_depreciation.asset_remaining_value) * rate
residual_value_at_date = self.currency_id.round(previous_value_residual - lost_value_at_date)
if self.currency_id.compare_amounts(self.original_value, 0) > 0:
return max(residual_value_at_date, 0)
else:
return min(residual_value_at_date, 0)