# Copyright 2009-2018 Noviat # Copyright 2021 Tecnativa - João Marques # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models from odoo.exceptions import UserError class AccountAssetLine(models.Model): _name = "account.asset.line" _description = "Asset depreciation table line" _order = "type, line_date" _check_company_auto = True name = fields.Char(string="Depreciation Name", size=64, readonly=True) asset_id = fields.Many2one( comodel_name="account.asset", string="Asset", required=True, ondelete="cascade", check_company=True, index=True, ) 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", ) depreciation_base = fields.Monetary( related="asset_id.depreciation_base", string="Depreciation Base", ) amount = fields.Monetary(required=True) remaining_value = fields.Monetary( compute="_compute_values", string="Next Period Depreciation", store=True, ) depreciated_value = fields.Monetary( compute="_compute_values", 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, check_company=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.", ) company_id = fields.Many2one(related="asset_id.company_id", store=True) currency_id = fields.Many2one( related="asset_id.company_id.currency_id", store=True, string="Company Currency" ) @api.depends("amount", "previous_id", "type") def _compute_values(self): self.depreciated_value = 0.0 self.remaining_value = 0.0 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) # Give value 0 to the lines that are not going to be calculated # to avoid cache miss error all_excluded_lines = self - dlines all_excluded_lines.depreciated_value = 0 all_excluded_lines.remaining_value = 0 # 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") 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 ) 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") and dl.type != "create" ): 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 al: al != dl and (al.init_entry or al.move_check) and al.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) 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 = { "date": depreciation_date, "ref": "{} - {}".format(asset.name, 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): """Prepare data to be propagated to account.move.line""" asset = self.asset_id currency = asset.company_id.currency_id amount = self.amount amount_comp = currency.compare_amounts(amount, 0) analytic_distribution = False if ml_type == "depreciation": debit = amount_comp < 0 and -amount or 0.0 credit = amount_comp > 0 and amount or 0.0 elif ml_type == "expense": debit = amount_comp > 0 and amount or 0.0 credit = amount_comp < 0 and -amount or 0.0 analytic_distribution = asset.analytic_distribution 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_distribution": analytic_distribution, "date": depreciation_date, "asset_id": asset.id, } return move_line_data 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.action_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.currency_id.is_zero(asset.value_residual): asset.state = "close" return created_move_ids def open_move(self): self.ensure_one() return { "name": _("Journal Entry"), "view_mode": "form", "res_id": self.move_id.id, "res_model": "account.move", "view_id": False, "type": "ir.actions.act_window", "context": self.env.context, } def update_asset_line_after_unlink_move(self): self.write({"move_id": False}) if self.parent_state == "close": self.asset_id.write({"state": "open"}) elif self.parent_state == "removed" and self.type == "remove": self.asset_id.write({"state": "close", "date_remove": False}) self.unlink() def unlink_move(self): for line in self: if line.asset_id.profile_id.allow_reversal: context = dict(self._context or {}) context.update( { "active_model": self._name, "active_ids": line.ids, "active_id": line.id, } ) return { "name": _("Reverse Move"), "view_mode": "form", "res_model": "wiz.asset.move.reverse", "target": "new", "type": "ir.actions.act_window", "context": context, } else: move = line.move_id move.button_draft() move.with_context(force_delete=True, unlink_from_asset=True).unlink() line.with_context( unlink_from_asset=True ).update_asset_line_after_unlink_move() return True