mirror of
https://github.com/OCA/account-financial-tools.git
synced 2025-02-02 12:47:26 +02:00
Steps to reproduce the problem: * Go to assets view * Group by profile * Unfold a group and click on an asset * Click on "Journal Entries" smart-button * Go back to the asset list * Click again on the same asset (or another). * Click on "Journal Entries" smart-button Current behavior: Error saying "KeyError: 'profile_id'" Expected behavior: No error The cause for this is that Odoo stores in the context the key `group_by` with the value `profile_id` in the specified chain of steps. That context entry is used for grouping records in the list, and system tries to group the journal entries also by that field, which doesn't exists in the other model, and thus the error. We avoided it copying the context to be passes and leaving out that entry.
1164 lines
44 KiB
Python
1164 lines
44 KiB
Python
# Copyright 2009-2018 Noviat
|
|
# Copyright 2019 Tecnativa - Pedro M. Baeza
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
import calendar
|
|
import logging
|
|
from datetime import date
|
|
from functools import reduce
|
|
from sys import exc_info
|
|
from traceback import format_exception
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.osv import expression
|
|
|
|
_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",
|
|
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="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="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="Account",
|
|
string="Residual Value",
|
|
store=True,
|
|
)
|
|
value_depreciated = fields.Float(
|
|
compute="_compute_depreciation",
|
|
digits="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.company
|
|
|
|
def _compute_move_line_check(self):
|
|
for asset in self:
|
|
move_line_check = False
|
|
for line in asset.depreciation_line_ids:
|
|
if line.move_id:
|
|
move_line_check = True
|
|
break
|
|
asset.move_line_check = move_line_check
|
|
|
|
@api.depends("purchase_value", "salvage_value", "method")
|
|
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.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.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.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
|
|
|
|
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"]
|
|
|
|
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.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
|
|
|
|
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
|
|
|
|
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_mode": "form",
|
|
"res_model": "account.asset.remove",
|
|
"target": "new",
|
|
"type": "ir.actions.act_window",
|
|
"context": ctx,
|
|
}
|
|
|
|
def set_to_draft(self):
|
|
return self.write({"state": "draft"})
|
|
|
|
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]
|
|
# needed for avoiding errors after grouping in assets
|
|
context = dict(self.env.context)
|
|
context.pop("group_by", None)
|
|
return {
|
|
"name": _("Journal Entries"),
|
|
"view_mode": "tree,form",
|
|
"res_model": "account.move",
|
|
"view_id": False,
|
|
"type": "ir.actions.act_window",
|
|
"context": context,
|
|
"domain": [("id", "in", am_ids)],
|
|
}
|
|
|
|
def _group_lines(self, table):
|
|
"""group lines prior to depreciation start period."""
|
|
|
|
def group_lines(x, y):
|
|
y.update({"amount": x["amount"] + y["amount"]})
|
|
return y
|
|
|
|
depreciation_start_date = self.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
|
|
|
|
def _compute_depreciation_line(
|
|
self,
|
|
depreciated_value_posted,
|
|
table_i_start,
|
|
line_i_start,
|
|
table,
|
|
last_line,
|
|
posted_lines,
|
|
):
|
|
digits = self.env["decimal.precision"].precision_get("Account")
|
|
|
|
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 = self._get_depreciation_entry_name(seq)
|
|
amount = line["amount"]
|
|
if line["date"] == last_date:
|
|
# ensure that the last entry of the table always
|
|
# depreciates the remaining value
|
|
amount = self.depreciation_base - depreciated_value
|
|
if self.method in ["linear-limit", "degr-limit"]:
|
|
amount -= self.salvage_value
|
|
if amount:
|
|
vals = {
|
|
"previous_id": depr_line.id,
|
|
"amount": round(amount, digits),
|
|
"asset_id": self.id,
|
|
"name": name,
|
|
"line_date": line["date"],
|
|
"line_days": line["days"],
|
|
"init_entry": entry["init"],
|
|
}
|
|
depreciated_value += round(amount, digits)
|
|
depr_line = self.env["account.asset.line"].create(vals)
|
|
else:
|
|
seq -= 1
|
|
line_i_start = 0
|
|
|
|
def compute_depreciation_board(self):
|
|
|
|
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
|
|
|
|
asset._group_lines(table)
|
|
|
|
# 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
|
|
|
|
asset._compute_depreciation_line(
|
|
depreciated_value_posted,
|
|
table_i_start,
|
|
line_i_start,
|
|
table,
|
|
last_line,
|
|
posted_lines,
|
|
)
|
|
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 = (date(year, 12, 31) - fy_date_start).days + 1
|
|
factor = float(duration) / cy_days
|
|
elif i == cnt - 1: # last year
|
|
duration = (fy_date_stop - date(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 entry in 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)
|
|
|
|
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 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 = "[{}] {}".format(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)
|