# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import calendar from datetime import date, datetime from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.tools import float_compare, float_is_zero class AccountAssetCategory(models.Model): _name = 'account.asset.category' _description = 'Asset category' _inherit = ['mail.thread', 'mail.activity.mixin', 'analytic.mixin'] exclude_types = ['asset_receivable', 'asset_cash', 'liability_payable', 'liability_credit_card', 'equity', 'equity_unaffected'] active = fields.Boolean(default=True) name = fields.Char(required=True, index=True, string="Asset Type") account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') # analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tag') account_asset_id = fields.Many2one('account.account', string='Asset Account', required=True, domain=[('account_type', 'not in', exclude_types), ('deprecated', '=', False)], help="Account used to record the purchase of the asset at its original price.") account_depreciation_id = fields.Many2one('account.account', string='Depreciation Entries: Asset Account', required=True, domain=[('account_type', 'not in', exclude_types), ('deprecated', '=', False)], help="Account used in the depreciation entries, to decrease the asset value.") account_depreciation_expense_id = fields.Many2one('account.account', string='Depreciation Entries: Expense Account', required=True, domain=[('account_type', 'not in', exclude_types), ('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', required=True) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, 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") method_number = fields.Integer(string='Number of Depreciations', default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer(string='Period Length', default=1, help="State here the time between 2 depreciations, in months", required=True) method_progress_factor = fields.Float('Degressive Factor', default=0.3) method_time = fields.Selection([('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, default='number', help="Choose the method to use to compute the dates and number of entries.\n" " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") method_end = fields.Date('Ending date') prorata = fields.Boolean(string='Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the ' 'purchase date instead of the first of January') open_asset = fields.Boolean(string='Auto-Confirm Assets', help="Check this if you want to automatically confirm the assets " "of this category when created by invoices.") group_entries = fields.Boolean(string='Group Journal Entries', help="Check this if you want to group the generated entries by categories.") type = fields.Selection([('sale', 'Sale: Revenue Recognition'), ('purchase', 'Purchase: Asset')], required=True, index=True, default='purchase') date_first_depreciation = fields.Selection([ ('last_day_period', 'Based on Last Day of Purchase Period'), ('manual', 'Manual (Defaulted on Purchase Date)')], string='Depreciation Dates', default='manual', required=True, help='The way to compute the date of the first depreciation.\n' ' * Based on last day of purchase period: The depreciation dates will' ' be based on the last day of the purchase month or the purchase' ' year (depending on the periodicity of the depreciations).\n' ' * Based on purchase date: The depreciation dates will be based on the purchase date.') @api.onchange('account_asset_id') def onchange_account_asset(self): if self.type == "purchase": self.account_depreciation_id = self.account_asset_id elif self.type == "sale": self.account_depreciation_expense_id = self.account_asset_id @api.onchange('type') def onchange_type(self): if self.type == 'sale': self.prorata = True self.method_period = 1 else: self.method_period = 12 @api.onchange('method_time') def _onchange_method_time(self): if self.method_time != 'number': self.prorata = False class AccountAssetAsset(models.Model): _name = 'account.asset.asset' _description = 'Asset/Revenue Recognition' _inherit = ['mail.thread', 'mail.activity.mixin', 'analytic.mixin'] entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) value = fields.Monetary(string='Gross Value', required=True, readonly=True, states={'draft': [('readonly', False)]}) currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id.id) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company) note = fields.Text() category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today) state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, 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" "You can manually close an asset when the depreciation is over. If the last line" " of depreciation is posted, the asset automatically goes in that status.") active = fields.Boolean(default=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], 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") method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, help="The amount of time between two depreciations, in months") method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) value_residual = fields.Monetary(compute='_amount_residual', string='Residual Value') method_time = fields.Selection([('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, help="Choose the method to use to compute the dates and number of entries.\n" " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") 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 asset date (purchase date) ' 'instead of the first January / Start date of fiscal year') depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}) salvage_value = fields.Monetary(string='Salvage Value', readonly=True, states={'draft': [('readonly', False)]}, help="It is the amount you plan to have that you cannot depreciate.") invoice_id = fields.Many2one('account.move', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) type = fields.Selection(related="category_id.type", string='Type', required=True) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') # analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tag') date_first_depreciation = fields.Selection([ ('last_day_period', 'Based on Last Day of Purchase Period'), ('manual', 'Manual')], string='Depreciation Dates', default='manual', readonly=True, states={'draft': [('readonly', False)]}, required=True, help='The way to compute the date of the first depreciation.\n' ' * Based on last day of purchase period: The depreciation' ' dates will be based on the last day of the purchase month or the ' 'purchase year (depending on the periodicity of the depreciations).\n' ' * Based on purchase date: The depreciation dates will be based on the purchase date.\n') first_depreciation_manual_date = fields.Date( string='First Depreciation Date', readonly=True, states={'draft': [('readonly', False)]}, help='Note that this date does not alter the computation of the first ' 'journal entry in case of prorata temporis assets. It simply changes its accounting date' ) def unlink(self): for asset in self: if asset.state in ['open', 'close']: raise UserError(_('You cannot delete a document that is in %s state.') % (asset.state,)) for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: raise UserError(_('You cannot delete a document that contains posted entries.')) return super(AccountAssetAsset, self).unlink() @api.model def _cron_generate_entries(self): self.compute_generated_entries(datetime.today()) @api.model def compute_generated_entries(self, date, asset_type=None): # Entries generated : one by grouped category and one by asset from ungrouped category created_move_ids = [] type_domain = [] if asset_type: type_domain = [('type', '=', asset_type)] ungrouped_assets = self.env['account.asset.asset'].search(type_domain + [('state', '=', 'open'), ('category_id.group_entries', '=', False)]) created_move_ids += ungrouped_assets._compute_entries(date, group_entries=False) for grouped_category in self.env['account.asset.category'].search(type_domain + [('group_entries', '=', True)]): assets = self.env['account.asset.asset'].search([('state', '=', 'open'), ('category_id', '=', grouped_category.id)]) created_move_ids += assets._compute_entries(date, group_entries=True) return created_move_ids def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): amount = 0 if sequence == undone_dotation_number: amount = residual_amount else: if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) if self.prorata: amount = amount_to_depr / self.method_number if sequence == 1: date = self.date if self.method_period % 12 != 0: month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 amount = (amount_to_depr / self.method_number) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates(date)['date_to'] - date).days + 1 amount = (amount_to_depr / self.method_number) / total_days * days elif self.method == 'degressive': amount = residual_amount * self.method_progress_factor if self.prorata: if sequence == 1: date = self.date if self.method_period % 12 != 0: month_days = calendar.monthrange(date.year, date.month)[1] days = month_days - date.day + 1 amount = (residual_amount * self.method_progress_factor) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates(date)['date_to'] - date).days + 1 amount = (residual_amount * self.method_progress_factor) / total_days * days return amount def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): undone_dotation_number = self.method_number if self.method_time == 'end': end_date = self.method_end undone_dotation_number = 0 while depreciation_date <= end_date: depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) undone_dotation_number += 1 if self.prorata: undone_dotation_number += 1 return undone_dotation_number def compute_depreciation_board(self): self.ensure_one() posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date) unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) # Remove old unposted depreciation lines. We cannot use unlink() with One2many field commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] if self.value_residual != 0.0: amount_to_depr = residual_amount = self.value_residual # if we already have some previous validated entries, starting date is last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: last_depreciation_date = fields.Date.from_string(posted_depreciation_line_ids[-1].depreciation_date) depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: # depreciation_date computed from the purchase date depreciation_date = self.date if self.date_first_depreciation == 'last_day_period': # depreciation_date = the last day of the month depreciation_date = depreciation_date + relativedelta(day=31) # ... or fiscalyear depending the number of period if self.method_period == 12: depreciation_date = depreciation_date + relativedelta(month=int(self.company_id.fiscalyear_last_month)) depreciation_date = depreciation_date + relativedelta(day=int(self.company_id.fiscalyear_last_day)) if depreciation_date < self.date: depreciation_date = depreciation_date + relativedelta(years=1) elif self.first_depreciation_manual_date and self.first_depreciation_manual_date != self.date: # depreciation_date set manually from the 'first_depreciation_manual_date' field depreciation_date = self.first_depreciation_manual_date total_days = (depreciation_date.year % 4) and 365 or 366 month_day = depreciation_date.day undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): sequence = x + 1 amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) amount = self.currency_id.round(amount) if float_is_zero(amount, precision_rounding=self.currency_id.rounding): continue residual_amount -= amount vals = { 'amount': amount, 'asset_id': self.id, 'sequence': sequence, 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), 'depreciation_date': depreciation_date, } commands.append((0, False, vals)) depreciation_date = depreciation_date + relativedelta(months=+self.method_period) if month_day > 28 and self.date_first_depreciation == 'manual': max_day_in_month = calendar.monthrange(depreciation_date.year, depreciation_date.month)[1] depreciation_date = depreciation_date.replace(day=min(max_day_in_month, month_day)) # datetime doesn't take into account that the number of days is not the same for each month if not self.prorata and self.method_period % 12 != 0 and self.date_first_depreciation == 'last_day_period': max_day_in_month = calendar.monthrange(depreciation_date.year, depreciation_date.month)[1] depreciation_date = depreciation_date.replace(day=max_day_in_month) self.write({'depreciation_line_ids': commands}) return True def validate(self): self.write({'state': 'open'}) fields = [ 'method', 'method_number', 'method_period', 'method_end', 'method_progress_factor', 'method_time', 'salvage_value', 'invoice_id', ] ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields) for asset in self: tracked_fields = ref_tracked_fields.copy() if asset.method == 'linear': del(tracked_fields['method_progress_factor']) if asset.method_time != 'end': del(tracked_fields['method_end']) else: del(tracked_fields['method_number']) dummy, tracking_value_ids = asset._mail_track(tracked_fields, dict.fromkeys(fields)) asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids) def _return_disposal_view(self, move_ids): name = _('Disposal Move') view_mode = 'form' if len(move_ids) > 1: name = _('Disposal Moves') view_mode = 'tree,form' return { 'name': name, 'view_type': 'form', 'view_mode': view_mode, 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'target': 'current', 'res_id': move_ids[0], } def _get_disposal_moves(self): move_ids = [] for asset in self: unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(lambda x: not x.move_check) if unposted_depreciation_line_ids: old_values = { 'method_end': asset.method_end, 'method_number': asset.method_number, } # Remove all unposted depr. lines commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] # Create a new depr. line with the residual amount and post it sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1 today = fields.Datetime.today() vals = { 'amount': asset.value_residual, 'asset_id': asset.id, 'sequence': sequence, 'name': (asset.code or '') + '/' + str(sequence), 'remaining_value': 0, 'depreciated_value': asset.value - asset.salvage_value, # the asset is completely depreciated 'depreciation_date': today, } commands.append((0, False, vals)) asset.write({'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence}) tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_end']) changes, tracking_value_ids = asset._mail_track(tracked_fields, old_values) if changes: asset.message_post(subject=_('Asset sold or disposed. Accounting entry awaiting for validation.'), tracking_value_ids=tracking_value_ids) move_ids += asset.depreciation_line_ids[-1].create_move(post_move=False) return move_ids def set_to_close(self): move_ids = self._get_disposal_moves() if move_ids: return self._return_disposal_view(move_ids) # Fallback, as if we just clicked on the smartbutton return self.open_entries() def set_to_draft(self): self.write({'state': 'draft'}) @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check', 'depreciation_line_ids.amount') def _amount_residual(self): for rec in self: total_amount = 0.0 for line in rec.depreciation_line_ids: if line.move_check: total_amount += line.amount rec.value_residual = rec.value - total_amount - rec.salvage_value @api.onchange('company_id') def onchange_company_id(self): self.currency_id = self.company_id.currency_id.id @api.onchange('date_first_depreciation') def onchange_date_first_depreciation(self): for record in self: if record.date_first_depreciation == 'manual': record.first_depreciation_manual_date = record.date @api.depends('depreciation_line_ids.move_id') def _entry_count(self): for asset in self: res = self.env['account.asset.depreciation.line'].search_count([('asset_id', '=', asset.id), ('move_id', '!=', False)]) asset.entry_count = res or 0 @api.constrains('prorata', 'method_time') def _check_prorata(self): if self.prorata and self.method_time != 'number': raise ValidationError(_('Prorata temporis can be applied only for the "number of depreciations" time method.')) @api.onchange('category_id') def onchange_category_id(self): vals = self.onchange_category_id_values(self.category_id.id) # We cannot use 'write' on an object that doesn't exist yet if vals: for k, v in vals['value'].items(): setattr(self, k, v) def onchange_category_id_values(self, category_id): if category_id: category = self.env['account.asset.category'].browse(category_id) return { 'value': { 'method': category.method, 'method_number': category.method_number, 'method_time': category.method_time, 'method_period': category.method_period, 'method_progress_factor': category.method_progress_factor, 'method_end': category.method_end, 'prorata': category.prorata, 'date_first_depreciation': category.date_first_depreciation, 'account_analytic_id': category.account_analytic_id.id, 'analytic_distribution': category.analytic_distribution, # 'analytic_tag_ids': [(6, 0, category.analytic_tag_ids.ids)], } } @api.onchange('method_time') def onchange_method_time(self): if self.method_time != 'number': self.prorata = False def copy_data(self, default=None): if default is None: default = {} default['name'] = self.name + _(' (copy)') return super(AccountAssetAsset, self).copy_data(default) def _compute_entries(self, date, group_entries=False): depreciation_ids = self.env['account.asset.depreciation.line'].search([ ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), ('move_check', '=', False)]) if group_entries: return depreciation_ids.create_grouped_move() return depreciation_ids.create_move() @api.model_create_multi def create(self, vals_list): assets = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals_list) for asset in assets: asset.sudo().compute_depreciation_board() return assets def write(self, vals): res = super(AccountAssetAsset, self).write(vals) if 'depreciation_line_ids' not in vals and 'state' not in vals: for rec in self: rec.compute_depreciation_board() return res def open_entries(self): move_ids = [] for asset in self: for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: move_ids.append(depreciation_line.move_id.id) return { 'name': _('Journal Entries'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('id', 'in', move_ids)], } class AccountAssetDepreciationLine(models.Model): _name = 'account.asset.depreciation.line' _description = 'Asset depreciation line' name = fields.Char(string='Depreciation Name', required=True, index=True) sequence = fields.Integer(required=True) asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True, ondelete='cascade') parent_state = fields.Selection(related='asset_id.state', string='State of Asset') amount = fields.Monetary(string='Current Depreciation', required=True) remaining_value = fields.Monetary(string='Next Period Depreciation', required=True) depreciated_value = fields.Monetary(string='Cumulative Depreciation', required=True) depreciation_date = fields.Date('Depreciation Date', index=True) move_id = fields.Many2one('account.move', string='Depreciation Entry') move_check = fields.Boolean(compute='_get_move_check', string='Linked', store=True) move_posted_check = fields.Boolean(compute='_get_move_posted_check', string='Posted', store=True) currency_id = fields.Many2one('res.currency', string='Currency', related='asset_id.currency_id', readonly=True) @api.depends('move_id') def _get_move_check(self): for line in self: line.move_check = bool(line.move_id) @api.depends('move_id.state') def _get_move_posted_check(self): for line in self: line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False def create_move(self, post_move=True): created_moves = self.env['account.move'] for line in self: if line.move_id: raise UserError(_('This depreciation is already linked to a journal entry. Please post or delete it.')) move_vals = self._prepare_move(line) move = self.env['account.move'].create(move_vals) line.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: created_moves.filtered(lambda m: any(m.asset_depreciation_ids.mapped('asset_id.category_id.open_asset'))).action_post() return [x.id for x in created_moves] def _prepare_move(self, line): category_id = line.asset_id.category_id account_analytic_id = line.asset_id.account_analytic_id # analytic_tag_ids = line.asset_id.analytic_tag_ids analytic_distribution = line.asset_id.analytic_distribution depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id prec = company_currency.decimal_places amount = current_currency._convert( line.amount, company_currency, line.asset_id.company_id, depreciation_date) asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, # 'analytic_account_id': account_analytic_id.id if category_id.type == 'sale' else False, # 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'sale' else False, 'analytic_distribution': analytic_distribution, 'currency_id': company_currency != current_currency and current_currency.id or company_currency.id, 'amount_currency': - 1.0 * line.amount } move_line_2 = { 'name': asset_name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'partner_id': line.asset_id.partner_id.id, # 'analytic_account_id': account_analytic_id.id if category_id.type == 'purchase' else False, # 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'purchase' else False, 'analytic_distribution': analytic_distribution, 'currency_id': company_currency != current_currency and current_currency.id or company_currency.id, 'amount_currency': line.amount, } move_vals = { 'ref': line.asset_id.code, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } return move_vals def _prepare_move_grouped(self): asset_id = self[0].asset_id category_id = asset_id.category_id # we can suppose that all lines have the same category account_analytic_id = asset_id.account_analytic_id # analytic_tag_ids = asset_id.analytic_tag_ids analytic_distribution = asset_id.analytic_distribution depreciation_date = self.env.context.get('depreciation_date') or fields.Date.context_today(self) amount = 0.0 for line in self: # Sum amount of all depreciation lines company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id company = line.asset_id.company_id amount += current_currency._convert(line.amount, company_currency, company, fields.Date.today()) name = category_id.name + _(' (grouped)') move_line_1 = { 'name': name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0, 'credit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': account_analytic_id.id if category_id.type == 'sale' else False, 'analytic_distribution': analytic_distribution, # 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'sale' else False, } move_line_2 = { 'name': name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0, 'debit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': account_analytic_id.id if category_id.type == 'purchase' else False, 'analytic_distribution': analytic_distribution, # 'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)] if category_id.type == 'purchase' else False, } move_vals = { 'ref': category_id.name, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } return move_vals def create_grouped_move(self, post_move=True): if not self.exists(): return [] created_moves = self.env['account.move'] move = self.env['account.move'].create(self._prepare_move_grouped()) self.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: created_moves.action_post() return [x.id for x in created_moves] def post_lines_and_close_asset(self): # we re-evaluate the assets to determine whether we can close them for line in self: line.log_message_when_posted() asset = line.asset_id if asset.currency_id.is_zero(asset.value_residual): asset.message_post(body=_("Document closed.")) asset.write({'state': 'close'}) def log_message_when_posted(self): def _format_message(message_description, tracked_values): message = '' if message_description: message = '%s' % message_description for name, values in tracked_values.items(): message += '