Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
7
account_asset_management/models/__init__.py
Executable file
7
account_asset_management/models/__init__.py
Executable file
@@ -0,0 +1,7 @@
|
||||
from . import account_account
|
||||
from . import account_asset
|
||||
from . import account_asset_group
|
||||
from . import account_asset_profile
|
||||
from . import account_asset_line
|
||||
from . import account_asset_recompute_trigger
|
||||
from . import account_move
|
||||
30
account_asset_management/models/account_account.py
Executable file
30
account_asset_management/models/account_account.py
Executable file
@@ -0,0 +1,30 @@
|
||||
# Copyright 2009-2017 Noviat
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountAccount(models.Model):
|
||||
_inherit = "account.account"
|
||||
|
||||
asset_profile_id = fields.Many2one(
|
||||
comodel_name="account.asset.profile",
|
||||
string="Asset Profile",
|
||||
check_company=True,
|
||||
help="Default Asset Profile when creating invoice lines with this account.",
|
||||
)
|
||||
|
||||
@api.constrains("asset_profile_id")
|
||||
def _check_asset_profile(self):
|
||||
for account in self:
|
||||
if (
|
||||
account.asset_profile_id
|
||||
and account.asset_profile_id.account_asset_id != account
|
||||
):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
"The Asset Account defined in the Asset Profile "
|
||||
"must be equal to the account."
|
||||
)
|
||||
)
|
||||
1318
account_asset_management/models/account_asset.py
Executable file
1318
account_asset_management/models/account_asset.py
Executable file
File diff suppressed because it is too large
Load Diff
63
account_asset_management/models/account_asset_group.py
Executable file
63
account_asset_management/models/account_asset_group.py
Executable file
@@ -0,0 +1,63 @@
|
||||
# Copyright 2009-2020 Noviat
|
||||
# Copyright 2019 Tecnativa - Pedro M. Baeza
|
||||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountAssetGroup(models.Model):
|
||||
_name = "account.asset.group"
|
||||
_description = "Asset Group"
|
||||
_order = "code, name"
|
||||
_parent_store = True
|
||||
_check_company_auto = True
|
||||
_check_company_domain = models.check_company_domain_parent_of
|
||||
_rec_names_search = ["code", "name"]
|
||||
|
||||
name = fields.Char(size=64, required=True, index=True)
|
||||
code = fields.Char(index=True)
|
||||
parent_path = fields.Char(index=True)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
required=True,
|
||||
default=lambda self: self._default_company_id(),
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
comodel_name="account.asset.group",
|
||||
string="Parent Asset Group",
|
||||
ondelete="restrict",
|
||||
check_company=True,
|
||||
)
|
||||
child_ids = fields.One2many(
|
||||
comodel_name="account.asset.group",
|
||||
inverse_name="parent_id",
|
||||
string="Child Asset Groups",
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_company_id(self):
|
||||
return self.env.company
|
||||
|
||||
@api.depends("code", "name")
|
||||
def _compute_display_name(self):
|
||||
params = self.env.context.get("params")
|
||||
list_view = params and params.get("view_type") == "list"
|
||||
short_name_len = 16
|
||||
for rec in self:
|
||||
if rec.code:
|
||||
full_name = rec.code + " " + rec.name
|
||||
short_name = rec.code
|
||||
else:
|
||||
full_name = rec.name
|
||||
if len(full_name) > short_name_len:
|
||||
short_name = full_name[:16] + "..."
|
||||
else:
|
||||
short_name = full_name
|
||||
if list_view:
|
||||
name = short_name
|
||||
else:
|
||||
name = full_name
|
||||
rec.display_name = name
|
||||
336
account_asset_management/models/account_asset_line.py
Executable file
336
account_asset_management/models/account_asset_line.py
Executable file
@@ -0,0 +1,336 @@
|
||||
# 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
|
||||
_check_company_domain = models.check_company_domain_parent_of
|
||||
|
||||
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 line: line.id not in exclude_ids)
|
||||
dlines = dlines.filtered(lambda line: line.type == "depreciate")
|
||||
dlines = dlines.sorted(key=lambda line: line.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 line, asset=asset: line.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(
|
||||
self.env._(
|
||||
"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(
|
||||
self.env._(
|
||||
"You cannot change a depreciation line "
|
||||
"with an associated accounting entry."
|
||||
)
|
||||
)
|
||||
elif vals.get("init_entry"):
|
||||
check = asset_lines.filtered(
|
||||
lambda line, line_date=line_date: line.move_check
|
||||
and line.type == "depreciate"
|
||||
and line.line_date <= line_date
|
||||
)
|
||||
if check:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"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 line: line.type != "create"
|
||||
and (line.init_entry or line.move_check)
|
||||
and line.line_date < fields.Date.to_date(vals["line_date"])
|
||||
)
|
||||
if check:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You cannot set the Asset Start Date "
|
||||
"after already posted entries."
|
||||
)
|
||||
)
|
||||
else:
|
||||
check = asset_lines.filtered(
|
||||
lambda al, dl=dl: 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(
|
||||
self.env._(
|
||||
"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(
|
||||
self.env._(
|
||||
"You cannot remove an asset line of type 'Depreciation Base'."
|
||||
)
|
||||
)
|
||||
elif dl.move_id:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"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 line, dl=dl: line.previous_id == dl and line 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": f"{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": self.env._("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": self.env._("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
|
||||
248
account_asset_management/models/account_asset_profile.py
Executable file
248
account_asset_management/models/account_asset_profile.py
Executable file
@@ -0,0 +1,248 @@
|
||||
# Copyright 2009-2018 Noviat
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountAssetProfile(models.Model):
|
||||
_name = "account.asset.profile"
|
||||
_inherit = "analytic.mixin"
|
||||
_check_company_auto = True
|
||||
_check_company_domain = models.check_company_domain_parent_of
|
||||
_description = "Asset profile"
|
||||
_order = "name"
|
||||
|
||||
name = fields.Char(size=64, required=True, index=True)
|
||||
note = fields.Text()
|
||||
account_asset_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
string="Asset Account",
|
||||
check_company=True,
|
||||
required=True,
|
||||
)
|
||||
account_depreciation_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
string="Depreciation Account",
|
||||
check_company=True,
|
||||
required=True,
|
||||
)
|
||||
account_expense_depreciation_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
string="Depr. Expense Account",
|
||||
check_company=True,
|
||||
required=True,
|
||||
)
|
||||
account_plus_value_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
check_company=True,
|
||||
string="Plus-Value Account",
|
||||
)
|
||||
account_min_value_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
check_company=True,
|
||||
string="Min-Value Account",
|
||||
)
|
||||
account_residual_value_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
domain=[("deprecated", "=", False)],
|
||||
check_company=True,
|
||||
string="Residual Value Account",
|
||||
)
|
||||
journal_id = fields.Many2one(
|
||||
comodel_name="account.journal",
|
||||
domain="[('type', '=', 'general'), ('company_id', '=', company_id)]",
|
||||
string="Journal",
|
||||
check_company=True,
|
||||
required=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
required=True,
|
||||
default=lambda self: self._default_company_id(),
|
||||
)
|
||||
group_ids = fields.Many2many(
|
||||
comodel_name="account.asset.group",
|
||||
relation="account_asset_profile_group_rel",
|
||||
column1="profile_id",
|
||||
column2="group_id",
|
||||
check_company=True,
|
||||
string="Asset Groups",
|
||||
)
|
||||
salvage_value = fields.Float(
|
||||
digits="Account",
|
||||
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.",
|
||||
)
|
||||
salvage_type = fields.Selection(
|
||||
selection=[("fixed", "Fixed"), ("percent", "Percentage of Price")]
|
||||
)
|
||||
method = fields.Selection(
|
||||
selection=lambda self: self._selection_method(),
|
||||
string="Computation Method",
|
||||
required=True,
|
||||
help="Choose the method to use to compute the depreciation lines.\n"
|
||||
" * Linear: Calculated on basis of: "
|
||||
"Depreciation Base / Number of Depreciations. "
|
||||
"Depreciation Base = Purchase Value - Salvage Value.\n"
|
||||
" * Linear-Limit: Linear up to Salvage Value. "
|
||||
"Depreciation Base = Purchase Value.\n"
|
||||
" * Degressive: Calculated on basis of: "
|
||||
"Residual Value * Degressive Factor.\n"
|
||||
" * Degressive-Linear (only for Time Method = Year): "
|
||||
"Degressive becomes linear when the annual linear "
|
||||
"depreciation exceeds the annual degressive depreciation.\n"
|
||||
" * Degressive-Limit: Degressive up to Salvage Value. "
|
||||
"The Depreciation Base is equal to the asset value.",
|
||||
default="linear",
|
||||
)
|
||||
method_number = fields.Integer(
|
||||
string="Number of Years",
|
||||
help="The number of years needed to depreciate your asset",
|
||||
default=5,
|
||||
)
|
||||
method_period = fields.Selection(
|
||||
selection=lambda self: self._selection_method_period(),
|
||||
string="Period Length",
|
||||
required=True,
|
||||
default="year",
|
||||
help="Period length for the depreciation accounting entries",
|
||||
)
|
||||
method_progress_factor = fields.Float(string="Degressive Factor", default=0.3)
|
||||
method_time = fields.Selection(
|
||||
selection=lambda self: self._selection_method_time(),
|
||||
string="Time Method",
|
||||
required=True,
|
||||
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",
|
||||
)
|
||||
days_calc = fields.Boolean(
|
||||
string="Calculate by days",
|
||||
default=False,
|
||||
help="Use number of days to calculate depreciation amount",
|
||||
)
|
||||
use_leap_years = fields.Boolean(
|
||||
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",
|
||||
compute="_compute_prorrata",
|
||||
readonly=False,
|
||||
store=True,
|
||||
help="Indicates that the first depreciation entry for this asset "
|
||||
"has to be done from the depreciation start date instead of "
|
||||
"the first day of the fiscal year.",
|
||||
)
|
||||
open_asset = fields.Boolean(
|
||||
string="Skip Draft State",
|
||||
help="Check this if you want to automatically confirm the assets "
|
||||
"of this profile when created by invoices.",
|
||||
)
|
||||
asset_product_item = fields.Boolean(
|
||||
string="Create an asset by product item",
|
||||
help="By default during the validation of an invoice, an asset "
|
||||
"is created by invoice line as long as an accounting entry is "
|
||||
"created by invoice line. "
|
||||
"With this setting, an accounting entry will be created by "
|
||||
"product item. So, there will be an asset by product item.",
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
allow_reversal = fields.Boolean(
|
||||
"Allow Reversal of journal entries",
|
||||
help="If set, when pressing the Delete/Reverse Move button in a "
|
||||
"posted depreciation line will prompt the option to reverse the "
|
||||
"journal entry, instead of deleting them.",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _default_company_id(self):
|
||||
return self.env.company
|
||||
|
||||
@api.model
|
||||
def _selection_method(self):
|
||||
return [
|
||||
("linear", self.env._("Linear")),
|
||||
("linear-limit", self.env._("Linear up to Salvage Value")),
|
||||
("degressive", self.env._("Degressive")),
|
||||
("degr-linear", self.env._("Degressive-Linear")),
|
||||
("degr-limit", self.env._("Degressive up to Salvage Value")),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _selection_method_period(self):
|
||||
return [
|
||||
("month", self.env._("Month")),
|
||||
("quarter", self.env._("Quarter")),
|
||||
("year", self.env._("Year")),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _selection_method_time(self):
|
||||
return [
|
||||
("year", self.env._("Number of Years or end date")),
|
||||
("number", self.env._("Number of Depreciations")),
|
||||
]
|
||||
|
||||
@api.constrains("method", "method_time")
|
||||
def _check_method(self):
|
||||
if any(a.method == "degr-linear" and a.method_time != "year" for a in self):
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"Degressive-Linear is only supported for Time Method = Year."
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends("method_time")
|
||||
def _compute_prorrata(self):
|
||||
for profile in self:
|
||||
if profile.method_time != "year":
|
||||
profile.prorata = True
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("method_time") != "year" and not vals.get("prorata"):
|
||||
vals["prorata"] = True
|
||||
profile_ids = super().create(vals_list)
|
||||
account_dict = {}
|
||||
for profile_id in profile_ids.filtered(
|
||||
lambda x: not x.account_asset_id.asset_profile_id
|
||||
):
|
||||
account_dict.setdefault(profile_id.account_asset_id, []).append(
|
||||
profile_id.id
|
||||
)
|
||||
for account, profile_list in account_dict.items():
|
||||
account.write({"asset_profile_id": profile_list[-1]})
|
||||
return profile_ids
|
||||
|
||||
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)
|
||||
# TODO last profile in self is defined as default on the related
|
||||
# account. must be improved.
|
||||
account = self.env["account.account"].browse(vals.get("account_asset_id"))
|
||||
if self and account and not account.asset_profile_id:
|
||||
account.write({"asset_profile_id": self[-1].id})
|
||||
return res
|
||||
23
account_asset_management/models/account_asset_recompute_trigger.py
Executable file
23
account_asset_management/models/account_asset_recompute_trigger.py
Executable file
@@ -0,0 +1,23 @@
|
||||
# Copyright 2009-2018 Noviat
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAssetRecomputeTrigger(models.Model):
|
||||
_name = "account.asset.recompute.trigger"
|
||||
_description = "Asset table recompute triggers"
|
||||
|
||||
reason = fields.Char(required=True)
|
||||
company_id = fields.Many2one("res.company", string="Company", required=True)
|
||||
date_trigger = fields.Datetime(
|
||||
"Trigger Date",
|
||||
readonly=True,
|
||||
help="Date of the event triggering the need to recompute the Asset Tables.",
|
||||
)
|
||||
date_completed = fields.Datetime("Completion Date", readonly=True)
|
||||
state = fields.Selection(
|
||||
selection=[("open", "Open"), ("done", "Done")],
|
||||
default="open",
|
||||
readonly=True,
|
||||
)
|
||||
262
account_asset_management/models/account_move.py
Executable file
262
account_asset_management/models/account_move.py
Executable file
@@ -0,0 +1,262 @@
|
||||
# Copyright 2009-2018 Noviat
|
||||
# Copyright 2021 Tecnativa - João Marques
|
||||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
# List of move's fields that can't be modified if move is linked
|
||||
# with a depreciation line
|
||||
FIELDS_AFFECTS_ASSET_MOVE = {"journal_id", "date"}
|
||||
# List of move line's fields that can't be modified if move is linked
|
||||
# with a depreciation line
|
||||
FIELDS_AFFECTS_ASSET_MOVE_LINE = {
|
||||
"credit",
|
||||
"debit",
|
||||
"account_id",
|
||||
"journal_id",
|
||||
"date",
|
||||
"asset_profile_id",
|
||||
"asset_id",
|
||||
}
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
asset_count = fields.Integer(compute="_compute_asset_count")
|
||||
|
||||
def _compute_asset_count(self):
|
||||
rg_res = self.env["account.asset.line"].read_group(
|
||||
[("move_id", "in", self.ids)], ["move_id"], ["move_id"]
|
||||
)
|
||||
mapped_data = {x["move_id"][0]: x["move_id_count"] for x in rg_res}
|
||||
for move in self:
|
||||
move.asset_count = mapped_data.get(move.id, 0)
|
||||
|
||||
def unlink(self):
|
||||
# for move in self:
|
||||
deprs = (
|
||||
self.env["account.asset.line"]
|
||||
.sudo()
|
||||
.search(
|
||||
[("move_id", "in", self.ids), ("type", "in", ["depreciate", "remove"])]
|
||||
)
|
||||
)
|
||||
if deprs and not self.env.context.get("unlink_from_asset"):
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You are not allowed to remove an accounting entry "
|
||||
"linked to an asset."
|
||||
"\nYou should remove such entries from the asset."
|
||||
)
|
||||
)
|
||||
# trigger store function
|
||||
deprs.write({"move_id": False})
|
||||
return super().unlink()
|
||||
|
||||
def write(self, vals):
|
||||
if set(vals).intersection(FIELDS_AFFECTS_ASSET_MOVE):
|
||||
deprs = (
|
||||
self.env["account.asset.line"]
|
||||
.sudo()
|
||||
.search([("move_id", "in", self.ids), ("type", "=", "depreciate")])
|
||||
)
|
||||
if deprs:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You cannot change an accounting entry "
|
||||
"linked to an asset depreciation line."
|
||||
)
|
||||
)
|
||||
return super().write(vals)
|
||||
|
||||
def _prepare_asset_vals(self, aml):
|
||||
depreciation_base = aml.balance
|
||||
return {
|
||||
"name": aml.name,
|
||||
"code": self.name,
|
||||
"profile_id": aml.asset_profile_id.id,
|
||||
"purchase_value": depreciation_base,
|
||||
"partner_id": aml.partner_id.id,
|
||||
"date_start": self.date,
|
||||
}
|
||||
|
||||
def action_post(self):
|
||||
ret_val = super().action_post()
|
||||
for move in self:
|
||||
for aml in move.line_ids.filtered(
|
||||
lambda line: line.asset_profile_id and not line.tax_line_id
|
||||
):
|
||||
if not aml.name:
|
||||
raise UserError(
|
||||
self.env._("Asset name must be set in the label of the line.")
|
||||
)
|
||||
if aml.asset_id:
|
||||
continue
|
||||
vals = move._prepare_asset_vals(aml)
|
||||
asset = (
|
||||
self.env["account.asset"]
|
||||
.with_company(move.company_id)
|
||||
.with_context(create_asset_from_move_line=True, move_id=move.id)
|
||||
.create(vals)
|
||||
)
|
||||
asset.analytic_distribution = aml.analytic_distribution
|
||||
aml.with_context(
|
||||
allow_asset=True, allow_asset_removal=True
|
||||
).asset_id = asset.id
|
||||
new_name_get = []
|
||||
for asset in move.line_ids.filtered("asset_profile_id").asset_id:
|
||||
new_name_get = [asset.id, asset.display_name]
|
||||
if new_name_get:
|
||||
message = self.env._(
|
||||
"This invoice created the asset(s): %s",
|
||||
Markup(
|
||||
"""<a href=# data-oe-model=account.asset"""
|
||||
f""" data-oe-id={new_name_get[0]}"""
|
||||
f""">{new_name_get[1]}</a>"""
|
||||
),
|
||||
)
|
||||
move.message_post(body=message)
|
||||
return ret_val
|
||||
|
||||
def button_draft(self):
|
||||
invoices = self.filtered(lambda r: r.is_purchase_document())
|
||||
if invoices:
|
||||
invoices.line_ids.asset_id.unlink()
|
||||
return super().button_draft()
|
||||
|
||||
def _reverse_move_vals(self, default_values, cancel=True):
|
||||
move_vals = super()._reverse_move_vals(default_values, cancel)
|
||||
if move_vals["move_type"] not in ("out_invoice", "out_refund"):
|
||||
for line_command in move_vals.get("line_ids", []):
|
||||
line_vals = line_command[2] # (0, 0, {...})
|
||||
asset = self.env["account.asset"].browse(line_vals["asset_id"])
|
||||
# We remove the asset if we recognize that we are reversing
|
||||
# the asset creation
|
||||
if asset:
|
||||
asset_line = self.env["account.asset.line"].search(
|
||||
[("asset_id", "=", asset.id), ("type", "=", "create")], limit=1
|
||||
)
|
||||
if asset_line and asset_line.move_id == self:
|
||||
asset.unlink()
|
||||
line_vals.update(asset_profile_id=False, asset_id=False)
|
||||
return move_vals
|
||||
|
||||
def action_view_assets(self):
|
||||
assets = (
|
||||
self.env["account.asset.line"]
|
||||
.search([("move_id", "=", self.id)])
|
||||
.mapped("asset_id")
|
||||
)
|
||||
action = self.env.ref("account_asset_management.account_asset_action")
|
||||
action_dict = action.sudo().read()[0]
|
||||
if len(assets) == 1:
|
||||
res = self.env.ref(
|
||||
"account_asset_management.account_asset_view_form", False
|
||||
)
|
||||
action_dict["views"] = [(res and res.id or False, "form")]
|
||||
action_dict["res_id"] = assets.id
|
||||
elif assets:
|
||||
action_dict["domain"] = [("id", "in", assets.ids)]
|
||||
else:
|
||||
action_dict = {"type": "ir.actions.act_window_close"}
|
||||
return action_dict
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
asset_profile_id = fields.Many2one(
|
||||
comodel_name="account.asset.profile",
|
||||
string="Asset Profile",
|
||||
compute="_compute_asset_profile",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
asset_id = fields.Many2one(
|
||||
comodel_name="account.asset",
|
||||
string="Asset",
|
||||
ondelete="restrict",
|
||||
check_company=True,
|
||||
)
|
||||
|
||||
@api.depends("account_id", "asset_id")
|
||||
def _compute_asset_profile(self):
|
||||
for rec in self:
|
||||
if rec.account_id.asset_profile_id and not rec.asset_id:
|
||||
rec.asset_profile_id = rec.account_id.asset_profile_id
|
||||
elif rec.asset_id:
|
||||
rec.asset_profile_id = rec.asset_id.profile_id
|
||||
|
||||
@api.onchange("asset_profile_id")
|
||||
def _onchange_asset_profile_id(self):
|
||||
if self.asset_profile_id.account_asset_id:
|
||||
self.account_id = self.asset_profile_id.account_asset_id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
move = self.env["account.move"].browse(vals.get("move_id"))
|
||||
if not move.is_sale_document():
|
||||
if vals.get("asset_id") and not self.env.context.get("allow_asset"):
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You are not allowed to link "
|
||||
"an accounting entry to an asset."
|
||||
"\nYou should generate such entries from the asset."
|
||||
)
|
||||
)
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record._expand_asset_line()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
if set(vals).intersection(FIELDS_AFFECTS_ASSET_MOVE_LINE) and not (
|
||||
self.env.context.get("allow_asset_removal")
|
||||
and list(vals.keys()) == ["asset_id"]
|
||||
):
|
||||
# Check if at least one asset is linked to a move
|
||||
linked_asset = False
|
||||
for move_line in self.filtered(lambda r: not r.move_id.is_sale_document()):
|
||||
linked_asset = move_line.asset_id
|
||||
if linked_asset:
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You cannot change an accounting item "
|
||||
"linked to an asset depreciation line."
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.filtered(lambda r: not r.move_id.is_sale_document())
|
||||
and vals.get("asset_id")
|
||||
and not self.env.context.get("allow_asset")
|
||||
):
|
||||
raise UserError(
|
||||
self.env._(
|
||||
"You are not allowed to link "
|
||||
"an accounting entry to an asset."
|
||||
"\nYou should generate such entries from the asset."
|
||||
)
|
||||
)
|
||||
super().write(vals)
|
||||
if "quantity" in vals or "asset_profile_id" in vals:
|
||||
for record in self:
|
||||
record._expand_asset_line()
|
||||
return True
|
||||
|
||||
def _expand_asset_line(self):
|
||||
self.ensure_one()
|
||||
if self.asset_profile_id.asset_product_item and self.quantity > 1.0:
|
||||
aml = self.with_context(check_move_validity=False)
|
||||
qty = self.quantity
|
||||
name = self.name
|
||||
aml.write({"quantity": 1, "name": f"{name} {1}"})
|
||||
for i in range(1, int(qty)):
|
||||
aml.copy({"name": f"{name} {i + 1}"})
|
||||
Reference in New Issue
Block a user