forked from Mapan/odoo17e
1172 lines
64 KiB
Python
1172 lines
64 KiB
Python
# -*- 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('<br>').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('<br>') + \
|
|
_('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('<br>') + 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)
|