summaryrefslogtreecommitdiff
path: root/base_accounting_kit/models/account_asset.py
diff options
context:
space:
mode:
Diffstat (limited to 'base_accounting_kit/models/account_asset.py')
-rw-r--r--base_accounting_kit/models/account_asset.py800
1 files changed, 800 insertions, 0 deletions
diff --git a/base_accounting_kit/models/account_asset.py b/base_accounting_kit/models/account_asset.py
new file mode 100644
index 0000000..1afdbfd
--- /dev/null
+++ b/base_accounting_kit/models/account_asset.py
@@ -0,0 +1,800 @@
+# -*- coding: utf-8 -*-
+#############################################################################
+#
+# Cybrosys Technologies Pvt. Ltd.
+#
+# Copyright (C) 2019-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
+# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
+#
+# You can modify it under the terms of the GNU LESSER
+# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
+#
+# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
+# (LGPL v3) along with this program.
+# If not, see <http://www.gnu.org/licenses/>.
+#
+#############################################################################
+
+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 DEFAULT_SERVER_DATE_FORMAT as DF
+from odoo.tools import float_compare, float_is_zero
+
+
+class AccountAssetCategory(models.Model):
+ _name = 'account.asset.category'
+ _description = 'Asset category'
+
+ 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')
+ account_asset_id = fields.Many2one('account.account',
+ string='Asset Account', required=True,
+ domain=[('internal_type', '=', 'other'),
+ ('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=[
+ ('internal_type', '=', 'other'), ('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=[
+ ('internal_type', '=', 'other'), ('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')
+
+ @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']
+
+ 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.Float(string='Gross Value', required=True, readonly=True,
+ digits=0, 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.company.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.Float(compute='_amount_residual',
+ digits=0, 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 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.Float(string='Salvage Value', digits=0,
+ 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)
+
+ def unlink(self):
+ for asset in self:
+ if asset.state in ['open', 'close']:
+ raise UserError(
+ _('You cannot delete a document 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()
+
+ def _get_last_depreciation_date(self):
+ """
+ @param id: ids of a account.asset.asset objects
+ @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset
+ """
+ self.env.cr.execute("""
+ SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date
+ FROM account_asset_asset a
+ LEFT JOIN account_asset_depreciation_line rel ON (rel.asset_id = a.id)
+ LEFT JOIN account_move m ON (rel.move_id = m.id)
+ WHERE a.id IN %s
+ GROUP BY a.id, m.date """, (tuple(self.ids),))
+ result = dict(self.env.cr.fetchall())
+ return result
+
+ @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:
+ if self.method_period % 12 != 0:
+ date = datetime.strptime(str(self.date), '%Y-%m-%d')
+ 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(
+ depreciation_date)[
+ 'date_to'] - depreciation_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:
+ if self.method_period % 12 != 0:
+ date = datetime.strptime(str(self.date), '%Y-%m-%d')
+ 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(
+ depreciation_date)[
+ 'date_to'] - depreciation_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 = datetime.strptime(str(self.method_end), DF).date()
+ 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 self.prorata:
+ # if we already have some previous validated entries, starting date is last entry + method perio
+ if posted_depreciation_line_ids and \
+ posted_depreciation_line_ids[-1].depreciation_date:
+ last_depreciation_date = datetime.strptime(
+ posted_depreciation_line_ids[-1].depreciation_date,
+ DF).date()
+ depreciation_date = last_depreciation_date + relativedelta(
+ months=+self.method_period)
+ else:
+ depreciation_date = datetime.strptime(
+ str(self._get_last_depreciation_date()[self.id]),
+ DF).date()
+ else:
+ # depreciation_date = 1st of January of purchase year if annual valuation, 1st of
+ # purchase month in other cases
+ if self.method_period >= 12:
+ if self.company_id.fiscalyear_last_month:
+ asset_date = date(year=int(self.date.year),
+ month=int(
+ self.company_id.fiscalyear_last_month),
+ day=int(
+ self.company_id.fiscalyear_last_day)) + relativedelta(
+ days=1) + \
+ relativedelta(year=int(
+ self.date.year)) # e.g. 2018-12-31 +1 -> 2019
+ else:
+ asset_date = datetime.strptime(
+ str(self.date)[:4] + '-01-01', DF).date()
+ else:
+ asset_date = datetime.strptime(str(self.date)[:7] + '-01',
+ DF).date()
+ # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
+ if posted_depreciation_line_ids and \
+ posted_depreciation_line_ids[-1].depreciation_date:
+ last_depreciation_date = datetime.strptime(str(
+ posted_depreciation_line_ids[-1].depreciation_date),
+ DF).date()
+ depreciation_date = last_depreciation_date + relativedelta(
+ months=+self.method_period)
+ else:
+ depreciation_date = asset_date
+ day = depreciation_date.day
+ month = depreciation_date.month
+ year = depreciation_date.year
+ total_days = (year % 4) and 365 or 366
+
+ 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.strftime(DF),
+ }
+ commands.append((0, False, vals))
+ # Considering Depr. Period as months
+ depreciation_date = date(year, month, day) + relativedelta(
+ months=+self.method_period)
+ day = depreciation_date.day
+ month = depreciation_date.month
+ year = depreciation_date.year
+
+ 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._message_track(tracked_fields,
+ dict.fromkeys(
+ fields))
+ asset.message_post(subject=_('Asset created'),
+ tracking_value_ids=tracking_value_ids)
+
+ 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 = datetime.today().strftime(DF)
+ 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._message_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:
+ 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],
+ }
+ # 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 record in self:
+ total_amount = 0.0
+ for line in record.depreciation_line_ids:
+ if line.move_check:
+ total_amount += line.amount
+ record.value_residual = record.value - total_amount - record.salvage_value
+
+ @api.onchange('company_id')
+ def onchange_company_id(self):
+ self.currency_id = self.company_id.currency_id.id
+
+ @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 time method "number of depreciations".'))
+
+ @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,
+ }
+ }
+
+ @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
+ def create(self, vals):
+ asset = super(AccountAssetAsset,
+ self.with_context(mail_create_nolog=True)).create(vals)
+ asset.sudo().compute_depreciation_board()
+ return asset
+
+ 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_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.Float(string='Current Depreciation', digits=0,
+ required=True)
+ remaining_value = fields.Float(string='Next Period Depreciation', digits=0,
+ required=True)
+ depreciated_value = fields.Float(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)
+
+ @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']
+ prec = self.env['decimal.precision'].precision_get('Account')
+ if self.mapped('move_id'):
+ raise UserError(_(
+ 'This depreciation is already linked to a journal entry! Please post or delete it.'))
+ for line in self:
+ category_id = line.asset_id.category_id
+ 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
+ amount = current_currency.with_context(
+ date=depreciation_date).compute(line.amount, company_currency)
+ asset_name = line.asset_id.name + ' (%s/%s)' % (
+ line.sequence, len(line.asset_id.depreciation_line_ids))
+ partner = self.env['res.partner']._find_accounting_partner(
+ line.asset_id.partner_id)
+ 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,
+ 'journal_id': category_id.journal_id.id,
+ 'partner_id': partner.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False,
+ 'currency_id': company_currency != current_currency and current_currency.id or False,
+ 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0,
+ }
+ 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,
+ 'journal_id': category_id.journal_id.id,
+ 'partner_id': partner.id,
+ 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False,
+ 'currency_id': company_currency != current_currency and current_currency.id or False,
+ 'amount_currency': company_currency != current_currency and line.amount or 0.0,
+ }
+ 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)],
+ }
+ 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'))).post()
+ return [x.id for x in created_moves]
+
+ def create_grouped_move(self, post_move=True):
+ if not self.exists():
+ return []
+
+ created_moves = self.env['account.move']
+ category_id = self[
+ 0].asset_id.category_id # we can suppose that all lines have the same category
+ 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
+ amount += current_currency.compute(line.amount, company_currency)
+
+ 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': category_id.account_analytic_id.id 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': category_id.account_analytic_id.id 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)],
+ }
+ move = self.env['account.move'].create(move_vals)
+ self.write({'move_id': move.id, 'move_check': True})
+ created_moves |= move
+
+ if post_move and created_moves:
+ self.post_lines_and_close_asset()
+ created_moves.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
+ # `message_post` invalidates the (whole) cache
+ # preprocess the assets and lines in which a message should be posted,
+ # and then post in batch will prevent the re-fetch of the same data over and over.
+ assets_to_close = self.env['account.asset.asset']
+ for line in self:
+ asset = line.asset_id
+ if asset.currency_id.is_zero(asset.value_residual):
+ assets_to_close |= asset
+ self.log_message_when_posted()
+ assets_to_close.write({'state': 'close'})
+ for asset in assets_to_close:
+ asset.message_post(body=_("Document closed."))
+
+ def log_message_when_posted(self):
+ def _format_message(message_description, tracked_values):
+ message = ''
+ if message_description:
+ message = '<span>%s</span>' % message_description
+ for name, values in tracked_values.items():
+ message += '<div> &nbsp; &nbsp; &bull; <b>%s</b>: ' % name
+ message += '%s</div>' % values
+ return message
+
+ # `message_post` invalidates the (whole) cache
+ # preprocess the assets in which messages should be posted,
+ # and then post in batch will prevent the re-fetch of the same data over and over.
+ assets_to_post = {}
+ for line in self:
+ if line.move_id and line.move_id.state == 'draft':
+ partner_name = line.asset_id.partner_id.name
+ currency_name = line.asset_id.currency_id.name
+ msg_values = {_('Currency'): currency_name,
+ _('Amount'): line.amount}
+ if partner_name:
+ msg_values[_('Partner')] = partner_name
+ msg = _format_message(_('Depreciation line posted.'),
+ msg_values)
+ assets_to_post.setdefault(line.asset_id, []).append(msg)
+ for asset, messages in assets_to_post.items():
+ for msg in messages:
+ asset.message_post(body=msg)
+
+ def unlink(self):
+ for record in self:
+ if record.move_check:
+ if record.asset_id.category_id.type == 'purchase':
+ msg = _("You cannot delete posted depreciation lines.")
+ else:
+ msg = _("You cannot delete posted installment lines.")
+ raise UserError(msg)
+ return super(AccountAssetDepreciationLine, self).unlink()