2
0

Merge PR #1479 into 16.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot 2023-01-06 12:13:25 +00:00
commit 760f1a55ad
120 changed files with 139750 additions and 0 deletions

View File

@ -0,0 +1,168 @@
=================
Assets Management
=================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
:target: https://odoo-community.org/page/development-status
:alt: Mature
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
:target: https://github.com/OCA/account-financial-tools/tree/15.0/account_asset_management
:alt: OCA/account-financial-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-financial-tools-15-0/account-financial-tools-15-0-account_asset_management
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/92/15.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This Module manages the assets owned by a company. It will keep
track of depreciation's occurred on those assets. And it allows to create
accounting entries from the depreciation lines.
The full asset life-cycle is managed (from asset creation to asset removal).
Assets can be created manually as well as automatically
(via the creation of an accounting entry on the asset account).
Depreciation Journal Entries can be created manually in the "Deprecation Board" tab,
or automatically by two ways:
* Using the "Invoicing/Assets/Compute Assets" wizard.
* Activating the "Asset Management: Generate assets" cron.
These options are compatibles each other.
The module contains a large number of functional enhancements compared to
the standard account_asset module from Odoo.
**Table of contents**
.. contents::
:local:
Usage
=====
The module in NOT compatible with the standard account_asset module.
Changelog
=========
14.0.1.0.0 (2021-01-08)
~~~~~~~~~~~~~~~~~~~~~~~
* [BREAKING] Removed all functionality associated with `account.fiscal.year`
13.0.3.0.0 (2021-07-06)
~~~~~~~~~~~~~~~~~~~~~~~
* Allow to reverse the posting of a depreciation line instead of deleting the
journal entry.
13.0.2.0.0 (2021-02-19)
~~~~~~~~~~~~~~~~~~~~~~~
* Add support for multi-company
13.0.1.0.0 (2019-10-21)
~~~~~~~~~~~~~~~~~~~~~~~
* Python code and views were adapted to be compatible with v13.
* When assets are created through accounting journal items,
they are created when the journal items is posted.
* When a Bill Invoice is created or modified, at the time it is saved,
for each line that has an Asset profile and Quantity 'N'
greater than 1, it will be replaced by 'N' lines identical to it but
with quantity 1. This was done to maintain the same behavior as in
the previous version, in which for each asset created there is a
Journal Item. In addition, this solution does not change the data
model which does not cause migration scripts.
* The configuration option was removed so the only function of that is to
allow the module to be uninstalled by unchecking that configuration option.
* Tests were adapted.
12.0.2.1.0 (2019-10-21)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP] Add option to calculate depreciation table by days
12.0.1.0.0 (2019-01-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [BREAKING] account.asset: parent_path has replaced parent_left & parent_right (TODO: migration script)
* [BREAKING] account.asset.recompute.trigger: depends on date_range.py (TODO: re-implement in account_fiscal_year.py)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_asset_management%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Noviat
Contributors
~~~~~~~~~~~~
* OpenERP SA
* Luc De Meyer (Noviat)
* Frédéric Clementi (camptocamp)
* Florian Dacosta (Akretion)
* Stéphane Bidoul (Acsone)
* Adrien Peiffer (Acsone)
* Akim Juillerat <akim.juillerat@camptocamp.com>
* Henrik Norlin (Apps2GROW)
* Maxence Groine <mgroine@fiefmanage.ch>
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>
* Saran Lim. <saranl@ecosoft.co.th>
* `Tecnativa <https://www.tecnativa.com>`_:
* Ernesto Tejeda
* Pedro M. Baeza
* João Marques
* Víctor Martínez
* `ForgeFlow <https://www.forgeflow.com>`_:
* Jordi Ballester <jordi.ballester@forgeflow.com>
* Miquel Raïch <miquel.raich@forgeflow.com>
* `Sygel <https://www.sygel.es>`_:
* Manuel Regidor <manuel.regidor@sygel.es>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/15.0/account_asset_management>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,3 @@
from . import models
from . import report
from . import wizard

View File

@ -0,0 +1,33 @@
# Copyright 2009-2019 Noviat
# Copyright 2019 Tecnativa - Pedro M. Baeza
# Copyright 2021 Tecnativa - João Marques
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Assets Management",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"depends": ["account", "report_xlsx_helper"],
"excludes": ["account_asset"],
"development_status": "Mature",
"external_dependencies": {"python": ["python-dateutil"]},
"author": "Noviat, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-financial-tools",
"category": "Accounting & Finance",
"data": [
"security/account_asset_security.xml",
"security/ir.model.access.csv",
"wizard/account_asset_compute.xml",
"wizard/account_asset_remove.xml",
"views/account_account.xml",
"views/account_asset.xml",
"views/account_asset_group.xml",
"views/account_asset_profile.xml",
"views/account_move.xml",
"views/account_move_line.xml",
"views/menuitem.xml",
"data/cron.xml",
"wizard/wiz_account_asset_report.xml",
"wizard/wiz_asset_move_reverse.xml",
],
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record forcecreate="True" id="ir_cron_assets_generator" model="ir.cron">
<field name="name">Asset Management: Generate assets</field>
<field name="model_id" ref="model_account_asset_compute" />
<field name="state">code</field>
<field name="code">model.create({}).asset_compute()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active" eval="False" />
<field name="doall" eval="False" />
</record>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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(
_(
"The Asset Account defined in the Asset Profile "
"must be equal to the account."
)
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
# 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
from odoo.osv import expression
class AccountAssetGroup(models.Model):
_name = "account.asset.group"
_description = "Asset Group"
_order = "code, name"
_parent_store = True
_check_company_auto = True
name = fields.Char(size=64, required=True, index=True)
code = fields.Char(index=True)
parent_path = fields.Char(index=True, unaccent=False)
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
def name_get(self):
result = []
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
result.append((rec.id, name))
return result
@api.model
def _name_search(
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
):
args = args or []
domain = []
if name:
domain = [
"|",
("code", "=ilike", name.split(" ")[0] + "%"),
("name", operator, name),
]
if operator in expression.NEGATIVE_TERM_OPERATORS:
domain = ["&", "!"] + domain[1:]
return self._search(
expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid
)

View File

@ -0,0 +1,331 @@
# Copyright 2009-2018 Noviat
# Copyright 2021 Tecnativa - João Marques
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class AccountAssetLine(models.Model):
_name = "account.asset.line"
_description = "Asset depreciation table line"
_order = "type, line_date"
_check_company_auto = True
name = fields.Char(string="Depreciation Name", size=64, readonly=True)
asset_id = fields.Many2one(
comodel_name="account.asset",
string="Asset",
required=True,
ondelete="cascade",
check_company=True,
index=True,
)
previous_id = fields.Many2one(
comodel_name="account.asset.line",
string="Previous Depreciation Line",
readonly=True,
)
parent_state = fields.Selection(
related="asset_id.state",
string="State of Asset",
)
depreciation_base = fields.Monetary(
related="asset_id.depreciation_base",
string="Depreciation Base",
)
amount = fields.Monetary(required=True)
remaining_value = fields.Monetary(
compute="_compute_values",
string="Next Period Depreciation",
store=True,
)
depreciated_value = fields.Monetary(
compute="_compute_values",
string="Amount Already Depreciated",
store=True,
)
line_date = fields.Date(string="Date", required=True)
line_days = fields.Integer(string="Days", readonly=True)
move_id = fields.Many2one(
comodel_name="account.move",
string="Depreciation Entry",
readonly=True,
check_company=True,
)
move_check = fields.Boolean(
compute="_compute_move_check", string="Posted", store=True
)
type = fields.Selection(
selection=[
("create", "Depreciation Base"),
("depreciate", "Depreciation"),
("remove", "Asset Removal"),
],
readonly=True,
default="depreciate",
)
init_entry = fields.Boolean(
string="Initial Balance Entry",
help="Set this flag for entries of previous fiscal years "
"for which Odoo has not generated accounting entries.",
)
company_id = fields.Many2one(related="asset_id.company_id", store=True)
currency_id = fields.Many2one(
related="asset_id.company_id.currency_id", store=True, string="Company Currency"
)
@api.depends("amount", "previous_id", "type")
def _compute_values(self):
self.depreciated_value = 0.0
self.remaining_value = 0.0
dlines = self
if self.env.context.get("no_compute_asset_line_ids"):
# skip compute for lines in unlink
exclude_ids = self.env.context["no_compute_asset_line_ids"]
dlines = self.filtered(lambda l: l.id not in exclude_ids)
dlines = dlines.filtered(lambda l: l.type == "depreciate")
dlines = dlines.sorted(key=lambda l: l.line_date)
# Give value 0 to the lines that are not going to be calculated
# to avoid cache miss error
all_excluded_lines = self - dlines
all_excluded_lines.depreciated_value = 0
all_excluded_lines.remaining_value = 0
# Group depreciation lines per asset
asset_ids = dlines.mapped("asset_id")
grouped_dlines = []
for asset in asset_ids:
grouped_dlines.append(dlines.filtered(lambda l: l.asset_id.id == asset.id))
for dlines in grouped_dlines:
for i, dl in enumerate(dlines):
if i == 0:
depreciation_base = dl.depreciation_base
tmp = depreciation_base - dl.previous_id.remaining_value
depreciated_value = dl.previous_id and tmp or 0.0
remaining_value = depreciation_base - depreciated_value - dl.amount
else:
depreciated_value += dl.previous_id.amount
remaining_value -= dl.amount
dl.depreciated_value = depreciated_value
dl.remaining_value = remaining_value
@api.depends("move_id")
def _compute_move_check(self):
for line in self:
line.move_check = bool(line.move_id)
@api.onchange("amount")
def _onchange_amount(self):
if self.type == "depreciate":
self.remaining_value = (
self.depreciation_base - self.depreciated_value - self.amount
)
def write(self, vals):
for dl in self:
line_date = vals.get("line_date") or dl.line_date
asset_lines = dl.asset_id.depreciation_line_ids
if list(vals.keys()) == ["move_id"] and not vals["move_id"]:
# allow to remove an accounting entry via the
# 'Delete Move' button on the depreciation lines.
if not self.env.context.get("unlink_from_asset"):
raise UserError(
_(
"You are not allowed to remove an accounting entry "
"linked to an asset."
"\nYou should remove such entries from the asset."
)
)
elif list(vals.keys()) == ["asset_id"]:
continue
elif (
dl.move_id
and not self.env.context.get("allow_asset_line_update")
and dl.type != "create"
):
raise UserError(
_(
"You cannot change a depreciation line "
"with an associated accounting entry."
)
)
elif vals.get("init_entry"):
check = asset_lines.filtered(
lambda l: l.move_check
and l.type == "depreciate"
and l.line_date <= line_date
)
if check:
raise UserError(
_(
"You cannot set the 'Initial Balance Entry' flag "
"on a depreciation line "
"with prior posted entries."
)
)
elif vals.get("line_date"):
if dl.type == "create":
check = asset_lines.filtered(
lambda l: l.type != "create"
and (l.init_entry or l.move_check)
and l.line_date < fields.Date.to_date(vals["line_date"])
)
if check:
raise UserError(
_(
"You cannot set the Asset Start Date "
"after already posted entries."
)
)
else:
check = asset_lines.filtered(
lambda al: al != dl
and (al.init_entry or al.move_check)
and al.line_date > fields.Date.to_date(vals["line_date"])
)
if check:
raise UserError(
_(
"You cannot set the date on a depreciation line "
"prior to already posted entries."
)
)
return super().write(vals)
def unlink(self):
for dl in self:
if dl.type == "create" and dl.amount:
raise UserError(
_("You cannot remove an asset line " "of type 'Depreciation Base'.")
)
elif dl.move_id:
raise UserError(
_(
"You cannot delete a depreciation line with "
"an associated accounting entry."
)
)
previous = dl.previous_id
next_line = dl.asset_id.depreciation_line_ids.filtered(
lambda l: l.previous_id == dl and l not in self
)
if next_line:
next_line.previous_id = previous
return super(
AccountAssetLine, self.with_context(no_compute_asset_line_ids=self.ids)
).unlink()
def _setup_move_data(self, depreciation_date):
asset = self.asset_id
move_data = {
"date": depreciation_date,
"ref": "{} - {}".format(asset.name, self.name),
"journal_id": asset.profile_id.journal_id.id,
}
return move_data
def _setup_move_line_data(self, depreciation_date, account, ml_type, move):
"""Prepare data to be propagated to account.move.line"""
asset = self.asset_id
currency = asset.company_id.currency_id
amount = self.amount
amount_comp = currency.compare_amounts(amount, 0)
analytic_distribution = False
if ml_type == "depreciation":
debit = amount_comp < 0 and -amount or 0.0
credit = amount_comp > 0 and amount or 0.0
elif ml_type == "expense":
debit = amount_comp > 0 and amount or 0.0
credit = amount_comp < 0 and -amount or 0.0
analytic_distribution = asset.analytic_distribution
move_line_data = {
"name": asset.name,
"ref": self.name,
"move_id": move.id,
"account_id": account.id,
"credit": credit,
"debit": debit,
"journal_id": asset.profile_id.journal_id.id,
"partner_id": asset.partner_id.id,
"analytic_distribution": analytic_distribution,
"date": depreciation_date,
"asset_id": asset.id,
}
return move_line_data
def create_move(self):
created_move_ids = []
asset_ids = set()
ctx = dict(self.env.context, allow_asset=True, check_move_validity=False)
for line in self:
asset = line.asset_id
depreciation_date = line.line_date
am_vals = line._setup_move_data(depreciation_date)
move = self.env["account.move"].with_context(**ctx).create(am_vals)
depr_acc = asset.profile_id.account_depreciation_id
exp_acc = asset.profile_id.account_expense_depreciation_id
aml_d_vals = line._setup_move_line_data(
depreciation_date, depr_acc, "depreciation", move
)
self.env["account.move.line"].with_context(**ctx).create(aml_d_vals)
aml_e_vals = line._setup_move_line_data(
depreciation_date, exp_acc, "expense", move
)
self.env["account.move.line"].with_context(**ctx).create(aml_e_vals)
move.action_post()
line.with_context(allow_asset_line_update=True).write({"move_id": move.id})
created_move_ids.append(move.id)
asset_ids.add(asset.id)
# we re-evaluate the assets to determine if we can close them
for asset in self.env["account.asset"].browse(list(asset_ids)):
if asset.currency_id.is_zero(asset.value_residual):
asset.state = "close"
return created_move_ids
def open_move(self):
self.ensure_one()
return {
"name": _("Journal Entry"),
"view_mode": "form",
"res_id": self.move_id.id,
"res_model": "account.move",
"view_id": False,
"type": "ir.actions.act_window",
"context": self.env.context,
}
def update_asset_line_after_unlink_move(self):
self.write({"move_id": False})
if self.parent_state == "close":
self.asset_id.write({"state": "open"})
elif self.parent_state == "removed" and self.type == "remove":
self.asset_id.write({"state": "close", "date_remove": False})
self.unlink()
def unlink_move(self):
for line in self:
if line.asset_id.profile_id.allow_reversal:
context = dict(self._context or {})
context.update(
{
"active_model": self._name,
"active_ids": line.ids,
"active_id": line.id,
}
)
return {
"name": _("Reverse Move"),
"view_mode": "form",
"res_model": "wiz.asset.move.reverse",
"target": "new",
"type": "ir.actions.act_window",
"context": context,
}
else:
move = line.move_id
move.button_draft()
move.with_context(force_delete=True, unlink_from_asset=True).unlink()
line.with_context(
unlink_from_asset=True
).update_asset_line_after_unlink_move()
return True

View File

@ -0,0 +1,232 @@
# 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
_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), ('company_id', '=', company_id)]",
string="Asset Account",
check_company=True,
required=True,
)
account_depreciation_id = fields.Many2one(
comodel_name="account.account",
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
string="Depreciation Account",
check_company=True,
required=True,
)
account_expense_depreciation_id = fields.Many2one(
comodel_name="account.account",
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
string="Depr. Expense Account",
check_company=True,
required=True,
)
account_plus_value_id = fields.Many2one(
comodel_name="account.account",
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
check_company=True,
string="Plus-Value Account",
)
account_min_value_id = fields.Many2one(
comodel_name="account.account",
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
check_company=True,
string="Min-Value Account",
)
account_residual_value_id = fields.Many2one(
comodel_name="account.account",
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
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",
)
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", _("Linear")),
("linear-limit", _("Linear up to Salvage Value")),
("degressive", _("Degressive")),
("degr-linear", _("Degressive-Linear")),
("degr-limit", _("Degressive up to Salvage Value")),
]
@api.model
def _selection_method_period(self):
return [("month", _("Month")), ("quarter", _("Quarter")), ("year", _("Year"))]
@api.model
def _selection_method_time(self):
return [
("year", _("Number of Years or end date")),
("number", _("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(
_("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

View 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,
)

View File

@ -0,0 +1,259 @@
# 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).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tests.common import Form
_logger = logging.getLogger(__name__)
# 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"].search(
[("move_id", "in", self.ids), ("type", "in", ["depreciate", "remove"])]
)
if deprs and not self.env.context.get("unlink_from_asset"):
raise UserError(
_(
"You are not allowed to remove an accounting entry "
"linked to an asset."
"\nYou should remove such entries from the asset."
)
)
# 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"].search(
[("move_id", "in", self.ids), ("type", "=", "depreciate")]
)
if deprs:
raise UserError(
_(
"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,
"purchase_value": depreciation_base,
"partner_id": aml.partner_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
):
vals = move._prepare_asset_vals(aml)
if not aml.name:
raise UserError(
_("Asset name must be set in the label of the line.")
)
if aml.asset_id:
continue
asset_form = Form(
self.env["account.asset"]
.with_company(move.company_id)
.with_context(create_asset_from_move_line=True, move_id=move.id)
)
for key, val in vals.items():
setattr(asset_form, key, val)
asset = asset_form.save()
asset.analytic_distribution = aml.analytic_distribution
aml.with_context(
allow_asset=True, allow_asset_removal=True
).asset_id = asset.id
refs = [
"<a href=# data-oe-model=account.asset data-oe-id=%s>%s</a>"
% tuple(name_get)
for name_get in move.line_ids.filtered(
"asset_profile_id"
).asset_id.name_get()
]
if refs:
message = _("This invoice created the asset(s): %s") % ", ".join(refs)
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(
_(
"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(
_(
"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(
_(
"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 and self.quantity > 1.0:
profile = self.asset_profile_id
if profile.asset_product_item:
aml = self.with_context(check_move_validity=False)
qty = self.quantity
name = self.name
aml.write({"quantity": 1, "name": "{} {}".format(name, 1)})
for i in range(1, int(qty)):
aml.copy({"name": "{} {}".format(name, i + 1)})

View File

@ -0,0 +1,26 @@
* OpenERP SA
* Luc De Meyer (Noviat)
* Frédéric Clementi (camptocamp)
* Florian Dacosta (Akretion)
* Stéphane Bidoul (Acsone)
* Adrien Peiffer (Acsone)
* Akim Juillerat <akim.juillerat@camptocamp.com>
* Henrik Norlin (Apps2GROW)
* Maxence Groine <mgroine@fiefmanage.ch>
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>
* Saran Lim. <saranl@ecosoft.co.th>
* `Tecnativa <https://www.tecnativa.com>`_:
* Ernesto Tejeda
* Pedro M. Baeza
* João Marques
* Víctor Martínez
* `ForgeFlow <https://www.forgeflow.com>`_:
* Jordi Ballester <jordi.ballester@forgeflow.com>
* Miquel Raïch <miquel.raich@forgeflow.com>
* `Sygel <https://www.sygel.es>`_:
* Manuel Regidor <manuel.regidor@sygel.es>

View File

@ -0,0 +1,19 @@
This Module manages the assets owned by a company. It will keep
track of depreciation's occurred on those assets. And it allows to create
accounting entries from the depreciation lines.
The full asset life-cycle is managed (from asset creation to asset removal).
Assets can be created manually as well as automatically
(via the creation of an accounting entry on the asset account).
Depreciation Journal Entries can be created manually in the "Deprecation Board" tab,
or automatically by two ways:
* Using the "Invoicing/Assets/Compute Assets" wizard.
* Activating the "Asset Management: Generate assets" cron.
These options are compatibles each other.
The module contains a large number of functional enhancements compared to
the standard account_asset module from Odoo.

View File

@ -0,0 +1,43 @@
14.0.1.0.0 (2021-01-08)
~~~~~~~~~~~~~~~~~~~~~~~
* [BREAKING] Removed all functionality associated with `account.fiscal.year`
13.0.3.0.0 (2021-07-06)
~~~~~~~~~~~~~~~~~~~~~~~
* Allow to reverse the posting of a depreciation line instead of deleting the
journal entry.
13.0.2.0.0 (2021-02-19)
~~~~~~~~~~~~~~~~~~~~~~~
* Add support for multi-company
13.0.1.0.0 (2019-10-21)
~~~~~~~~~~~~~~~~~~~~~~~
* Python code and views were adapted to be compatible with v13.
* When assets are created through accounting journal items,
they are created when the journal items is posted.
* When a Bill Invoice is created or modified, at the time it is saved,
for each line that has an Asset profile and Quantity 'N'
greater than 1, it will be replaced by 'N' lines identical to it but
with quantity 1. This was done to maintain the same behavior as in
the previous version, in which for each asset created there is a
Journal Item. In addition, this solution does not change the data
model which does not cause migration scripts.
* The configuration option was removed so the only function of that is to
allow the module to be uninstalled by unchecking that configuration option.
* Tests were adapted.
12.0.2.1.0 (2019-10-21)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP] Add option to calculate depreciation table by days
12.0.1.0.0 (2019-01-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [BREAKING] account.asset: parent_path has replaced parent_left & parent_right (TODO: migration script)
* [BREAKING] account.asset.recompute.trigger: depends on date_range.py (TODO: re-implement in account_fiscal_year.py)

View File

@ -0,0 +1 @@
The module in NOT compatible with the standard account_asset module.

View File

@ -0,0 +1 @@
from . import account_asset_report_xls

View File

@ -0,0 +1,734 @@
# Copyright 2009-2019 Noviat
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, models
from odoo.exceptions import UserError
from odoo.addons.report_xlsx_helper.report.report_xlsx_format import (
FORMATS,
XLS_HEADERS,
)
_logger = logging.getLogger(__name__)
IR_TRANSLATION_NAME = "account.asset.report"
class AssetReportXlsx(models.AbstractModel):
_name = "report.account_asset_management.asset_report_xls"
_description = "Dynamic XLS asset report generator"
_inherit = "report.report_xlsx.abstract"
def _get_ws_params(self, wb, data, wiz):
self._get_assets(wiz, data)
s1 = self._get_acquisition_ws_params(wb, data, wiz)
s2 = self._get_active_ws_params(wb, data, wiz)
s3 = self._get_removal_ws_params(wb, data, wiz)
return [s1, s2, s3]
def _get_asset_template(self):
asset_template = {
"account": {
"header": {"type": "string", "value": _("Account")},
"asset": {
"type": "string",
"value": self._render(
"asset.profile_id.account_asset_id.code or ''"
),
},
"totals": {"type": "string", "value": _("Totals")},
"width": 20,
},
"name": {
"header": {"type": "string", "value": _("Name")},
"asset_group": {
"type": "string",
"value": self._render("group.name or ''"),
},
"asset": {"type": "string", "value": self._render("asset.name")},
"width": 40,
},
"code": {
"header": {"type": "string", "value": _("Reference")},
"asset_group": {
"type": "string",
"value": self._render("group.code or ''"),
},
"asset": {"type": "string", "value": self._render("asset.code or ''")},
"width": 20,
},
"date_start": {
"header": {"type": "string", "value": _("Asset Start Date")},
"asset": {
"value": self._render("asset.date_start or ''"),
"format": FORMATS["format_tcell_date_left"],
},
"width": 20,
},
"date_remove": {
"header": {"type": "string", "value": _("Asset Removal Date")},
"asset": {
"value": self._render("asset.date_remove or ''"),
"format": FORMATS["format_tcell_date_left"],
},
"width": 20,
},
"depreciation_base": {
"header": {
"type": "string",
"value": _("Depreciation Base"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "number",
"value": self._render('group_entry["_depreciation_base"]'),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "number",
"value": self._render("asset.depreciation_base"),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("asset_total_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"salvage_value": {
"header": {
"type": "string",
"value": _("Salvage Value"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "number",
"value": self._render('group_entry["_salvage_value"]'),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "number",
"value": self._render("asset.salvage_value"),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("salvage_total_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"purchase_value": {
"header": {
"type": "string",
"value": _("Purchase Value"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "number",
"value": self._render('group_entry["_purchase_value"]'),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "number",
"value": self._render("asset.purchase_value"),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("purchase_total_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"period_start_value": {
"header": {
"type": "string",
"value": _("Period Start Value"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "number",
"value": self._render('group_entry["_period_start_value"]'),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "number",
"value": self._render('asset_entry["_period_start_value"]'),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("period_start_total_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"period_depr": {
"header": {
"type": "string",
"value": _("Period Depreciation"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "formula",
"value": self._render("period_diff_formula"),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "formula",
"value": self._render("period_diff_formula"),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("period_diff_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"period_end_value": {
"header": {
"type": "string",
"value": _("Period End Value"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "number",
"value": self._render('group_entry["_period_end_value"]'),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "number",
"value": self._render('asset_entry["_period_end_value"]'),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("period_end_total_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"period_end_depr": {
"header": {
"type": "string",
"value": _("Tot. Depreciation"),
"format": FORMATS["format_theader_yellow_right"],
},
"asset_group": {
"type": "formula",
"value": self._render("total_depr_formula"),
"format": FORMATS["format_theader_blue_amount_right"],
},
"asset": {
"type": "formula",
"value": self._render("total_depr_formula"),
"format": FORMATS["format_tcell_amount_right"],
},
"totals": {
"type": "formula",
"value": self._render("total_depr_formula"),
"format": FORMATS["format_theader_yellow_amount_right"],
},
"width": 18,
},
"method": {
"header": {
"type": "string",
"value": _("Comput. Method"),
"format": FORMATS["format_theader_yellow_center"],
},
"asset": {
"type": "string",
"value": self._render("asset.method or ''"),
"format": FORMATS["format_tcell_center"],
},
"width": 20,
},
"method_number": {
"header": {
"type": "string",
"value": _("Number of Years"),
"format": FORMATS["format_theader_yellow_center"],
},
"asset": {
"type": "number",
"value": self._render("asset.method_number"),
"format": FORMATS["format_tcell_integer_center"],
},
"width": 20,
},
"prorata": {
"header": {
"type": "string",
"value": _("Prorata Temporis"),
"format": FORMATS["format_theader_yellow_center"],
},
"asset": {
"type": "boolean",
"value": self._render("asset.prorata"),
"format": FORMATS["format_tcell_center"],
},
"width": 20,
},
"state": {
"header": {
"type": "string",
"value": _("Status"),
"format": FORMATS["format_theader_yellow_center"],
},
"asset": {
"type": "string",
"value": self._render("asset.state"),
"format": FORMATS["format_tcell_center"],
},
"width": 8,
},
}
asset_template.update(self.env["account.asset"]._xls_asset_template())
return asset_template
def _get_acquisition_ws_params(self, wb, data, wiz):
acquisition_template = self._get_asset_template()
acquisition_template.update(
self.env["account.asset"]._xls_acquisition_template()
)
wl_acq = self.env["account.asset"]._xls_acquisition_fields()
title = self._get_title(wiz, "acquisition", frmt="normal")
title_short = self._get_title(wiz, "acquisition", frmt="short")
sheet_name = title_short[:31].replace("/", "-")
return {
"ws_name": sheet_name,
"generate_ws_method": "_asset_report",
"title": title,
"wanted_list": wl_acq,
"col_specs": acquisition_template,
"report_type": "acquisition",
}
def _get_active_ws_params(self, wb, data, wiz):
active_template = self._get_asset_template()
active_template.update(self.env["account.asset"]._xls_active_template())
wl_act = self.env["account.asset"]._xls_active_fields()
title = self._get_title(wiz, "active", frmt="normal")
title_short = self._get_title(wiz, "active", frmt="short")
sheet_name = title_short[:31].replace("/", "-")
return {
"ws_name": sheet_name,
"generate_ws_method": "_asset_report",
"title": title,
"wanted_list": wl_act,
"col_specs": active_template,
"report_type": "active",
}
def _get_removal_ws_params(self, wb, data, wiz):
removal_template = self._get_asset_template()
removal_template.update(self.env["account.asset"]._xls_removal_template())
wl_dsp = self.env["account.asset"]._xls_removal_fields()
title = self._get_title(wiz, "removal", frmt="normal")
title_short = self._get_title(wiz, "removal", frmt="short")
sheet_name = title_short[:31].replace("/", "-")
return {
"ws_name": sheet_name,
"generate_ws_method": "_asset_report",
"title": title,
"wanted_list": wl_dsp,
"col_specs": removal_template,
"report_type": "removal",
}
def _get_title(self, wiz, report, frmt="normal"):
prefix = "{} - {}".format(wiz.date_from, wiz.date_to)
if report == "acquisition":
if frmt == "normal":
title = prefix + " : " + _("New Acquisitions")
else:
title = "ACQ"
elif report == "active":
if frmt == "normal":
title = prefix + " : " + _("Active Assets")
else:
title = "ACT"
else:
if frmt == "normal":
title = prefix + " : " + _("Removed Assets")
else:
title = "DSP"
return title
def _report_title(self, ws, row_pos, ws_params, data, wiz):
return self._write_ws_title(ws, row_pos, ws_params)
def _empty_report(self, ws, row_pos, ws_params, data, wiz):
report = ws_params["report_type"]
if report == "acquisition":
suffix = _("New Acquisitions")
elif report == "active":
suffix = _("Active Assets")
else:
suffix = _("Removed Assets")
no_entries = _("No") + " " + suffix
ws.write_string(row_pos, 0, no_entries, FORMATS["format_left_bold"])
def _get_assets(self, wiz, data):
"""Add the selected assets, both grouped and ungrouped, to `data`"""
dom = [
("date_start", "<=", wiz.date_to),
"|",
("date_remove", "=", False),
("date_remove", ">=", wiz.date_from),
]
parent_group = wiz.asset_group_id
if parent_group:
def _child_get(parent):
groups = [parent]
children = parent.child_ids
children = children.sorted(lambda r: r.code or r.name)
for child in children:
if child in groups:
raise UserError(
_(
"Inconsistent reporting structure."
"\nPlease correct Asset Group '{group}' (id {id})"
).format(group=child.name, id=child.id)
)
groups.extend(_child_get(child))
return groups
groups = _child_get(parent_group)
dom.append(("group_ids", "in", [x.id for x in groups]))
if not wiz.draft:
dom.append(("state", "!=", "draft"))
assets = self.env["account.asset"].search(dom)
grouped_assets = {}
self._group_assets(assets, parent_group, grouped_assets)
data.update(
{
"assets": assets,
"grouped_assets": grouped_assets,
}
)
@staticmethod
def acquisition_filter(wiz, asset):
return asset.date_start >= wiz.date_from
@staticmethod
def active_filter(wiz, asset):
return True
@staticmethod
def removal_filter(wiz, asset):
return (
asset.date_remove
and asset.date_remove >= wiz.date_from
and asset.date_remove <= wiz.date_to
)
def _group_assets(self, assets, group, grouped_assets):
if group:
group_assets = assets.filtered(lambda r: group in r.group_ids)
else:
group_assets = assets
group_assets = group_assets.sorted(
lambda r: (r.date_start or "", r.code or "", r.name)
)
grouped_assets[group] = {"assets": group_assets}
for child in group.child_ids:
self._group_assets(assets, child, grouped_assets[group])
def _create_report_entries(
self, ws_params, wiz, entries, group, group_val, error_dict
):
report = ws_params["report_type"]
def asset_filter(asset):
filt = getattr(self, "{}_filter".format(report))
return filt(wiz, asset)
def _has_assets(group, group_val):
assets = group_val.get("assets")
assets = assets.filtered(asset_filter)
if assets:
return True
for child in group.child_ids:
if _has_assets(child, group_val[child]):
return True
return False
assets = group_val.get("assets")
assets = assets.filtered(asset_filter)
# remove empty entries
if not _has_assets(group, group_val):
return
asset_entries = []
group_entry = {
"_purchase_value": 0.0,
"_depreciation_base": 0.0,
"_salvage_value": 0.0,
"_period_start_value": 0.0,
"_period_end_value": 0.0,
"group": group,
}
for asset in assets:
asset_entry = {"asset": asset}
group_entry["_purchase_value"] += asset.purchase_value
group_entry["_depreciation_base"] += asset.depreciation_base
group_entry["_salvage_value"] += asset.salvage_value
dls_all = asset.depreciation_line_ids.filtered(
lambda r: r.type == "depreciate"
)
dls_all = dls_all.sorted(key=lambda r: r.line_date)
if not dls_all:
error_dict["no_table"] += asset
# period_start_value
dls = dls_all.filtered(lambda r: r.line_date <= wiz.date_from)
if dls:
value_depreciated = dls[-1].depreciated_value + dls[-1].amount
else:
value_depreciated = 0.0
asset_entry["_period_start_value"] = (
asset.depreciation_base - value_depreciated
)
group_entry["_period_start_value"] += asset_entry["_period_start_value"]
# period_end_value
dls = dls_all.filtered(lambda r: r.line_date <= wiz.date_to)
if dls:
value_depreciated = dls[-1].depreciated_value + dls[-1].amount
else:
value_depreciated = 0.0
asset_entry["_period_end_value"] = (
asset.depreciation_base - value_depreciated
)
group_entry["_period_end_value"] += asset_entry["_period_end_value"]
asset_entries.append(asset_entry)
todos = []
for g in group.child_ids:
if _has_assets(g, group_val[g]):
todos.append(g)
entries.append(group_entry)
entries.extend(asset_entries)
for todo in todos:
self._create_report_entries(
ws_params, wiz, entries, todo, group_val[todo], error_dict
)
def _asset_report(self, workbook, ws, ws_params, data, wiz):
report = ws_params["report_type"]
ws.set_portrait()
ws.fit_to_pages(1, 0)
ws.set_header(XLS_HEADERS["xls_headers"]["standard"])
ws.set_footer(XLS_HEADERS["xls_footers"]["standard"])
wl = ws_params["wanted_list"]
if "account" not in wl:
raise UserError(
_(
"The 'account' field is a mandatory entry of the "
"'_xls_%s_fields' list !"
)
% report
)
self._set_column_width(ws, ws_params)
row_pos = 0
row_pos = self._report_title(ws, row_pos, ws_params, data, wiz)
def asset_filter(asset):
filt = getattr(self, "{}_filter".format(report))
return filt(wiz, asset)
assets = data["assets"].filtered(asset_filter)
if not assets:
return self._empty_report(ws, row_pos, ws_params, data, wiz)
row_pos = self._write_line(
ws,
row_pos,
ws_params,
col_specs_section="header",
default_format=FORMATS["format_theader_yellow_left"],
)
ws.freeze_panes(row_pos, 0)
row_pos_start = row_pos
purchase_value_pos = "purchase_value" in wl and wl.index("purchase_value")
depreciation_base_pos = "depreciation_base" in wl and wl.index(
"depreciation_base"
)
salvage_value_pos = "salvage_value" in wl and wl.index("salvage_value")
period_start_value_pos = "period_start_value" in wl and wl.index(
"period_start_value"
)
period_end_value_pos = "period_end_value" in wl and wl.index("period_end_value")
entries = []
root = wiz.asset_group_id
root_val = data["grouped_assets"][root]
error_dict = {
"no_table": self.env["account.asset"],
"dups": self.env["account.asset"],
}
self._create_report_entries(ws_params, wiz, entries, root, root_val, error_dict)
# traverse entries in reverse order to calc totals
for i, entry in enumerate(reversed(entries)):
if "group" in entry:
parent = entry["group"].parent_id
for parent_entry in reversed(entries[: -i - 1]):
if "group" in parent_entry and parent_entry["group"] == parent:
parent_entry["_purchase_value"] += entry["_purchase_value"]
parent_entry["_depreciation_base"] += entry[
"_depreciation_base"
]
parent_entry["_salvage_value"] += entry["_salvage_value"]
parent_entry["_period_start_value"] += entry[
"_period_start_value"
]
parent_entry["_period_end_value"] += entry["_period_end_value"]
continue
processed = []
for entry in entries:
period_start_value_cell = period_start_value_pos and self._rowcol_to_cell(
row_pos, period_start_value_pos
)
period_end_value_cell = period_end_value_pos and self._rowcol_to_cell(
row_pos, period_end_value_pos
)
depreciation_base_cell = depreciation_base_pos and self._rowcol_to_cell(
row_pos, depreciation_base_pos
)
period_diff_formula = period_end_value_cell and (
period_start_value_cell + "-" + period_end_value_cell
)
total_depr_formula = period_end_value_cell and (
depreciation_base_cell + "-" + period_end_value_cell
)
if "group" in entry:
row_pos = self._write_line(
ws,
row_pos,
ws_params,
col_specs_section="asset_group",
render_space={
"group": entry["group"],
"group_entry": entry,
"period_diff_formula": period_diff_formula,
"total_depr_formula": total_depr_formula,
},
default_format=FORMATS["format_theader_blue_left"],
)
else:
asset = entry["asset"]
if asset in processed:
error_dict["dups"] += asset
continue
else:
processed.append(asset)
row_pos = self._write_line(
ws,
row_pos,
ws_params,
col_specs_section="asset",
render_space={
"asset": entry["asset"],
"asset_entry": entry,
"period_diff_formula": period_diff_formula,
"total_depr_formula": total_depr_formula,
},
default_format=FORMATS["format_tcell_left"],
)
purchase_total_formula = purchase_value_pos and self._rowcol_to_cell(
row_pos_start, purchase_value_pos
)
asset_total_formula = depreciation_base_pos and self._rowcol_to_cell(
row_pos_start, depreciation_base_pos
)
salvage_total_formula = salvage_value_pos and self._rowcol_to_cell(
row_pos_start, salvage_value_pos
)
period_start_total_formula = period_start_value_pos and self._rowcol_to_cell(
row_pos_start, period_start_value_pos
)
period_end_total_formula = period_end_value_pos and self._rowcol_to_cell(
row_pos_start, period_end_value_pos
)
period_start_value_cell = period_start_value_pos and self._rowcol_to_cell(
row_pos, period_start_value_pos
)
period_end_value_cell = period_end_value_pos and self._rowcol_to_cell(
row_pos, period_end_value_pos
)
depreciation_base_cell = depreciation_base_pos and self._rowcol_to_cell(
row_pos, depreciation_base_pos
)
period_diff_formula = period_end_value_cell and (
period_start_value_cell + "-" + period_end_value_cell
)
total_depr_formula = period_end_value_cell and (
depreciation_base_cell + "-" + period_end_value_cell
)
row_pos = self._write_line(
ws,
row_pos,
ws_params,
col_specs_section="totals",
render_space={
"purchase_total_formula": purchase_total_formula,
"asset_total_formula": asset_total_formula,
"salvage_total_formula": salvage_total_formula,
"period_start_total_formula": period_start_total_formula,
"period_end_total_formula": period_end_total_formula,
"period_diff_formula": period_diff_formula,
"total_depr_formula": total_depr_formula,
},
default_format=FORMATS["format_theader_yellow_left"],
)
for k in error_dict:
if error_dict[k]:
if k == "no_table":
reason = _("Missing depreciation table")
elif k == "dups":
reason = _("Duplicate reporting entries")
else:
reason = _("Undetermined error")
row_pos += 1
err_msg = _("Assets to be corrected") + ": "
err_msg += "%s" % [x[1] for x in error_dict[k].name_get()]
err_msg += " - " + _("Reason") + ": " + reason
ws.write_string(row_pos, 0, err_msg, FORMATS["format_left_bold"])

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="account_asset_profile_multi_company_rule" model="ir.rule">
<field name="name">Account Asset Profile multi-company</field>
<field ref="model_account_asset_profile" name="model_id" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="account_asset_multi_company_rule" model="ir.rule">
<field name="name">Account Asset multi-company</field>
<field ref="model_account_asset" name="model_id" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="account_asset_group_multi_company_rule" model="ir.rule">
<field name="name">Account Asset Group multi-company</field>
<field ref="model_account_asset_group" name="model_id" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@ -0,0 +1,19 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_asset_profile_invoice,account.asset.profile,model_account_asset_profile,account.group_account_invoice,1,0,0,0
access_account_asset_profile_user,account.asset.profile,model_account_asset_profile,account.group_account_user,1,0,0,0
access_account_asset_profile_manager,account.asset.profile,model_account_asset_profile,account.group_account_manager,1,1,1,1
access_account_asset_invoice,account.asset,model_account_asset,account.group_account_invoice,1,1,1,1
access_account_asset_user,account.asset,model_account_asset,account.group_account_user,1,1,1,1
access_account_asset_manager,account.asset,model_account_asset,account.group_account_manager,1,1,1,1
access_account_asset_line_invoice,account.asset.line,model_account_asset_line,account.group_account_invoice,1,1,1,1
access_account_asset_line_user,account.asset.line,model_account_asset_line,account.group_account_user,1,1,1,1
access_account_asset_line_manager,account.asset.line,model_account_asset_line,account.group_account_manager,1,1,1,1
access_account_asset_recompute_trigger_user,account.asset.recompute.trigger,model_account_asset_recompute_trigger,account.group_account_user,1,1,1,1
access_account_asset_recompute_trigger_manager,account.asset.recompute.trigger,model_account_asset_recompute_trigger,account.group_account_manager,1,1,1,1
access_account_asset_group_invoice,account.asset.group,model_account_asset_group,account.group_account_invoice,1,0,0,0
access_account_asset_group_user,account.asset.group,model_account_asset_group,account.group_account_user,1,0,0,0
access_account_asset_group_manager,account.asset.group,model_account_asset_group,account.group_account_manager,1,1,1,1
access_account_asset_remove_user,account.asset.remove,model_account_asset_remove,account.group_account_user,1,1,1,1
access_account_asset_compute_user,account.asset.compute,model_account_asset_compute,account.group_account_user,1,1,1,1
access_wiz_account_asset_report,wiz.account.asset.report,model_wiz_account_asset_report,account.group_account_readonly,1,1,1,0
access_wiz_asset_move_reverse_user,wiz.asset.move.reverse,model_wiz_asset_move_reverse,account.group_account_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_asset_profile_invoice account.asset.profile model_account_asset_profile account.group_account_invoice 1 0 0 0
3 access_account_asset_profile_user account.asset.profile model_account_asset_profile account.group_account_user 1 0 0 0
4 access_account_asset_profile_manager account.asset.profile model_account_asset_profile account.group_account_manager 1 1 1 1
5 access_account_asset_invoice account.asset model_account_asset account.group_account_invoice 1 1 1 1
6 access_account_asset_user account.asset model_account_asset account.group_account_user 1 1 1 1
7 access_account_asset_manager account.asset model_account_asset account.group_account_manager 1 1 1 1
8 access_account_asset_line_invoice account.asset.line model_account_asset_line account.group_account_invoice 1 1 1 1
9 access_account_asset_line_user account.asset.line model_account_asset_line account.group_account_user 1 1 1 1
10 access_account_asset_line_manager account.asset.line model_account_asset_line account.group_account_manager 1 1 1 1
11 access_account_asset_recompute_trigger_user account.asset.recompute.trigger model_account_asset_recompute_trigger account.group_account_user 1 1 1 1
12 access_account_asset_recompute_trigger_manager account.asset.recompute.trigger model_account_asset_recompute_trigger account.group_account_manager 1 1 1 1
13 access_account_asset_group_invoice account.asset.group model_account_asset_group account.group_account_invoice 1 0 0 0
14 access_account_asset_group_user account.asset.group model_account_asset_group account.group_account_user 1 0 0 0
15 access_account_asset_group_manager account.asset.group model_account_asset_group account.group_account_manager 1 1 1 1
16 access_account_asset_remove_user account.asset.remove model_account_asset_remove account.group_account_user 1 1 1 1
17 access_account_asset_compute_user account.asset.compute model_account_asset_compute account.group_account_user 1 1 1 1
18 access_wiz_account_asset_report wiz.account.asset.report model_wiz_account_asset_report account.group_account_readonly 1 1 1 0
19 access_wiz_asset_move_reverse_user wiz.asset.move.reverse model_wiz_asset_move_reverse account.group_account_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,528 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Assets Management</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="assets-management">
<h1 class="title">Assets Management</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Mature" src="https://img.shields.io/badge/maturity-Mature-brightgreen.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/15.0/account_asset_management"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-financial-tools-15-0/account-financial-tools-15-0-account_asset_management"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/92/15.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This Module manages the assets owned by a company. It will keep
track of depreciations occurred on those assets. And it allows to create
accounting entries from the depreciation lines.</p>
<p>The full asset life-cycle is managed (from asset creation to asset removal).</p>
<p>Assets can be created manually as well as automatically
(via the creation of an accounting entry on the asset account).</p>
<p>Depreciation Journal Entries can be created manually in the “Deprecation Board” tab,
or automatically by two ways:</p>
<ul class="simple">
<li>Using the “Invoicing/Assets/Compute Assets” wizard.</li>
<li>Activating the “Asset Management: Generate assets” cron.</li>
</ul>
<p>These options are compatibles each other.</p>
<p>The module contains a large number of functional enhancements compared to
the standard account_asset module from Odoo.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id7">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="id8">Changelog</a><ul>
<li><a class="reference internal" href="#id1" id="id9">14.0.1.0.0 (2021-01-08)</a></li>
<li><a class="reference internal" href="#id2" id="id10">13.0.3.0.0 (2021-07-06)</a></li>
<li><a class="reference internal" href="#id3" id="id11">13.0.2.0.0 (2021-02-19)</a></li>
<li><a class="reference internal" href="#id4" id="id12">13.0.1.0.0 (2019-10-21)</a></li>
<li><a class="reference internal" href="#id5" id="id13">12.0.2.1.0 (2019-10-21)</a></li>
<li><a class="reference internal" href="#id6" id="id14">12.0.1.0.0 (2019-01-13)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="id15">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id16">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id17">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id18">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id19">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id7">Usage</a></h1>
<p>The module in NOT compatible with the standard account_asset module.</p>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#id8">Changelog</a></h1>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id9">14.0.1.0.0 (2021-01-08)</a></h2>
<blockquote>
<ul class="simple">
<li>[BREAKING] Removed all functionality associated with <cite>account.fiscal.year</cite></li>
</ul>
</blockquote>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id10">13.0.3.0.0 (2021-07-06)</a></h2>
<ul class="simple">
<li>Allow to reverse the posting of a depreciation line instead of deleting the
journal entry.</li>
</ul>
</div>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id11">13.0.2.0.0 (2021-02-19)</a></h2>
<ul class="simple">
<li>Add support for multi-company</li>
</ul>
</div>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id12">13.0.1.0.0 (2019-10-21)</a></h2>
<ul class="simple">
<li>Python code and views were adapted to be compatible with v13.</li>
<li>When assets are created through accounting journal items,
they are created when the journal items is posted.</li>
<li>When a Bill Invoice is created or modified, at the time it is saved,
for each line that has an Asset profile and Quantity N
greater than 1, it will be replaced by N lines identical to it but
with quantity 1. This was done to maintain the same behavior as in
the previous version, in which for each asset created there is a
Journal Item. In addition, this solution does not change the data
model which does not cause migration scripts.</li>
<li>The configuration option was removed so the only function of that is to
allow the module to be uninstalled by unchecking that configuration option.</li>
<li>Tests were adapted.</li>
</ul>
</div>
<div class="section" id="id5">
<h2><a class="toc-backref" href="#id13">12.0.2.1.0 (2019-10-21)</a></h2>
<ul class="simple">
<li>[IMP] Add option to calculate depreciation table by days</li>
</ul>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id14">12.0.1.0.0 (2019-01-13)</a></h2>
<ul class="simple">
<li>[BREAKING] account.asset: parent_path has replaced parent_left &amp; parent_right (TODO: migration script)</li>
<li>[BREAKING] account.asset.recompute.trigger: depends on date_range.py (TODO: re-implement in account_fiscal_year.py)</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id15">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_asset_management%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id16">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id17">Authors</a></h2>
<ul class="simple">
<li>Noviat</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id18">Contributors</a></h2>
<ul class="simple">
<li>OpenERP SA</li>
<li>Luc De Meyer (Noviat)</li>
<li>Frédéric Clementi (camptocamp)</li>
<li>Florian Dacosta (Akretion)</li>
<li>Stéphane Bidoul (Acsone)</li>
<li>Adrien Peiffer (Acsone)</li>
<li>Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;</li>
<li>Henrik Norlin (Apps2GROW)</li>
<li>Maxence Groine &lt;<a class="reference external" href="mailto:mgroine&#64;fiefmanage.ch">mgroine&#64;fiefmanage.ch</a>&gt;</li>
<li>Kitti Upariphutthiphong &lt;<a class="reference external" href="mailto:kittiu&#64;ecosoft.co.th">kittiu&#64;ecosoft.co.th</a>&gt;</li>
<li>Saran Lim. &lt;<a class="reference external" href="mailto:saranl&#64;ecosoft.co.th">saranl&#64;ecosoft.co.th</a>&gt;</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Ernesto Tejeda</li>
<li>Pedro M. Baeza</li>
<li>João Marques</li>
<li>Víctor Martínez</li>
</ul>
</li>
<li><a class="reference external" href="https://www.forgeflow.com">ForgeFlow</a>:<ul>
<li>Jordi Ballester &lt;<a class="reference external" href="mailto:jordi.ballester&#64;forgeflow.com">jordi.ballester&#64;forgeflow.com</a>&gt;</li>
<li>Miquel Raïch &lt;<a class="reference external" href="mailto:miquel.raich&#64;forgeflow.com">miquel.raich&#64;forgeflow.com</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://www.sygel.es">Sygel</a>:<ul>
<li>Manuel Regidor &lt;<a class="reference external" href="mailto:manuel.regidor&#64;sygel.es">manuel.regidor&#64;sygel.es</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id19">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/15.0/account_asset_management">OCA/account-financial-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,2 @@
from . import test_account_asset_management
from . import test_asset_management_xls

View File

@ -0,0 +1,957 @@
# Copyright (c) 2014 ACSONE SA/NV (acsone.eu).
# Copyright 2009-2018 Noviat
# Copyright 2021 Tecnativa - João Marques
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import calendar
import time
from datetime import date, datetime
from odoo import Command, fields
from odoo.tests import tagged
from odoo.tests.common import Form
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
@tagged("post_install", "-at_install")
class TestAssetManagement(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# ENVIRONMENTS
cls.asset_model = cls.env["account.asset"]
cls.asset_profile_model = cls.env["account.asset.profile"]
cls.dl_model = cls.env["account.asset.line"]
cls.remove_model = cls.env["account.asset.remove"]
# INSTANCES
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
cls.product = cls.env["product.product"].create(
{"name": "Test", "standard_price": 500.0}
)
cls.invoice = (
cls.env["account.move"]
.with_context(check_move_validity=False)
.create(
{
"move_type": "in_invoice",
"invoice_date": fields.Date.context_today(cls.env.user),
"partner_id": cls.partner.id,
"invoice_line_ids": [
Command.create(
{
"name": "test",
"product_id": cls.product.id,
"price_unit": 2000.00,
"quantity": 1,
}
),
],
}
)
)
cls.invoice_2 = (
cls.env["account.move"]
.with_context(check_move_validity=False)
.create(
{
"move_type": "in_invoice",
"invoice_date": fields.Date.context_today(cls.env.user),
"partner_id": cls.partner.id,
"invoice_line_ids": [
Command.create(
{
"name": "test 2",
"product_id": cls.product.id,
"price_unit": 10000.00,
"quantity": 1,
}
),
Command.create(
{
"name": "test 3",
"product_id": cls.product.id,
"price_unit": 20000.00,
"quantity": 1,
}
),
],
}
)
)
# analytic configuration
cls.env.user.groups_id += cls.env.ref("analytic.group_analytic_accounting")
cls.default_plan = cls.env["account.analytic.plan"].create(
{"name": "Default", "company_id": False}
)
cls.analytic_account = cls.env["account.analytic.account"].create(
{"name": "test_analytic_account", "plan_id": cls.default_plan.id}
)
cls.distribution = cls.env["account.analytic.distribution.model"].create(
{
"partner_id": cls.partner.id,
"analytic_distribution": {cls.analytic_account.id: 100},
}
)
# Asset Profile 1
cls.ict3Y = cls.asset_profile_model.create(
{
"account_expense_depreciation_id": cls.company_data[
"default_account_expense"
].id,
"account_asset_id": cls.company_data["default_account_assets"].id,
"account_depreciation_id": cls.company_data[
"default_account_assets"
].id,
"journal_id": cls.company_data["default_journal_purchase"].id,
"name": "Hardware - 3 Years",
"method_time": "year",
"method_number": 3,
"method_period": "year",
}
)
# Asset Profile 2
cls.car5y = cls.asset_profile_model.create(
{
"account_expense_depreciation_id": cls.company_data[
"default_account_expense"
].id,
"account_asset_id": cls.company_data["default_account_assets"].id,
"account_depreciation_id": cls.company_data[
"default_account_assets"
].id,
"journal_id": cls.company_data["default_journal_purchase"].id,
"name": "Cars - 5 Years",
"method_time": "year",
"method_number": 5,
"method_period": "year",
"analytic_distribution": cls.distribution._get_distribution(
{
"partner_id": cls.partner.id,
}
),
# "account_analytic_id": cls.analytic_account.id,
}
)
def test_invoice_line_without_product(self):
tax = self.env["account.tax"].create(
{
"name": "TAX 15%",
"amount_type": "percent",
"type_tax_use": "purchase",
"amount": 15.0,
}
)
invoice = (
self.env["account.move"]
.with_context(check_move_validity=False)
.create(
{
"move_type": "in_invoice",
"invoice_date": fields.Date.context_today(self.env.user),
"partner_id": self.partner.id,
"invoice_line_ids": [
Command.create(
{
"name": "Line 1",
"price_unit": 200.0,
"quantity": 1,
"tax_ids": [tax.id],
}
),
],
}
)
)
self.assertEqual(invoice.partner_id, self.partner)
def test_00_fiscalyear_lock_date_month(self):
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1500,
"date_start": "1901-02-01",
"method_time": "year",
"method_number": 3,
"method_period": "month",
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
self.assertTrue(asset.depreciation_line_ids[0].init_entry)
for i in range(1, 36):
self.assertFalse(asset.depreciation_line_ids[i].init_entry)
def test_00_fiscalyear_lock_date_year(self):
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1500,
"date_start": "1901-02-01",
"method_time": "year",
"method_number": 3,
"method_period": "year",
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
self.assertTrue(asset.depreciation_line_ids[0].init_entry)
for i in range(1, 4):
self.assertFalse(asset.depreciation_line_ids[i].init_entry)
def test_01_nonprorata_basic(self):
"""Basic tests of depreciation board computations and postings."""
# First create demo assets and do some sanity checks
# Asset Model 1
ict0 = self.asset_model.create(
{
"state": "draft",
"method_time": "year",
"method_number": 3,
"method_period": "year",
"name": "Laptop",
"code": "PI00101",
"purchase_value": 1500.0,
"profile_id": self.ict3Y.id,
"date_start": time.strftime("%Y-01-01"),
}
)
# Sanity checks
self.assertEqual(ict0.state, "draft")
self.assertEqual(ict0.purchase_value, 1500)
self.assertEqual(ict0.salvage_value, 0)
self.assertEqual(ict0.depreciation_base, 1500)
self.assertEqual(len(ict0.depreciation_line_ids), 1)
# Asset Model 2
vehicle0 = self.asset_model.create(
{
"state": "draft",
"method_time": "year",
"method_number": 5,
"method_period": "year",
"name": "CEO's Car",
"purchase_value": 12000.0,
"salvage_value": 2000.0,
"profile_id": self.car5y.id,
"date_start": time.strftime("%Y-01-01"),
}
)
# Sanity checks
self.assertEqual(vehicle0.state, "draft")
self.assertEqual(vehicle0.purchase_value, 12000)
self.assertEqual(vehicle0.salvage_value, 2000)
self.assertEqual(vehicle0.depreciation_base, 10000)
self.assertEqual(len(vehicle0.depreciation_line_ids), 1)
# Compute the depreciation boards
ict0.compute_depreciation_board()
ict0.invalidate_recordset()
self.assertEqual(len(ict0.depreciation_line_ids), 4)
self.assertEqual(ict0.depreciation_line_ids[1].amount, 500)
vehicle0.compute_depreciation_board()
vehicle0.invalidate_recordset()
self.assertEqual(len(vehicle0.depreciation_line_ids), 6)
self.assertEqual(vehicle0.depreciation_line_ids[1].amount, 2000)
# Post the first depreciation line
ict0.validate()
ict0.depreciation_line_ids[1].create_move()
ict0.invalidate_recordset()
self.assertEqual(ict0.state, "open")
self.assertEqual(ict0.value_depreciated, 500)
self.assertEqual(ict0.value_residual, 1000)
vehicle0.validate()
created_move_ids = vehicle0.depreciation_line_ids[1].create_move()
for move_id in created_move_ids:
move = self.env["account.move"].browse(move_id)
expense_line = move.line_ids.filtered(
lambda line: line.account_id.internal_group == "expense"
)
self.assertEqual(
expense_line.analytic_distribution,
self.distribution._get_distribution(
{
"partner_id": self.partner.id,
}
)
or False,
)
vehicle0.invalidate_recordset()
self.assertEqual(vehicle0.state, "open")
self.assertEqual(vehicle0.value_depreciated, 2000)
self.assertEqual(vehicle0.value_residual, 8000)
def test_02_prorata_basic(self):
"""Prorata temporis depreciation basic test."""
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 3333,
"salvage_value": 0,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": True,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
if calendar.isleap(date.today().year):
self.assertAlmostEqual(
asset.depreciation_line_ids[1].amount, 46.44, places=2
)
else:
self.assertAlmostEqual(
asset.depreciation_line_ids[1].amount, 47.33, places=2
)
self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 55.55, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 55.55, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[5].amount, 55.55, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[6].amount, 55.55, places=2)
if calendar.isleap(date.today().year):
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 9.11, places=2
)
else:
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 8.22, places=2
)
def test_03_proprata_init_prev_year(self):
"""Prorata temporis depreciation with init value in prev year."""
# Create an asset in current year
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 3333,
"salvage_value": 0,
"date_start": "%d-07-07" % (datetime.now().year - 1,),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": True,
}
)
# Create a initial depreciation line in previous year
self.dl_model.create(
{
"asset_id": asset.id,
"amount": 325.08,
"line_date": "%d-12-31" % (datetime.now().year - 1,),
"type": "depreciate",
"init_entry": True,
}
)
self.assertEqual(len(asset.depreciation_line_ids), 2)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check the depreciated value is the initial value
self.assertAlmostEqual(asset.value_depreciated, 325.08, places=2)
# check computed values in the depreciation board
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2)
if calendar.isleap(date.today().year - 1):
# for leap years the first year depreciation amount of 325.08
# is too high and hence a correction is applied to the next
# entry of the table
self.assertAlmostEqual(
asset.depreciation_line_ids[2].amount, 54.66, places=2
)
self.assertAlmostEqual(
asset.depreciation_line_ids[3].amount, 55.55, places=2
)
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 9.11, places=2
)
else:
self.assertAlmostEqual(
asset.depreciation_line_ids[2].amount, 55.55, places=2
)
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 8.22, places=2
)
def test_04_prorata_init_cur_year(self):
"""Prorata temporis depreciation with init value in curent year."""
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 3333,
"salvage_value": 0,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": True,
}
)
self.dl_model.create(
{
"asset_id": asset.id,
"amount": 279.44,
"line_date": time.strftime("%Y-11-30"),
"type": "depreciate",
"init_entry": True,
}
)
self.assertEqual(len(asset.depreciation_line_ids), 2)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check the depreciated value is the initial value
self.assertAlmostEqual(asset.value_depreciated, 279.44, places=2)
# check computed values in the depreciation board
if calendar.isleap(date.today().year):
self.assertAlmostEqual(
asset.depreciation_line_ids[2].amount, 44.75, places=2
)
else:
self.assertAlmostEqual(
asset.depreciation_line_ids[2].amount, 45.64, places=2
)
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2)
if calendar.isleap(date.today().year):
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 9.11, places=2
)
else:
self.assertAlmostEqual(
asset.depreciation_line_ids[-1].amount, 8.22, places=2
)
def test_05_degressive_linear(self):
"""Degressive-Linear with annual and quarterly depreciation."""
# annual depreciation
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1000,
"salvage_value": 0,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method": "degr-linear",
"method_progress_factor": 0.40,
"method_number": 5,
"method_period": "year",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check values in the depreciation board
self.assertEqual(len(asset.depreciation_line_ids), 5)
self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 400.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 240.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 200.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 160.00, places=2)
# quarterly depreciation
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1000,
"salvage_value": 0,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method": "degr-linear",
"method_progress_factor": 0.40,
"method_number": 5,
"method_period": "quarter",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check values in the depreciation board
self.assertEqual(len(asset.depreciation_line_ids), 15)
# lines prior to asset start period are grouped in the first entry
self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 300.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 60.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[7].amount, 50.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[13].amount, 40.00, places=2)
def test_06_degressive_limit(self):
"""Degressive with annual depreciation."""
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1000,
"salvage_value": 100,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method": "degr-limit",
"method_progress_factor": 0.40,
"method_number": 5,
"method_period": "year",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check values in the depreciation board
self.assertEqual(len(asset.depreciation_line_ids), 6)
self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 400.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 240.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 144.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 86.40, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[5].amount, 29.60, places=2)
def test_07_linear_limit(self):
"""Degressive with annual depreciation."""
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 1000,
"salvage_value": 100,
"date_start": time.strftime("%Y-07-07"),
"method_time": "year",
"method": "linear-limit",
"method_number": 5,
"method_period": "year",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
# check values in the depreciation board
self.assertEqual(len(asset.depreciation_line_ids), 6)
self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 200.00, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[-1].amount, 100.00, places=2)
def test_08_asset_removal(self):
"""Asset removal"""
asset = self.asset_model.create(
{
"name": "test asset removal",
"profile_id": self.car5y.id,
"purchase_value": 5000,
"salvage_value": 0,
"date_start": "2019-01-01",
"method_time": "year",
"method_number": 5,
"method_period": "quarter",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.validate()
wiz_ctx = {"active_id": asset.id, "early_removal": True}
wiz = self.remove_model.with_context(**wiz_ctx).create(
{
"date_remove": "2019-01-31",
"sale_value": 0.0,
"posting_regime": "gain_loss_on_sale",
"account_plus_value_id": self.company_data[
"default_account_revenue"
].id,
"account_min_value_id": self.company_data["default_account_expense"].id,
}
)
wiz.remove()
asset.invalidate_recordset()
self.assertEqual(len(asset.depreciation_line_ids), 3)
self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 81.46, places=2)
self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 4918.54, places=2)
def test_09_asset_from_invoice(self):
all_asset = self.env["account.asset"].search([])
invoice = self.invoice
asset_profile = self.car5y
asset_profile.asset_product_item = False
self.assertTrue(len(invoice.invoice_line_ids) > 0)
line = invoice.invoice_line_ids[0]
self.assertTrue(line.price_unit > 0.0)
invoice.invoice_line_ids[0].write(
{"quantity": 2, "asset_profile_id": asset_profile.id}
)
invoice.action_post()
# get all asset after invoice validation
current_asset = self.env["account.asset"].search([])
# get the new asset
new_asset = current_asset - all_asset
# check that a new asset is created
self.assertEqual(len(new_asset), 1)
# check that the new asset has the correct purchase value
self.assertAlmostEqual(
new_asset.purchase_value, line.price_unit * line.quantity, places=2
)
def test_10_asset_from_invoice_product_item(self):
all_asset = self.env["account.asset"].search([])
invoice = self.invoice
asset_profile = self.car5y
asset_profile.asset_product_item = True
self.assertTrue(len(invoice.invoice_line_ids) > 0)
line = invoice.invoice_line_ids[0]
self.assertTrue(line.price_unit > 0.0)
line.quantity = 2
line.asset_profile_id = asset_profile
self.assertEqual(len(invoice.invoice_line_ids), 2)
invoice.action_post()
# get all asset after invoice validation
current_asset = self.env["account.asset"].search([])
# get the new asset
new_asset = current_asset - all_asset
# check that a new asset is created
self.assertEqual(len(new_asset), 2)
for asset in new_asset:
# check that the new asset has the correct purchase value
self.assertAlmostEqual(asset.purchase_value, line.price_unit, places=2)
def test_11_assets_from_invoice(self):
all_assets = self.env["account.asset"].search([])
ctx = dict(self.invoice_2._context)
invoice = self.invoice_2.with_context(**ctx)
asset_profile = self.car5y
asset_profile.asset_product_item = True
# Compute depreciation lines on invoice validation
asset_profile.open_asset = True
self.assertTrue(len(invoice.invoice_line_ids) == 2)
invoice.invoice_line_ids.write(
{"quantity": 1, "asset_profile_id": asset_profile.id}
)
invoice.action_post()
# Retrieve all assets after invoice validation
current_assets = self.env["account.asset"].search([])
# What are the new assets?
new_assets = current_assets - all_assets
self.assertEqual(len(new_assets), 2)
for asset in new_assets:
dlines = asset.depreciation_line_ids.filtered(
lambda l: l.type == "depreciate"
)
dlines = dlines.sorted(key=lambda l: l.line_date)
self.assertAlmostEqual(dlines[0].depreciated_value, 0.0)
self.assertAlmostEqual(dlines[-1].remaining_value, 0.0)
def test_12_prorata_days_calc(self):
"""Prorata temporis depreciation with days calc option."""
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 3333,
"salvage_value": 0,
"date_start": "2019-07-07",
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": True,
"days_calc": True,
"use_leap_years": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
day_rate = 3333 / 1827 # 3333 / 1827 depreciation days
for i in range(1, 10):
self.assertAlmostEqual(
asset.depreciation_line_ids[i].amount,
asset.depreciation_line_ids[i].line_days * day_rate,
places=2,
)
# Last depreciation remaining
self.assertAlmostEqual(asset.depreciation_line_ids[-1].amount, 11.05, places=2)
def test_13_use_leap_year(self):
# When you use the depreciation with years method and using lap years,
# the depreciation amount is calculated as 10000 / 1826 days * 365 days
# = yearly depreciation amount of 1998.90.
# Then 1998.90 / 12 = 166.58
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 10000,
"salvage_value": 0,
"date_start": time.strftime("2019-01-01"),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": False,
"days_calc": False,
"use_leap_years": True,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
for i in range(2, 11):
self.assertAlmostEqual(
asset.depreciation_line_ids[i].amount, 166.58, places=2
)
self.assertAlmostEqual(
asset.depreciation_line_ids[13].depreciated_value, 1998.90, places=2
)
def test_14_not_use_leap_year(self):
# When you run a depreciation with method = 'year' and no not use
# lap years you divide 1000 / 5 years = 2000, then divided by 12 months
# to get 166.67 per month, equal for all periods.
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": self.car5y.id,
"purchase_value": 10000,
"salvage_value": 0,
"date_start": time.strftime("2019-01-01"),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": False,
"days_calc": False,
"use_leap_years": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
for _i in range(1, 11):
self.assertAlmostEqual(
asset.depreciation_line_ids[1].amount, 166.67, places=2
)
# In the last month of the fiscal year we compensate for the small
# deviations if that is necessary.
self.assertAlmostEqual(asset.depreciation_line_ids[12].amount, 166.63, places=2)
def test_15_account_asset_group(self):
"""Group's name_get behaves differently depending on code and context"""
group_fa = self.env["account.asset.group"].create(
{
"name": "Fixed Assets",
"code": "FA",
}
)
group_tfa = self.env["account.asset.group"].create(
{
"name": "Tangible Fixed Assets",
"code": "TFA",
}
)
# Groups are displayed by code (if any) plus name
self.assertEqual(
self.env["account.asset.group"].name_search("FA"),
[(group_fa.id, "FA Fixed Assets")],
)
# Groups with code are shown by code in list views
self.assertEqual(
self.env["account.asset.group"]
.with_context(params={"view_type": "list"})
.name_search("FA"),
[(group_fa.id, "FA")],
)
self.assertEqual(
self.env["account.asset.group"].name_search("TFA"),
[(group_tfa.id, "TFA Tangible Fixed Assets")],
)
group_tfa.code = False
group_fa.code = False
self.assertEqual(group_fa.name_get(), [(group_fa.id, "Fixed Assets")])
# Groups without code are shown by truncated name in lists
self.assertEqual(
group_tfa.name_get(), [(group_tfa.id, "Tangible Fixed Assets")]
)
self.assertEqual(
group_tfa.with_context(params={"view_type": "list"}).name_get(),
[(group_tfa.id, "Tangible Fixed A...")],
)
self.assertFalse(self.env["account.asset.group"].name_search("stessA dexiF"))
def test_16_use_number_of_depreciations(self):
# When you run a depreciation with method = 'number'
profile = self.car5y
profile.method_time = "number"
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": profile.id,
"purchase_value": 10000,
"salvage_value": 0,
"date_start": time.strftime("2019-01-01"),
"method_time": "year",
"method_number": 5,
"method_period": "month",
"prorata": False,
"days_calc": False,
"use_leap_years": False,
}
)
asset.compute_depreciation_board()
asset.invalidate_recordset()
for _i in range(1, 11):
self.assertAlmostEqual(
asset.depreciation_line_ids[1].amount, 166.67, places=2
)
# In the last month of the fiscal year we compensate for the small
# deviations if that is necessary.
self.assertAlmostEqual(asset.depreciation_line_ids[12].amount, 166.63, places=2)
def test_17_carry_forward_missed_depreciations(self):
"""Asset with accumulate missed depreciations."""
asset_profile = self.car5y
# Create an asset with carry_forward_missed_depreciations
# Theoretically, the depreciation would be 5000 / 12 months
# which is 416.67 per month
asset = self.asset_model.create(
{
"name": "test asset",
"profile_id": asset_profile.id,
"purchase_value": 5000,
"salvage_value": 0,
"date_start": time.strftime("2021-01-01"),
"method_time": "year",
"method_number": 1,
"method_period": "month",
"carry_forward_missed_depreciations": True,
}
)
# Set the fiscalyear lock date for the company
self.company_data["company"].fiscalyear_lock_date = time.strftime("2021-05-31")
# Compute the depreciation board
asset.compute_depreciation_board()
asset.invalidate_recordset()
d_lines = asset.depreciation_line_ids
init_lines = d_lines[1:6]
# Jan to May entries are before the lock date -> marked as init
self.assertTrue(init_lines.mapped("init_entry"))
# Depreciation amount for these lines is set to 0
for line in init_lines:
self.assertEqual(line.amount, 0.0)
# The amount to be carried is 416.67 * 5 = 2083.35
# This amount is accumulated in the first depreciation for the current
# available period -> 416.67 + 2083.35 = 2500.02
self.assertAlmostEqual(d_lines[6].amount, 2500.02, places=2)
# The rest of the lines should have the corresponding amount of 416.67
# just as usual
for _i in range(7, 12):
self.assertAlmostEqual(d_lines[_i].amount, 416.67, places=2)
# In the last month the small deviations are compensated
self.assertAlmostEqual(d_lines[12].amount, 416.63, places=2)
def test_18_reverse_entries(self):
"""Test that cancelling a posted entry creates a reversal."""
ict0 = self.asset_model.create(
{
"state": "draft",
"method_time": "year",
"method_number": 3,
"method_period": "year",
"name": "Laptop",
"code": "PI00101",
"purchase_value": 1500.0,
"profile_id": self.ict3Y.id,
"date_start": time.strftime("%Y-01-01"),
}
)
ict0.profile_id.allow_reversal = True
# compute the depreciation boards
ict0.compute_depreciation_board()
ict0.invalidate_recordset()
# post the first depreciation line
ict0.validate()
ict0.depreciation_line_ids[1].create_move()
original_move = ict0.depreciation_line_ids[1].move_id
ict0.invalidate_recordset()
self.assertEqual(ict0.state, "open")
self.assertEqual(ict0.value_depreciated, 500)
self.assertEqual(ict0.value_residual, 1000)
depreciation_line = ict0.depreciation_line_ids[1]
wiz_res = depreciation_line.unlink_move()
self.assertTrue(
"res_model" in wiz_res and wiz_res["res_model"] == "wiz.asset.move.reverse"
)
wiz = Form(
self.env["wiz.asset.move.reverse"].with_context(
**{
"active_model": depreciation_line._name,
"active_id": depreciation_line.id,
"active_ids": [depreciation_line.id],
}
)
)
reverse_wizard = wiz.save()
reverse_wizard.write({"journal_id": depreciation_line.move_id.journal_id.id})
reverse_wizard.reverse_move()
ict0.invalidate_recordset()
self.assertEqual(ict0.value_depreciated, 0)
self.assertEqual(ict0.value_residual, 1500)
self.assertEqual(len(original_move.reversal_move_id), 1)
def test_19_unlink_entries(self):
"""Test that cancelling a posted entry creates a reversal, if the
journal entry has the inalterability hash."""
ict0 = self.asset_model.create(
{
"state": "draft",
"method_time": "year",
"method_number": 3,
"method_period": "year",
"name": "Laptop",
"code": "PI00101",
"purchase_value": 1500.0,
"profile_id": self.ict3Y.id,
"date_start": time.strftime("%Y-01-01"),
}
)
# compute the depreciation boards
ict0.compute_depreciation_board()
ict0.invalidate_recordset()
# post the first depreciation line
ict0.validate()
ict0.depreciation_line_ids[1].create_move()
original_move_id = ict0.depreciation_line_ids[1].move_id.id
ict0.invalidate_recordset()
self.assertEqual(ict0.state, "open")
self.assertEqual(ict0.value_depreciated, 500)
self.assertEqual(ict0.value_residual, 1000)
ict0.depreciation_line_ids[1].unlink_move()
ict0.invalidate_recordset()
self.assertEqual(ict0.value_depreciated, 0)
self.assertEqual(ict0.value_residual, 1500)
move = self.env["account.move"].search([("id", "=", original_move_id)])
self.assertFalse(move)
def test_20_asset_removal_with_value_residual(self):
"""Asset removal with value residual"""
asset = self.asset_model.create(
{
"name": "test asset removal",
"profile_id": self.car5y.id,
"purchase_value": 1000,
"salvage_value": 0,
"date_start": "2019-01-01",
"method_time": "number",
"method_number": 10,
"method_period": "month",
"prorata": False,
}
)
asset.compute_depreciation_board()
asset.validate()
lines = asset.depreciation_line_ids.filtered(lambda x: not x.init_entry)
self.assertEqual(len(lines), 10)
last_line = lines[-1]
last_line["amount"] = last_line["amount"] - 0.10
for asset_line in lines:
asset_line.create_move()
self.assertEqual(asset.value_residual, 0.10)
asset.compute_depreciation_board()
lines = asset.depreciation_line_ids.filtered(lambda x: not x.init_entry)
self.assertEqual(len(lines), 11)
last_line = lines[-1]
self.assertEqual(last_line.amount, 0.10)
last_line.create_move()
self.assertEqual(asset.value_residual, 0)
self.assertEqual(asset.state, "close")

Some files were not shown because too many files have changed in this diff Show More