# Copyright 2009-2018 Noviat # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api, fields, models, _ import odoo.addons.decimal_precision as dp from odoo.exceptions import UserError class AccountAssetLine(models.Model): _name = 'account.asset.line' _description = 'Asset depreciation table line' _order = 'type, line_date' name = fields.Char(string='Depreciation Name', size=64, readonly=True) asset_id = fields.Many2one( comodel_name='account.asset', string='Asset', required=True, ondelete='cascade') previous_id = fields.Many2one( comodel_name='account.asset.line', string='Previous Depreciation Line', readonly=True) parent_state = fields.Selection( related='asset_id.state', string='State of Asset', readonly=True, ) depreciation_base = fields.Float( related='asset_id.depreciation_base', string='Depreciation Base', readonly=True, ) amount = fields.Float( string='Amount', digits=dp.get_precision('Account'), required=True) remaining_value = fields.Float( compute='_compute_values', digits=dp.get_precision('Account'), string='Next Period Depreciation', store=True) depreciated_value = fields.Float( compute='_compute_values', digits=dp.get_precision('Account'), string='Amount Already Depreciated', store=True) line_date = fields.Date(string='Date', required=True) line_days = fields.Integer( string='Days', readonly=True, ) move_id = fields.Many2one( comodel_name='account.move', string='Depreciation Entry', readonly=True) move_check = fields.Boolean( compute='_compute_move_check', string='Posted', store=True) type = fields.Selection( selection=[ ('create', 'Depreciation Base'), ('depreciate', 'Depreciation'), ('remove', 'Asset Removal')], readonly=True, default='depreciate') init_entry = fields.Boolean( string='Initial Balance Entry', help="Set this flag for entries of previous fiscal years " "for which Odoo has not generated accounting entries.") @api.depends('amount', 'previous_id', 'type') @api.multi def _compute_values(self): dlines = self if self.env.context.get('no_compute_asset_line_ids'): # skip compute for lines in unlink exclude_ids = self.env.context['no_compute_asset_line_ids'] dlines = self.filtered(lambda l: l.id not in exclude_ids) dlines = dlines.filtered(lambda l: l.type == 'depreciate') dlines = dlines.sorted(key=lambda l: l.line_date) # Group depreciation lines per asset asset_ids = dlines.mapped('asset_id') grouped_dlines = [] for asset in asset_ids: grouped_dlines.append( dlines.filtered(lambda l: l.asset_id.id == asset.id)) for dlines in grouped_dlines: for i, dl in enumerate(dlines): if i == 0: depreciation_base = dl.depreciation_base tmp = depreciation_base - dl.previous_id.remaining_value depreciated_value = dl.previous_id and tmp or 0.0 remaining_value = \ depreciation_base - depreciated_value - dl.amount else: depreciated_value += dl.previous_id.amount remaining_value -= dl.amount dl.depreciated_value = depreciated_value dl.remaining_value = remaining_value @api.depends('move_id') @api.multi def _compute_move_check(self): for line in self: line.move_check = bool(line.move_id) @api.onchange('amount') def _onchange_amount(self): if self.type == 'depreciate': self.remaining_value = self.depreciation_base - \ self.depreciated_value - self.amount @api.multi def write(self, vals): for dl in self: line_date = vals.get('line_date') or dl.line_date asset_lines = dl.asset_id.depreciation_line_ids if list(vals.keys()) == ['move_id'] and not vals['move_id']: # allow to remove an accounting entry via the # 'Delete Move' button on the depreciation lines. if not self.env.context.get('unlink_from_asset'): raise UserError(_( "You are not allowed to remove an accounting entry " "linked to an asset." "\nYou should remove such entries from the asset.")) elif list(vals.keys()) == ['asset_id']: continue elif dl.move_id and not self.env.context.get( 'allow_asset_line_update'): raise UserError(_( "You cannot change a depreciation line " "with an associated accounting entry.")) elif vals.get('init_entry'): check = asset_lines.filtered( lambda l: l.move_check and l.type == 'depreciate' and l.line_date <= line_date) if check: raise UserError(_( "You cannot set the 'Initial Balance Entry' flag " "on a depreciation line " "with prior posted entries.")) elif vals.get('line_date'): if dl.type == 'create': check = asset_lines.filtered( lambda l: l.type != 'create' and (l.init_entry or l.move_check) and l.line_date < fields.Date.to_date(vals['line_date'])) if check: raise UserError( _("You cannot set the Asset Start Date " "after already posted entries.")) else: check = asset_lines.filtered( lambda l: l != dl and (l.init_entry or l.move_check) and l.line_date > fields.Date.to_date(vals['line_date'])) if check: raise UserError(_( "You cannot set the date on a depreciation line " "prior to already posted entries.")) return super().write(vals) @api.multi def unlink(self): for dl in self: if dl.type == 'create' and dl.amount: raise UserError(_( "You cannot remove an asset line " "of type 'Depreciation Base'.")) elif dl.move_id: raise UserError(_( "You cannot delete a depreciation line with " "an associated accounting entry.")) previous = dl.previous_id next_line = dl.asset_id.depreciation_line_ids.filtered( lambda l: l.previous_id == dl and l not in self) if next_line: next_line.previous_id = previous return super(AccountAssetLine, self.with_context( no_compute_asset_line_ids=self.ids)).unlink() def _setup_move_data(self, depreciation_date): asset = self.asset_id move_data = { 'name': asset.name, 'date': depreciation_date, 'ref': self.name, 'journal_id': asset.profile_id.journal_id.id, } return move_data def _setup_move_line_data(self, depreciation_date, account, ml_type, move): asset = self.asset_id amount = self.amount analytic_id = False if ml_type == 'depreciation': debit = amount < 0 and -amount or 0.0 credit = amount > 0 and amount or 0.0 elif ml_type == 'expense': debit = amount > 0 and amount or 0.0 credit = amount < 0 and -amount or 0.0 analytic_id = asset.account_analytic_id.id move_line_data = { 'name': asset.name, 'ref': self.name, 'move_id': move.id, 'account_id': account.id, 'credit': credit, 'debit': debit, 'journal_id': asset.profile_id.journal_id.id, 'partner_id': asset.partner_id.id, 'analytic_account_id': analytic_id, 'date': depreciation_date, 'asset_id': asset.id, } return move_line_data @api.multi def create_move(self): created_move_ids = [] asset_ids = set() ctx = dict(self.env.context, allow_asset=True, check_move_validity=False) for line in self: asset = line.asset_id depreciation_date = line.line_date am_vals = line._setup_move_data(depreciation_date) move = self.env['account.move'].with_context(ctx).create(am_vals) depr_acc = asset.profile_id.account_depreciation_id exp_acc = asset.profile_id.account_expense_depreciation_id aml_d_vals = line._setup_move_line_data( depreciation_date, depr_acc, 'depreciation', move) self.env['account.move.line'].with_context(ctx).create(aml_d_vals) aml_e_vals = line._setup_move_line_data( depreciation_date, exp_acc, 'expense', move) self.env['account.move.line'].with_context(ctx).create(aml_e_vals) move.post() line.with_context(allow_asset_line_update=True).write({ 'move_id': move.id }) created_move_ids.append(move.id) asset_ids.add(asset.id) # we re-evaluate the assets to determine if we can close them for asset in self.env['account.asset'].browse(list(asset_ids)): if asset.company_currency_id.is_zero(asset.value_residual): asset.state = 'close' return created_move_ids @api.multi def open_move(self): self.ensure_one() return { 'name': _("Journal Entry"), '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', '=', self.move_id.id)], } @api.multi def unlink_move(self): for line in self: move = line.move_id if move.state == 'posted': move.button_cancel() move.with_context(unlink_from_asset=True).unlink() # trigger store function line.with_context(unlink_from_asset=True).write( {'move_id': False}) if line.parent_state == 'close': line.asset_id.write({'state': 'open'}) elif line.parent_state == 'removed' and line.type == 'remove': line.asset_id.write({ 'state': 'close', 'date_remove': False, }) line.unlink() return True