You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1050 lines
56 KiB
Python
1050 lines
56 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
from math import copysign
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
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, readonly=True, states={'draft': [('readonly', False)]}, 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',
|
|
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)
|
|
asset_type = fields.Selection([('sale', 'Sale: Revenue Recognition'), ('purchase', 'Purchase: Asset'), ('expense', 'Deferred Expense')], compute='_compute_asset_type', store=True, index=True, copy=True)
|
|
|
|
# Depreciation params
|
|
method = fields.Selection(
|
|
selection=[
|
|
('linear', 'Straight Line'),
|
|
('degressive', 'Declining'),
|
|
('degressive_then_linear', 'Declining then Straight Line')
|
|
],
|
|
string='Method',
|
|
readonly=True, states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
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', readonly=True, states={'draft': [('readonly', False)], 'model': [('readonly', False)]}, 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', readonly=True, default='12', states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
help="The amount of time between two depreciations")
|
|
method_progress_factor = fields.Float(string='Declining Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)], 'model': [('readonly', False)]})
|
|
prorata_computation_type = fields.Selection(
|
|
selection=[
|
|
('none', 'No Prorata'),
|
|
('constant_periods', 'Constant Periods'),
|
|
('daily_computation', 'Based on days per period'),
|
|
],
|
|
string="Computation",
|
|
readonly=True, states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
required=True, default='none',
|
|
)
|
|
prorata_date = fields.Date( # the starting date of the depreciations
|
|
string='Prorata Date',
|
|
compute='_compute_prorata_date', store=True, readonly=False,
|
|
)
|
|
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, states={'draft': [('readonly', False)], 'model': [('readonly', False)]}, domain="[('company_id', '=', company_id), ('is_off_balance', '=', False)]")
|
|
account_depreciation_id = fields.Many2one(
|
|
comodel_name='account.account',
|
|
string='Depreciation Account',
|
|
readonly=True, states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False), ('company_id', '=', company_id)]",
|
|
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',
|
|
readonly=True, states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
domain="[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card', 'off_balance')), ('deprecated', '=', False), ('company_id', '=', company_id)]",
|
|
help="Account used in the periodical entries, to record a part of the asset as expense.",
|
|
)
|
|
|
|
journal_id = fields.Many2one(
|
|
'account.journal',
|
|
string='Journal',
|
|
domain="[('type', '=', 'general'), ('company_id', '=', company_id)]",
|
|
compute='_compute_journal_id', store=True, readonly=True,
|
|
states={'draft': [('readonly', False)], 'model': [('readonly', False)]},
|
|
)
|
|
|
|
# Values
|
|
original_value = fields.Monetary(string="Original Value", compute='_compute_value', store=True, states={'draft': [('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', readonly=True, states={'draft': [('readonly', False)]},
|
|
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_book_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', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)], 'paused': [('readonly', False)]})
|
|
original_move_line_ids = fields.Many2many('account.move.line', 'asset_move_line_rel', 'asset_id', 'line_id', string='Journal Items', readonly=True, states={'draft': [('readonly', False)]}, copy=False)
|
|
|
|
# Dates
|
|
acquisition_date = fields.Date(compute='_compute_acquisition_date', store=True, states={'draft': [('readonly', False)]})
|
|
disposal_date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}, compute="_compute_disposal_date", store=True)
|
|
|
|
# model-related fields
|
|
model_id = fields.Many2one('account.asset', string='Model', change_default=True, readonly=True, states={'draft': [('readonly', False)]}, 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(
|
|
readonly=True, states={'draft': [('readonly', False)]},
|
|
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.Integer(compute="_compute_lifetime_days") # 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:
|
|
asset.journal_id = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', asset.company_id.id)], 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', 'asset_type', '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('default_account_asset_id')
|
|
def _compute_display_account_asset_id(self):
|
|
for record in self:
|
|
# Hide the field when creating an asset model from the CoA.
|
|
model_from_coa = self.env.context.get('default_account_asset_id') 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()
|
|
record._onchange_account_depreciation_expense_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 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
|
|
|
|
@api.depends('acquisition_date', 'company_id', 'prorata_computation_type')
|
|
def _compute_prorata_date(self):
|
|
for asset in self:
|
|
if asset.prorata_computation_type == 'none':
|
|
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(asset.original_move_line_ids.mapped('date') + [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_move_line_ids')
|
|
@api.depends_context('asset_type')
|
|
def _compute_asset_type(self):
|
|
for record in self:
|
|
if not record.asset_type and 'asset_type' in self.env.context:
|
|
record.asset_type = self.env.context['asset_type']
|
|
if not record.asset_type and record.original_move_line_ids:
|
|
account = record.original_move_line_ids.account_id
|
|
record.asset_type = account.asset_type
|
|
|
|
@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'))
|
|
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
|
|
move_lines = record.original_move_line_ids
|
|
non_deductible_tax_value = sum(move_lines.mapped('non_deductible_tax_value'))
|
|
if non_deductible_tax_value:
|
|
account = move_lines.account_id
|
|
auto_create_multi = account.create_asset != 'no' and account.multiple_assets_per_line
|
|
quantity = move_lines.quantity if auto_create_multi else 1
|
|
record.non_deductible_tax_value = record.currency_id.round(non_deductible_tax_value / quantity)
|
|
|
|
@api.depends('depreciation_move_ids.state', 'parent_id')
|
|
def _compute_counts(self):
|
|
depreciation_per_asset = {
|
|
group['asset_id'][0]: group['move_ids']
|
|
for group in self.env['account.move'].read_group(
|
|
domain=[
|
|
('asset_id', 'in', self.ids),
|
|
('state', '=', 'posted'),
|
|
],
|
|
fields=['move_ids:count(id)'],
|
|
groupby=['asset_id'],
|
|
)
|
|
}
|
|
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
|
|
# -------------------------------------------------------------------------
|
|
def onchange(self, values, field_name, field_onchange):
|
|
# Force the re-rendering of computed fields on the o2m
|
|
if field_name == 'depreciation_move_ids':
|
|
return super().onchange(values, False, {
|
|
fname: spec
|
|
for fname, spec in field_onchange.items()
|
|
if fname.startswith('depreciation_move_ids')
|
|
})
|
|
return super().onchange(values, field_name, field_onchange)
|
|
|
|
@api.onchange('account_depreciation_id')
|
|
def _onchange_account_depreciation_id(self):
|
|
if not self.original_move_line_ids:
|
|
if self.asset_type == 'expense':
|
|
# Always change the account since it is not visible in the form
|
|
self.account_asset_id = self.account_depreciation_id
|
|
if self.asset_type == 'purchase' and 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('account_depreciation_expense_id')
|
|
def _onchange_account_depreciation_expense_id(self):
|
|
if not self.original_move_line_ids and self.asset_type not in ('purchase', 'expense'):
|
|
self.account_asset_id = self.account_depreciation_expense_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):
|
|
if self.asset_type in ('purchase', 'expense'):
|
|
self.account_depreciation_id = self.account_depreciation_id or self.account_asset_id
|
|
else:
|
|
self.account_depreciation_expense_id = self.account_depreciation_expense_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_depreciation_id = model.account_depreciation_id
|
|
self.account_depreciation_expense_id = model.account_depreciation_expense_id
|
|
self.journal_id = model.journal_id
|
|
|
|
@api.onchange('asset_type')
|
|
def _onchange_type(self):
|
|
if self.state != 'model':
|
|
if self.asset_type == 'sale':
|
|
self.prorata_computation_type = 'daily_computation'
|
|
self.method_period = '1'
|
|
else:
|
|
self.method_period = '12'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# CONSTRAINT METHODS
|
|
# -------------------------------------------------------------------------
|
|
@api.constrains('active', 'state')
|
|
def _check_active(self):
|
|
for record in self:
|
|
if not record.active and record.state != 'close':
|
|
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)
|
|
))
|
|
|
|
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('import_file', False):
|
|
vals['state'] = 'draft'
|
|
if 'category_id' in vals:
|
|
changed_vals = self.onchange_category_id_values(vals['category_id'])['value']
|
|
# To avoid to overwrite vals explicitly set by the import
|
|
[changed_vals.pop(key, None) for key in vals.keys()]
|
|
vals.update(changed_vals)
|
|
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 get_formview_id(self, access_uid=None):
|
|
""" Overriding this method to redirect user to correct form view based on asset type """
|
|
for vid, view_type in self._get_views(self.asset_type):
|
|
if view_type == 'form':
|
|
return vid
|
|
|
|
# -------------------------------------------------------------------------
|
|
# BOARD COMPUTATION
|
|
# -------------------------------------------------------------------------
|
|
def _compute_board_amount(self, residual_amount, period_start_date, period_end_date, days_already_depreciated, days_left_to_depreciated, residual_declining):
|
|
number_days = self._get_delta_days(period_start_date, period_end_date)
|
|
total_days = number_days + days_already_depreciated
|
|
|
|
if self.method in ('degressive', 'degressive_then_linear'):
|
|
# Declining by year but divided per month
|
|
# We compute the amount of the period based on ratio how many days there are in the period
|
|
# e.g: monthly period = 30 days --> (30/360) * 12000 * 0.4
|
|
# => For each month in the year we will decline the same amount.
|
|
amount = (number_days / DAYS_PER_YEAR) * residual_declining * self.method_progress_factor
|
|
else:
|
|
computed_linear_amount = (self.total_depreciable_value * total_days / self.asset_lifetime_days) + residual_amount - self.total_depreciable_value
|
|
if float_compare(residual_amount, 0, precision_rounding=self.currency_id.rounding) >= 0:
|
|
linear_amount = min(computed_linear_amount, residual_amount)
|
|
amount = max(linear_amount, 0)
|
|
else:
|
|
linear_amount = max(computed_linear_amount, residual_amount)
|
|
amount = min(linear_amount, 0)
|
|
|
|
if self.method == 'degressive_then_linear' and days_left_to_depreciated != 0:
|
|
linear_amount = number_days * self.total_depreciable_value / self.asset_lifetime_days
|
|
amount = max(linear_amount, amount, key=abs)
|
|
|
|
# if self.method == 'degressif_chelou' and days_left_to_depreciated != 0:
|
|
# linear_amount = number_days * residual_declining / days_left_to_depreciated
|
|
# if float_compare(residual_amount, 0, precision_rounding=self.currency_id.rounding) >= 0:
|
|
# amount = max(linear_amount, amount)
|
|
# else:
|
|
# amount = min(linear_amount, amount)
|
|
|
|
|
|
if abs(residual_amount) < abs(amount) or total_days >= 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 equals to the residual amount.
|
|
amount = residual_amount
|
|
return number_days, self.currency_id.round(amount)
|
|
|
|
def compute_depreciation_board(self):
|
|
self.ensure_one()
|
|
new_depreciation_moves_data = self._recompute_board()
|
|
|
|
# Need to unlink draft move before adding new one because if we create new move before, it will cause an error
|
|
# in the compute for the depreciable/cumulative value
|
|
self.depreciation_move_ids.filtered(lambda mv: mv.state == 'draft').unlink()
|
|
new_depreciation_moves = self.env['account.move'].create(new_depreciation_moves_data)
|
|
if self.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._post()
|
|
|
|
return True
|
|
|
|
def _recompute_board(self):
|
|
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
|
|
if not posted_depreciation_move_ids:
|
|
residual_amount += imported_amount
|
|
residual_declining = residual_amount
|
|
|
|
# Days already depreciated
|
|
days_already_depreciated = sum(posted_depreciation_move_ids.mapped('asset_number_days'))
|
|
days_left_to_depreciated = self.asset_lifetime_days - days_already_depreciated
|
|
days_already_added = sum([(mv.date - mv.asset_depreciation_beginning_date).days + 1 for mv in posted_depreciation_move_ids])
|
|
|
|
start_depreciation_date = self.paused_prorata_date + relativedelta(days=days_already_added)
|
|
final_depreciation_date = self.paused_prorata_date + relativedelta(months=int(self.method_period) * self.method_number, days=-1)
|
|
final_depreciation_date = self._get_end_period_date(final_depreciation_date)
|
|
|
|
depreciation_move_values = []
|
|
if not float_is_zero(self.value_residual, precision_rounding=self.currency_id.rounding):
|
|
while days_already_depreciated < self.asset_lifetime_days:
|
|
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')
|
|
|
|
days, amount = self._compute_board_amount(residual_amount, start_depreciation_date, period_end_depreciation_date, days_already_depreciated, days_left_to_depreciated, residual_declining)
|
|
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):
|
|
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,
|
|
}))
|
|
days_already_depreciated += days
|
|
|
|
if period_end_depreciation_date == period_end_fiscalyear_date:
|
|
days_left_to_depreciated = self.asset_lifetime_days - days_already_depreciated
|
|
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
|
|
))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# 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):
|
|
form_ref = {
|
|
'purchase': 'account_asset.view_account_asset_form',
|
|
'sale': 'account_asset.view_account_asset_revenue_form',
|
|
'expense': 'account_asset.view_account_asset_expense_form',
|
|
}.get(self.asset_type)
|
|
|
|
return {
|
|
'name': _('Save model'),
|
|
'views': [[self.env.ref(form_ref).id, "form"]],
|
|
'res_model': 'account.asset',
|
|
'type': 'ir.actions.act_window',
|
|
'context': {
|
|
'default_asset_type': self.asset_type,
|
|
'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):
|
|
return {
|
|
'name': _('Gross Increase'),
|
|
'view_mode': 'tree,form',
|
|
'res_model': 'account.asset',
|
|
'view_id': False,
|
|
'type': 'ir.actions.act_window',
|
|
'domain': [('id', 'in', self.children_ids.ids)],
|
|
'views': self.env['account.asset']._get_views(self.asset_type),
|
|
}
|
|
|
|
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 = {
|
|
'purchase': (_('Asset created'), _('An asset has been created for this move:')),
|
|
'sale': (_('Deferred revenue created'), _('A deferred revenue has been created for this move:')),
|
|
'expense': (_('Deferred expense created'), _('A deferred expense has been created for this move:')),
|
|
}[asset.asset_type]
|
|
msg = asset_name[1] + f' {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)
|
|
if not asset.depreciation_move_ids:
|
|
asset.compute_depreciation_board()
|
|
asset._check_depreciations()
|
|
asset.depreciation_move_ids.filtered(lambda move: move.state != 'posted')._post()
|
|
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 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
|
|
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 "")
|
|
)
|
|
full_asset.write({'state': 'close'})
|
|
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 = '<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 <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:<br>%(entries)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'),
|
|
entries=entries,
|
|
)
|
|
asset._message_log(body=msg)
|
|
else:
|
|
asset._message_log(body=_('Asset Cancelled'))
|
|
asset.depreciation_move_ids.filtered(lambda m: m.state == 'draft').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']
|
|
asset_type = self.asset_type
|
|
else:
|
|
asset_type = self[0].asset_type
|
|
views = [v for v in self._get_views(asset_type) if v[1] in view_mode]
|
|
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': {
|
|
**self._context,
|
|
'asset_type': asset_type,
|
|
'default_asset_type': asset_type
|
|
}
|
|
}
|
|
if asset_type == 'sale':
|
|
action['name'] = _('Deferred Revenue')
|
|
elif asset_type == 'expense':
|
|
action['name'] = _('Deferred Expense')
|
|
|
|
return action
|
|
|
|
# -------------------------------------------------------------------------
|
|
# HELPER METHODS
|
|
# -------------------------------------------------------------------------
|
|
@api.model
|
|
def _get_views(self, asset_type):
|
|
form_view = self.env.ref('account_asset.view_account_asset_form')
|
|
tree_view = self.env.ref('account_asset.view_account_asset_purchase_tree')
|
|
if asset_type == 'sale':
|
|
form_view = self.env.ref('account_asset.view_account_asset_revenue_form')
|
|
tree_view = self.env.ref('account_asset.view_account_asset_sale_tree')
|
|
elif asset_type == 'expense':
|
|
form_view = self.env.ref('account_asset.view_account_asset_expense_form')
|
|
tree_view = self.env.ref('account_asset.view_account_asset_expense_tree')
|
|
return [[tree_view.id, "tree"], [form_view.id, "form"]]
|
|
|
|
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.
|
|
"""
|
|
self._cancel_future_moves(date)
|
|
all_lines_before_date = self.depreciation_move_ids.filtered(lambda x: x.date <= date)
|
|
|
|
days_already_depreciated = sum(all_lines_before_date.mapped('asset_number_days'))
|
|
days_left = self.asset_lifetime_days - days_already_depreciated
|
|
days_to_add = sum([
|
|
(mv.date - mv.asset_depreciation_beginning_date).days + 1 for mv in
|
|
all_lines_before_date.filtered(lambda x: not x.reversed_entry_id and not x.reversal_move_id)
|
|
])
|
|
|
|
imported_amount = self.already_depreciated_amount_import if not all_lines_before_date else 0
|
|
value_residual = self.value_residual + self.already_depreciated_amount_import if not all_lines_before_date else self.value_residual
|
|
|
|
beginning_depreciation_date = self.paused_prorata_date + relativedelta(days=days_to_add)
|
|
|
|
days_depreciated, amount = self._compute_board_amount(value_residual, beginning_depreciation_date, date, days_already_depreciated, days_left, value_residual)
|
|
|
|
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
|
|
"""
|
|
to_reverse = self.env['account.move']
|
|
to_cancel = self.env['account.move']
|
|
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'
|
|
and m.date > date
|
|
))
|
|
lock_date = asset.company_id._get_user_fiscal_lock_date()
|
|
for move in posted_moves:
|
|
if move.inalterable_hash or move.date <= lock_date:
|
|
to_reverse += move
|
|
else:
|
|
to_cancel += move
|
|
to_reverse._reverse_moves(cancel=True)
|
|
to_cancel.button_draft()
|
|
self.depreciation_move_ids.filtered(lambda m: m.state == 'draft').unlink()
|
|
|
|
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 if asset.asset_type == 'sale' else {},
|
|
'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.price_subtotal, -initial_amount) + dict_invoice.get(invoice_line.account_id, 0)
|
|
invoice_amount += copysign(invoice_line.price_subtotal, -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
|