# Copyright 2009-2018 Noviat # Copyright 2019 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import calendar from datetime import datetime from dateutil.relativedelta import relativedelta import logging from sys import exc_info from traceback import format_exception from odoo import api, fields, models, _ import odoo.addons.decimal_precision as dp from odoo.exceptions import UserError from odoo.osv import expression from functools import reduce _logger = logging.getLogger(__name__) class DummyFy(object): def __init__(self, *args, **argv): for key, arg in argv.items(): setattr(self, key, arg) class AccountAsset(models.Model): _name = 'account.asset' _description = 'Asset' _order = 'date_start desc, code, name' account_move_line_ids = fields.One2many( comodel_name='account.move.line', inverse_name='asset_id', string='Entries', readonly=True, copy=False) move_line_check = fields.Boolean( compute='_compute_move_line_check', string='Has accounting entries') name = fields.Char( string='Asset Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}) code = fields.Char( string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) purchase_value = fields.Float( string='Purchase Value', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="This amount represent the initial value of the asset." "\nThe Depreciation Base is calculated as follows:" "\nPurchase Value - Salvage Value.") salvage_value = fields.Float( string='Salvage Value', digits=dp.get_precision('Account'), readonly=True, states={'draft': [('readonly', False)]}, help="The estimated value that an asset will realize upon " "its sale at the end of its useful life.\n" "This value is used to determine the depreciation amounts.") depreciation_base = fields.Float( compute='_compute_depreciation_base', digits=dp.get_precision('Account'), string='Depreciation Base', store=True, help="This amount represent the depreciation base " "of the asset (Purchase Value - Salvage Value.") value_residual = fields.Float( compute='_compute_depreciation', digits=dp.get_precision('Account'), string='Residual Value', store=True) value_depreciated = fields.Float( compute='_compute_depreciation', digits=dp.get_precision('Account'), string='Depreciated Value', store=True) note = fields.Text('Note') profile_id = fields.Many2one( comodel_name='account.asset.profile', string='Asset Profile', change_default=True, readonly=True, required=True, states={'draft': [('readonly', False)]}) group_ids = fields.Many2many( comodel_name='account.asset.group', relation="account_asset_group_rel", column1="asset_id", column2="group_id", string='Asset Groups') date_start = fields.Date( string='Asset Start Date', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="You should manually add depreciation lines " "with the depreciations of previous fiscal years " "if the Depreciation Start Date is different from the date " "for which accounting entries need to be generated.") date_remove = fields.Date(string='Asset Removal Date', readonly=True) state = fields.Selection( selection=[ ('draft', 'Draft'), ('open', 'Running'), ('close', 'Close'), ('removed', 'Removed'), ], string='Status', required=True, default='draft', copy=False, 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 " "to the accounting.\n" "If the last depreciation line is posted, " "the asset goes into the 'Close' status.\n" "When the removal entries are generated, " "the asset goes into the 'Removed' status.") active = fields.Boolean(default=True) partner_id = fields.Many2one( comodel_name='res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) method = fields.Selection( selection=lambda self: self.env[ 'account.asset.profile']._selection_method(), string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', help="Choose the method to use to compute " "the amount of depreciation lines.\n" " * Linear: Calculated on basis of: " "Gross Value / Number of Depreciations\n" " * Degressive: Calculated on basis of: " "Residual Value * Degressive Factor" " * Degressive-Linear (only for Time Method = Year): " "Degressive becomes linear when the annual linear " "depreciation exceeds the annual degressive depreciation") method_number = fields.Integer( string='Number of Years', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of years needed to depreciate your asset") method_period = fields.Selection( selection=lambda self: self.env[ 'account.asset.profile']._selection_method_period(), string='Period Length', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='year', help="Period length for the depreciation accounting entries") method_end = fields.Date( string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) method_progress_factor = fields.Float( string='Degressive Factor', readonly=True, states={'draft': [('readonly', False)]}, default=0.3) method_time = fields.Selection( selection=lambda self: self.env[ 'account.asset.profile']._selection_method_time(), string='Time Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='year', help="Choose the method to use to compute the dates and " "number of depreciation lines.\n" " * Number of Years: Specify the number of years " "for the depreciation.\n" # " * Number of Depreciations: Fix the number of " # "depreciation lines and the time between 2 depreciations.\n" # " * Ending Date: Choose the time between 2 depreciations " # "and the date the depreciations won't go beyond." ) days_calc = fields.Boolean( string='Calculate by days', default=False, help="Use number of days to calculate depreciation amount", ) use_leap_years = fields.Boolean( string='Use leap years', default=False, help="If not set, the system will distribute evenly the amount to " "amortize across the years, based on the number of years. " "So the amount per year will be the " "depreciation base / number of years.\n " "If set, the system will consider if the current year " "is a leap year. The amount to depreciate per year will be " "calculated as depreciation base / (depreciation end date - " "start date + 1) * days in the current year.", ) prorata = fields.Boolean( string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, help="Indicates that the first depreciation entry for this asset " "have to be done from the depreciation start date instead " "of the first day of the fiscal year.") depreciation_line_ids = fields.One2many( comodel_name='account.asset.line', inverse_name='asset_id', string='Depreciation Lines', copy=False, readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one( comodel_name='res.company', string='Company', required=True, readonly=True, default=lambda self: self._default_company_id()) company_currency_id = fields.Many2one( comodel_name='res.currency', related='company_id.currency_id', string='Company Currency', store=True, readonly=True) account_analytic_id = fields.Many2one( comodel_name='account.analytic.account', string='Analytic account') @api.model def _default_company_id(self): return self.env['res.company']._company_default_get('account.asset') @api.multi def _compute_move_line_check(self): for asset in self: for line in asset.depreciation_line_ids: if line.move_id: asset.move_line_check = True break @api.depends('purchase_value', 'salvage_value', 'method') @api.multi def _compute_depreciation_base(self): for asset in self: if asset.method in ['linear-limit', 'degr-limit']: asset.depreciation_base = asset.purchase_value else: asset.depreciation_base = \ asset.purchase_value - asset.salvage_value @api.multi @api.depends('depreciation_base', 'depreciation_line_ids.type', 'depreciation_line_ids.amount', 'depreciation_line_ids.previous_id', 'depreciation_line_ids.init_entry', 'depreciation_line_ids.move_check',) def _compute_depreciation(self): for asset in self: lines = asset.depreciation_line_ids.filtered( lambda l: l.type in ('depreciate', 'remove') and (l.init_entry or l.move_check)) value_depreciated = sum([l.amount for l in lines]) residual = asset.depreciation_base - value_depreciated depreciated = value_depreciated asset.update({ 'value_residual': residual, 'value_depreciated': depreciated }) @api.multi @api.constrains('method', 'method_time') def _check_method(self): for asset in self: if asset.method == 'degr-linear' and asset.method_time != 'year': raise UserError( _("Degressive-Linear is only supported for Time Method = " "Year.")) @api.multi @api.constrains('date_start', 'method_end', 'method_time') def _check_dates(self): for asset in self: if asset.method_time == 'end': if asset.method_end <= asset.date_start: raise UserError( _("The Start Date must precede the Ending Date.")) @api.onchange('purchase_value', 'salvage_value', 'date_start', 'method') def _onchange_purchase_salvage_value(self): if self.method in ['linear-limit', 'degr-limit']: self.depreciation_base = self.purchase_value or 0.0 else: purchase_value = self.purchase_value or 0.0 salvage_value = self.salvage_value or 0.0 self.depreciation_base = purchase_value - salvage_value dl_create_line = self.depreciation_line_ids.filtered( lambda r: r.type == 'create') if dl_create_line: dl_create_line.update({ 'amount': self.depreciation_base, 'line_date': self.date_start }) @api.onchange('profile_id') def _onchange_profile_id(self): for line in self.depreciation_line_ids: if line.move_id: raise UserError( _("You cannot change the profile of an asset " "with accounting entries.")) profile = self.profile_id if profile: self.update({ 'method': profile.method, 'method_number': profile.method_number, 'method_time': profile.method_time, 'method_period': profile.method_period, 'days_calc': profile.days_calc, 'use_leap_years': profile.use_leap_years, 'method_progress_factor': profile.method_progress_factor, 'prorata': profile.prorata, 'account_analytic_id': profile.account_analytic_id, 'group_ids': profile.group_ids, }) @api.onchange('method_time') def _onchange_method_time(self): if self.method_time != 'year': self.prorata = True @api.onchange('method_number') def _onchange_method_number(self): if self.method_number and self.method_end: self.method_end = False @api.onchange('method_end') def _onchange_method_end(self): if self.method_end and self.method_number: self.method_number = 0 @api.model def create(self, vals): if vals.get('method_time') != 'year' and not vals.get('prorata'): vals['prorata'] = True asset = super().create(vals) if self.env.context.get('create_asset_from_move_line'): # Trigger compute of depreciation_base asset.salvage_value = 0.0 asset._create_first_asset_line() return asset @api.multi def write(self, vals): if vals.get('method_time'): if vals['method_time'] != 'year' and not vals.get('prorata'): vals['prorata'] = True res = super().write(vals) for asset in self: if self.env.context.get('asset_validate_from_write'): continue asset._create_first_asset_line() if asset.profile_id.open_asset and \ self.env.context.get('create_asset_from_move_line'): asset.compute_depreciation_board() # extra context to avoid recursion asset.with_context(asset_validate_from_write=True).validate() return res def _create_first_asset_line(self): self.ensure_one() if self.depreciation_base and not self.depreciation_line_ids: asset_line_obj = self.env['account.asset.line'] line_name = self._get_depreciation_entry_name(0) asset_line_vals = { 'amount': self.depreciation_base, 'asset_id': self.id, 'name': line_name, 'line_date': self.date_start, 'init_entry': True, 'type': 'create', } asset_line = asset_line_obj.create(asset_line_vals) if self.env.context.get('create_asset_from_move_line'): asset_line.move_id = self.env.context['move_id'] @api.multi def unlink(self): for asset in self: if asset.state != 'draft': raise UserError( _("You can only delete assets in draft state.")) if asset.depreciation_line_ids.filtered( lambda r: r.type == 'depreciate' and r.move_check): raise UserError( _("You cannot delete an asset that contains " "posted depreciation lines.")) # update accounting entries linked to lines of type 'create' amls = self.with_context( allow_asset_removal=True ).mapped('account_move_line_ids') amls.write({'asset_id': False}) return super().unlink() @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = ['|', ('code', '=ilike', name + '%'), ('name', operator, name)] if operator in expression.NEGATIVE_TERM_OPERATORS: domain = ['&', '!'] + domain[1:] assets = self.search(domain + args, limit=limit) return assets.name_get() @api.multi @api.depends('name', 'code') def name_get(self): result = [] for asset in self: name = asset.name if asset.code: name = ' - '.join([asset.code, name]) result.append((asset.id, name)) return result @api.multi def validate(self): for asset in self: if asset.company_currency_id.is_zero(asset.value_residual): asset.state = 'close' else: asset.state = 'open' return True @api.multi def remove(self): self.ensure_one() ctx = dict(self.env.context, active_ids=self.ids, active_id=self.id) early_removal = False if self.method in ['linear-limit', 'degr-limit']: if self.value_residual != self.salvage_value: early_removal = True elif self.value_residual: early_removal = True if early_removal: ctx.update({'early_removal': True}) return { 'name': _("Generate Asset Removal entries"), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.asset.remove', 'target': 'new', 'type': 'ir.actions.act_window', 'context': ctx, } @api.multi def set_to_draft(self): return self.write({'state': 'draft'}) @api.multi def open_entries(self): self.ensure_one() amls = self.env['account.move.line'].search( [('asset_id', '=', self.id)], order='date ASC') am_ids = [l.move_id.id for l in amls] return { 'name': _("Journal Entries"), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', 'context': self.env.context, 'domain': [('id', 'in', am_ids)], } @api.multi def compute_depreciation_board(self): def group_lines(x, y): y.update({'amount': x['amount'] + y['amount']}) return y line_obj = self.env['account.asset.line'] digits = self.env['decimal.precision'].precision_get('Account') for asset in self: if asset.value_residual == 0.0: continue domain = [ ('asset_id', '=', asset.id), ('type', '=', 'depreciate'), '|', ('move_check', '=', True), ('init_entry', '=', True)] posted_lines = line_obj.search( domain, order='line_date desc') if posted_lines: last_line = posted_lines[0] else: last_line = line_obj domain = [ ('asset_id', '=', asset.id), ('type', '=', 'depreciate'), ('move_id', '=', False), ('init_entry', '=', False)] old_lines = line_obj.search(domain) if old_lines: old_lines.unlink() table = asset._compute_depreciation_table() if not table: continue # group lines prior to depreciation start period depreciation_start_date = asset.date_start lines = table[0]['lines'] lines1 = [] lines2 = [] flag = lines[0]['date'] < depreciation_start_date for line in lines: if flag: lines1.append(line) if line['date'] >= depreciation_start_date: flag = False else: lines2.append(line) if lines1: lines1 = [reduce(group_lines, lines1)] lines1[0]['depreciated_value'] = 0.0 table[0]['lines'] = lines1 + lines2 # check table with posted entries and # recompute in case of deviation depreciated_value_posted = depreciated_value = 0.0 if posted_lines: last_depreciation_date = last_line.line_date last_date_in_table = table[-1]['lines'][-1]['date'] if last_date_in_table <= last_depreciation_date: raise UserError( _("The duration of the asset conflicts with the " "posted depreciation table entry dates.")) for table_i, entry in enumerate(table): residual_amount_table = \ entry['lines'][-1]['remaining_value'] if entry['date_start'] <= last_depreciation_date \ <= entry['date_stop']: break if entry['date_stop'] == last_depreciation_date: table_i += 1 line_i = 0 else: entry = table[table_i] date_min = entry['date_start'] for line_i, line in enumerate(entry['lines']): residual_amount_table = line['remaining_value'] if date_min <= last_depreciation_date <= line['date']: break date_min = line['date'] if line['date'] == last_depreciation_date: line_i += 1 table_i_start = table_i line_i_start = line_i # check if residual value corresponds with table # and adjust table when needed depreciated_value_posted = depreciated_value = \ sum([l.amount for l in posted_lines]) residual_amount = asset.depreciation_base - depreciated_value amount_diff = round( residual_amount_table - residual_amount, digits) if amount_diff: # compensate in first depreciation entry # after last posting line = table[table_i_start]['lines'][line_i_start] line['amount'] -= amount_diff else: # no posted lines table_i_start = 0 line_i_start = 0 seq = len(posted_lines) depr_line = last_line last_date = table[-1]['lines'][-1]['date'] depreciated_value = depreciated_value_posted for entry in table[table_i_start:]: for line in entry['lines'][line_i_start:]: seq += 1 name = asset._get_depreciation_entry_name(seq) if line['date'] == last_date: # ensure that the last entry of the table always # depreciates the remaining value if asset.method in ['linear-limit', 'degr-limit']: depr_max = asset.depreciation_base \ - asset.salvage_value else: depr_max = asset.depreciation_base amount = depr_max - depreciated_value else: amount = line['amount'] if amount: vals = { 'previous_id': depr_line.id, 'amount': round(amount, digits), 'asset_id': asset.id, 'name': name, 'line_date': line['date'], 'line_days': line['days'], 'init_entry': entry['init'], } depreciated_value += round(amount, digits) depr_line = line_obj.create(vals) else: seq -= 1 line_i_start = 0 return True def _get_fy_duration(self, fy, option='days'): """Returns fiscal year duration. @param option: - days: duration in days - months: duration in months, a started month is counted as a full month - years: duration in calendar years, considering also leap years """ fy_date_start = fy.date_from fy_date_stop = fy.date_to days = (fy_date_stop - fy_date_start).days + 1 months = (fy_date_stop.year - fy_date_start.year) * 12 \ + (fy_date_stop.month - fy_date_start.month) + 1 if option == 'days': return days elif option == 'months': return months elif option == 'years': year = fy_date_start.year cnt = fy_date_stop.year - fy_date_start.year + 1 for i in range(cnt): cy_days = calendar.isleap(year) and 366 or 365 if i == 0: # first year if fy_date_stop.year == year: duration = (fy_date_stop - fy_date_start).days + 1 else: duration = ( datetime(year, 12, 31) - fy_date_start).days + 1 factor = float(duration) / cy_days elif i == cnt - 1: # last year duration = ( fy_date_stop - datetime(year, 1, 1)).days + 1 factor += float(duration) / cy_days else: factor += 1.0 year += 1 return factor def _get_fy_duration_factor(self, entry, firstyear): """ localization: override this method to change the logic used to calculate the impact of extended/shortened fiscal years """ duration_factor = 1.0 fy = entry['fy'] if self.prorata: if firstyear: depreciation_date_start = self.date_start fy_date_stop = entry['date_stop'] first_fy_asset_days = \ (fy_date_stop - depreciation_date_start).days + 1 first_fy_duration = self._get_fy_duration(fy, option='days') first_fy_year_factor = self._get_fy_duration( fy, option='years') duration_factor = \ float(first_fy_asset_days) / first_fy_duration \ * first_fy_year_factor else: duration_factor = self._get_fy_duration(fy, option='years') else: fy_months = self._get_fy_duration(fy, option='months') duration_factor = float(fy_months) / 12 return duration_factor def _get_depreciation_start_date(self, fy): """ In case of 'Linear': the first month is counted as a full month if the fiscal year starts in the middle of a month. """ if self.prorata: depreciation_start_date = self.date_start else: depreciation_start_date = fy.date_from return depreciation_start_date def _get_depreciation_stop_date(self, depreciation_start_date): if self.method_time == 'year' and not self.method_end: depreciation_stop_date = depreciation_start_date + \ relativedelta(years=self.method_number, days=-1) elif self.method_time == 'number': if self.method_period == 'month': depreciation_stop_date = depreciation_start_date + \ relativedelta(months=self.method_number, days=-1) elif self.method_period == 'quarter': m = [x for x in [3, 6, 9, 12] if x >= depreciation_start_date.month][0] first_line_date = depreciation_start_date \ + relativedelta(month=m, day=31) months = self.method_number * 3 depreciation_stop_date = first_line_date \ + relativedelta(months=months - 1, days=-1) elif self.method_period == 'year': depreciation_stop_date = depreciation_start_date + \ relativedelta(years=self.method_number, days=-1) elif self.method_time == 'year' and self.method_end: depreciation_stop_date = self.method_end return depreciation_stop_date def _get_first_period_amount(self, table, entry, depreciation_start_date, line_dates): """ Return prorata amount for Time Method 'Year' in case of 'Prorata Temporis' """ amount = entry.get('period_amount') if self.prorata and self.method_time == 'year': dates = [x for x in line_dates if x <= entry['date_stop']] full_periods = len(dates) - 1 amount = entry['fy_amount'] - amount * full_periods return amount def _get_amount_linear( self, depreciation_start_date, depreciation_stop_date, entry): """ Override this method if you want to compute differently the yearly amount. """ if not self.use_leap_years and self.method_number: return self.depreciation_base / self.method_number year = entry['date_stop'].year cy_days = calendar.isleap(year) and 366 or 365 days = (depreciation_stop_date - depreciation_start_date).days + 1 return (self.depreciation_base / days) * cy_days def _compute_year_amount(self, residual_amount, depreciation_start_date, depreciation_stop_date, entry): """ Localization: override this method to change the degressive-linear calculation logic according to local legislation. """ if self.method_time != 'year': raise UserError( _("The '_compute_year_amount' method is only intended for " "Time Method 'Number of Years.")) year_amount_linear = self._get_amount_linear( depreciation_start_date, depreciation_stop_date, entry) if self.method == 'linear': return year_amount_linear if self.method == 'linear-limit': if (residual_amount - year_amount_linear) < self.salvage_value: return residual_amount - self.salvage_value else: return year_amount_linear year_amount_degressive = residual_amount * \ self.method_progress_factor if self.method == 'degressive': return year_amount_degressive if self.method == 'degr-linear': if year_amount_linear > year_amount_degressive: return min(year_amount_linear, residual_amount) else: return min(year_amount_degressive, residual_amount) if self.method == 'degr-limit': if (residual_amount - year_amount_degressive) < self.salvage_value: return residual_amount - self.salvage_value else: return year_amount_degressive else: raise UserError( _("Illegal value %s in asset.method.") % self.method) def _compute_line_dates(self, table, start_date, stop_date): """ The posting dates of the accounting entries depend on the chosen 'Period Length' as follows: - month: last day of the month - quarter: last of the quarter - year: last day of the fiscal year Override this method if another posting date logic is required. """ line_dates = [] if self.method_period == 'month': line_date = start_date + relativedelta(day=31) if self.method_period == 'quarter': m = [x for x in [3, 6, 9, 12] if x >= start_date.month][0] line_date = start_date + relativedelta(month=m, day=31) elif self.method_period == 'year': line_date = table[0]['date_stop'] i = 1 while line_date < stop_date: line_dates.append(line_date) if self.method_period == 'month': line_date = line_date + relativedelta(months=1, day=31) elif self.method_period == 'quarter': line_date = line_date + relativedelta(months=3, day=31) elif self.method_period == 'year': line_date = table[i]['date_stop'] i += 1 # last entry if not (self.method_time == 'number' and len(line_dates) == self.method_number): if self.days_calc: line_dates.append(stop_date) else: line_dates.append(line_date) return line_dates def _compute_depreciation_amount_per_fiscal_year( self, table, line_dates, depreciation_start_date, depreciation_stop_date): digits = self.env['decimal.precision'].precision_get('Account') fy_residual_amount = self.depreciation_base i_max = len(table) - 1 asset_sign = self.depreciation_base >= 0 and 1 or -1 day_amount = 0.0 if self.days_calc: days = (depreciation_stop_date - depreciation_start_date).days + 1 day_amount = self.depreciation_base / days for i, entry in enumerate(table): if self.method_time == 'year': year_amount = self._compute_year_amount( fy_residual_amount, depreciation_start_date, depreciation_stop_date, entry) if self.method_period == 'year': period_amount = year_amount elif self.method_period == 'quarter': period_amount = year_amount / 4 elif self.method_period == 'month': period_amount = year_amount / 12 if i == i_max: if self.method in ['linear-limit', 'degr-limit']: fy_amount = fy_residual_amount - self.salvage_value else: fy_amount = fy_residual_amount else: firstyear = i == 0 and True or False fy_factor = self._get_fy_duration_factor( entry, firstyear) fy_amount = year_amount * fy_factor if asset_sign * (fy_amount - fy_residual_amount) > 0: fy_amount = fy_residual_amount period_amount = round(period_amount, digits) fy_amount = round(fy_amount, digits) else: fy_amount = False if self.method_time == 'number': number = self.method_number else: number = len(line_dates) period_amount = round(self.depreciation_base / number, digits) entry.update({ 'period_amount': period_amount, 'fy_amount': fy_amount, 'day_amount': day_amount, }) if self.method_time == 'year': fy_residual_amount -= fy_amount if round(fy_residual_amount, digits) == 0: break i_max = i table = table[:i_max + 1] return table def _compute_depreciation_table_lines(self, table, depreciation_start_date, depreciation_stop_date, line_dates): digits = self.env['decimal.precision'].precision_get('Account') asset_sign = 1 if self.depreciation_base >= 0 else -1 i_max = len(table) - 1 remaining_value = self.depreciation_base depreciated_value = 0.0 for i, entry in enumerate(table): lines = [] fy_amount_check = 0.0 fy_amount = entry['fy_amount'] li_max = len(line_dates) - 1 prev_date = max(entry['date_start'], depreciation_start_date) for li, line_date in enumerate(line_dates): line_days = (line_date - prev_date).days + 1 if round(remaining_value, digits) == 0.0: break if (line_date > min(entry['date_stop'], depreciation_stop_date) and not (i == i_max and li == li_max)): prev_date = line_date break else: prev_date = line_date + relativedelta(days=1) if self.method == 'degr-linear' \ and asset_sign * (fy_amount - fy_amount_check) < 0: break if i == 0 and li == 0: if entry.get('day_amount') > 0.0: amount = line_days * entry.get('day_amount') else: amount = self._get_first_period_amount( table, entry, depreciation_start_date, line_dates) amount = round(amount, digits) else: if entry.get('day_amount') > 0.0: amount = line_days * entry.get('day_amount') else: amount = entry.get('period_amount') # last year, last entry # Handle rounding deviations. if i == i_max and li == li_max: amount = remaining_value remaining_value = 0.0 else: remaining_value -= amount fy_amount_check += amount line = { 'date': line_date, 'days': line_days, 'amount': amount, 'depreciated_value': depreciated_value, 'remaining_value': remaining_value, } lines.append(line) depreciated_value += amount # Handle rounding and extended/shortened FY deviations. # # Remark: # In account_asset_management version < 8.0.2.8.0 # the FY deviation for the first FY # was compensated in the first FY depreciation line. # The code has now been simplified with compensation # always in last FT depreciation line. if self.method_time == 'year' and not entry.get('day_amount'): if round(fy_amount_check - fy_amount, digits) != 0: diff = fy_amount_check - fy_amount amount = amount - diff remaining_value += diff lines[-1].update({ 'amount': amount, 'remaining_value': remaining_value, }) depreciated_value -= diff if not lines: table.pop(i) else: entry['lines'] = lines line_dates = line_dates[li:] for i, entry in enumerate(table): if not entry['fy_amount']: entry['fy_amount'] = sum( [l['amount'] for l in entry['lines']]) def _get_fy_info(self, date): """Return an homogeneus data structure for fiscal years.""" fy_info = self.company_id.compute_fiscalyear_dates(date) if 'record' not in fy_info: fy_info['record'] = DummyFy( date_from=fy_info['date_from'], date_to=fy_info['date_to'], ) return fy_info def _compute_depreciation_table(self): table = [] if self.method_time in ['year', 'number'] \ and not self.method_number and not self.method_end: return table company = self.company_id asset_date_start = self.date_start fiscalyear_lock_date = ( company.fiscalyear_lock_date or fields.Date.to_date('1901-01-01')) depreciation_start_date = self._get_depreciation_start_date( self._get_fy_info(asset_date_start)['record']) depreciation_stop_date = self._get_depreciation_stop_date( depreciation_start_date) fy_date_start = asset_date_start while fy_date_start <= depreciation_stop_date: fy_info = self._get_fy_info(fy_date_start) table.append({ 'fy': fy_info['record'], 'date_start': fy_info['date_from'], 'date_stop': fy_info['date_to'], 'init': fiscalyear_lock_date >= fy_info['date_from'], }) fy_date_start = fy_info['date_to'] + relativedelta(days=1) # Step 1: # Calculate depreciation amount per fiscal year. # This is calculation is skipped for method_time != 'year'. line_dates = self._compute_line_dates( table, depreciation_start_date, depreciation_stop_date) table = self._compute_depreciation_amount_per_fiscal_year( table, line_dates, depreciation_start_date, depreciation_stop_date ) # Step 2: # Spread depreciation amount per fiscal year # over the depreciation periods. self._compute_depreciation_table_lines( table, depreciation_start_date, depreciation_stop_date, line_dates) return table def _get_depreciation_entry_name(self, seq): """ use this method to customise the name of the accounting entry """ return (self.code or str(self.id)) + '/' + str(seq) @api.multi def _compute_entries(self, date_end, check_triggers=False): # TODO : add ir_cron job calling this method to # generate periodical accounting entries result = [] error_log = '' if check_triggers: recompute_obj = self.env['account.asset.recompute.trigger'] recomputes = recompute_obj.sudo().search([('state', '=', 'open')]) if check_triggers and recomputes: trigger_companies = recomputes.mapped('company_id') for asset in self: if asset.company_id.id in trigger_companies.ids: asset.compute_depreciation_board() depreciations = self.env['account.asset.line'].search([ ('asset_id', 'in', self.ids), ('type', '=', 'depreciate'), ('init_entry', '=', False), ('line_date', '<=', date_end), ('move_check', '=', False)], order='line_date') for depreciation in depreciations: try: with self.env.cr.savepoint(): result += depreciation.create_move() except Exception: e = exc_info()[0] tb = ''.join(format_exception(*exc_info())) asset_ref = depreciation.asset_id.name if depreciation.asset_id.code: asset_ref = '[%s] %s' % ( depreciation.asset_id.code, asset_ref) error_log += _( "\nError while processing asset '%s': %s" ) % (asset_ref, str(e)) error_msg = _( "Error while processing asset '%s': \n\n%s" ) % (asset_ref, tb) _logger.error("%s, %s", self._name, error_msg) if check_triggers and recomputes: companies = recomputes.mapped('company_id') triggers = recomputes.filtered( lambda r: r.company_id.id in companies.ids) if triggers: recompute_vals = { 'date_completed': fields.Datetime.now(), 'state': 'done', } triggers.sudo().write(recompute_vals) return (result, error_log)