Files
account-financial-tools/account_asset_management/models/account_asset.py
Pedro M. Baeza b9d13cdc51 [FIX] account_asset_management: Avoid error
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.
2021-01-13 07:53:35 +00:00

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)