init
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Assets Management',
|
||||
'description': """
|
||||
Assets management
|
||||
=================
|
||||
Manage assets owned by a company or a person.
|
||||
Keeps track of depreciations, and creates corresponding journal entries.
|
||||
|
||||
""",
|
||||
'category': 'Accounting/Accounting',
|
||||
'sequence': 32,
|
||||
'depends': ['account_reports'],
|
||||
'data': [
|
||||
'security/account_asset_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/asset_modify_views.xml',
|
||||
'views/account_account_views.xml',
|
||||
'views/account_asset_views.xml',
|
||||
'views/account_move_views.xml',
|
||||
'data/assets_report.xml',
|
||||
'data/account_report_actions.xml',
|
||||
'data/menuitems.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/account_asset_demo.xml',
|
||||
],
|
||||
'license': 'OEEL-1',
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'account_reports.assets_financial_report': [
|
||||
'account_asset/static/src/scss/account_asset.scss',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'account_asset/static/src/scss/account_asset.scss',
|
||||
'account_asset/static/src/components/**/*'
|
||||
],
|
||||
'web.assets_web_dark': [
|
||||
'account_asset/static/src/scss/*.dark.scss',
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_account_report_assets" model="ir.actions.client">
|
||||
<field name="name">Depreciation Schedule</field>
|
||||
<field name="tag">account_report</field>
|
||||
<field name="context" eval="{'report_id': ref('account_asset.assets_report')}"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="assets_report" model="account.report">
|
||||
<field name="name">Depreciation Schedule</field>
|
||||
<field name="filter_hierarchy">optional</field>
|
||||
<field name="filter_unfold_all" eval="True"/>
|
||||
<field name="filter_period_comparison" eval="False"/>
|
||||
<field name="custom_handler_model_id" ref="model_account_asset_report_handler"/>
|
||||
<field name="load_more_limit" eval="80"/>
|
||||
<field name="column_ids">
|
||||
<record id="assets_report_acquisition_date" model="account.report.column">
|
||||
<field name="name">Acquisition Date</field>
|
||||
<field name="expression_label">acquisition_date</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="assets_report_first_depreciation" model="account.report.column">
|
||||
<field name="name">First Depreciation</field>
|
||||
<field name="expression_label">first_depreciation</field>
|
||||
<field name="figure_type">date</field>
|
||||
</record>
|
||||
<record id="assets_report_first_method" model="account.report.column">
|
||||
<field name="name">Method</field>
|
||||
<field name="expression_label">method</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="assets_report_duration_rate" model="account.report.column">
|
||||
<field name="name">Duration / Rate</field>
|
||||
<field name="expression_label">duration_rate</field>
|
||||
<field name="figure_type">string</field>
|
||||
</record>
|
||||
<record id="assets_report_date_from" model="account.report.column">
|
||||
<field name="name">date from</field>
|
||||
<field name="expression_label">assets_date_from</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_plus" model="account.report.column">
|
||||
<field name="name">+</field>
|
||||
<field name="expression_label">assets_plus</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_minus" model="account.report.column">
|
||||
<field name="name">-</field>
|
||||
<field name="expression_label">assets_minus</field>
|
||||
</record>
|
||||
<record id="assets_report_assets_date_to" model="account.report.column">
|
||||
<field name="name">date to</field>
|
||||
<field name="expression_label">assets_date_to</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_date_from" model="account.report.column">
|
||||
<field name="name">date from</field>
|
||||
<field name="expression_label">depre_date_from</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_plus" model="account.report.column">
|
||||
<field name="name">+</field>
|
||||
<field name="expression_label">depre_plus</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_minus" model="account.report.column">
|
||||
<field name="name">-</field>
|
||||
<field name="expression_label">depre_minus</field>
|
||||
</record>
|
||||
<record id="assets_report_depre_date_to" model="account.report.column">
|
||||
<field name="name">date to</field>
|
||||
<field name="expression_label">depre_date_to</field>
|
||||
</record>
|
||||
<record id="assets_report_balance" model="account.report.column">
|
||||
<field name="name">book_value</field>
|
||||
<field name="expression_label">balance</field>
|
||||
</record>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_action_account_report_assets"
|
||||
name="Depreciation Schedule"
|
||||
action="action_account_report_assets"
|
||||
parent="account.account_reports_management_menu"
|
||||
groups="account.group_account_readonly"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="account_asset_model_demo" model="account.asset">
|
||||
<field name="name">Asset - 5 Years</field>
|
||||
<field name="prorata_computation_type">none</field>
|
||||
<field name="original_value">1000</field>
|
||||
<field name="journal_id" model="account.journal" search="[
|
||||
('type', '=', 'general'),
|
||||
('id', '!=', obj().env.user.company_id.currency_exchange_journal_id.id)]"/>
|
||||
<field name="account_asset_id" model="account.account" search="[
|
||||
('account_type', '=', 'asset_fixed'),
|
||||
('company_id', '=', ref('base.main_company'))]"/>
|
||||
<field name="account_depreciation_id" model="account.account" search="[
|
||||
('account_type', '=', 'asset_fixed'),
|
||||
('company_id', '=', ref('base.main_company'))]"/>
|
||||
<field name="account_depreciation_expense_id" model="account.account" search="[
|
||||
('account_type', '=', 'expense'),
|
||||
('tag_ids', 'in', [ref('account.account_tag_operating')]),
|
||||
('company_id', '=', ref('base.main_company'))]"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
</data>
|
||||
</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
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account
|
||||
from . import account_asset
|
||||
from . import account_assets_report
|
||||
from . import account_move
|
||||
from . import res_company
|
||||
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class AccountAccount(models.Model):
|
||||
_inherit = 'account.account'
|
||||
|
||||
asset_model = fields.Many2one(
|
||||
'account.asset',
|
||||
domain=[('state', '=', 'model')],
|
||||
help="If this is selected, an expense/revenue will be created automatically "
|
||||
"when Journal Items on this account are posted.",
|
||||
tracking=True)
|
||||
create_asset = fields.Selection([('no', 'No'), ('draft', 'Create in draft'), ('validate', 'Create and validate')],
|
||||
required=True, default='no', tracking=True)
|
||||
# specify if the account can generate asset depending on it's type. It is used in the account form view
|
||||
can_create_asset = fields.Boolean(compute="_compute_can_create_asset")
|
||||
form_view_ref = fields.Char(compute='_compute_can_create_asset')
|
||||
# decimal quantities are not supported, quantities are rounded to the lower int
|
||||
multiple_assets_per_line = fields.Boolean(string='Multiple Assets per Line', default=False, tracking=True,
|
||||
help="Multiple asset items will be generated depending on the bill line quantity instead of 1 global asset.")
|
||||
|
||||
@api.depends('account_type')
|
||||
def _compute_can_create_asset(self):
|
||||
for record in self:
|
||||
record.can_create_asset = record.account_type in ('asset_fixed', 'asset_non_current')
|
||||
record.form_view_ref = 'account_asset.view_account_asset_form'
|
||||
|
||||
@api.onchange('create_asset')
|
||||
def _onchange_multiple_assets_per_line(self):
|
||||
for record in self:
|
||||
if record.create_asset == 'no':
|
||||
record.multiple_assets_per_line = False
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,449 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.tools import format_date
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
MAX_NAME_LENGTH = 50
|
||||
|
||||
|
||||
class AssetsReportCustomHandler(models.AbstractModel):
|
||||
_name = 'account.asset.report.handler'
|
||||
_inherit = 'account.report.custom.handler'
|
||||
_description = 'Assets Report Custom Handler'
|
||||
|
||||
def _get_custom_display_config(self):
|
||||
return {
|
||||
'client_css_custom_class': 'depreciation_schedule',
|
||||
'templates': {
|
||||
'AccountReportFilters': 'account_asset.DepreciationScheduleFilters',
|
||||
}
|
||||
}
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
report = self._with_context_company2code2account(report)
|
||||
|
||||
lines, totals_by_column_group = self._generate_report_lines_without_grouping(report, options)
|
||||
# add the groups by account
|
||||
if options['assets_groupby_account']:
|
||||
lines = self._group_by_account(report, lines, options)
|
||||
else:
|
||||
lines = report._regroup_lines_by_name_prefix(options, lines, '_report_expand_unfoldable_line_assets_report_prefix_group', 0)
|
||||
|
||||
# add the total line
|
||||
total_columns = []
|
||||
for column_data in options['columns']:
|
||||
col_value = totals_by_column_group[column_data['column_group_key']].get(column_data['expression_label'])
|
||||
col_value = col_value if column_data.get('figure_type') == 'monetary' else ''
|
||||
|
||||
total_columns.append(report._build_column_dict(col_value, column_data, options=options))
|
||||
|
||||
if lines:
|
||||
lines.append({
|
||||
'id': report._get_generic_line_id(None, None, markup='total'),
|
||||
'level': 1,
|
||||
'name': _('Total'),
|
||||
'columns': total_columns,
|
||||
'unfoldable': False,
|
||||
'unfolded': False,
|
||||
})
|
||||
|
||||
return [(0, line) for line in lines]
|
||||
|
||||
def _generate_report_lines_without_grouping(self, report, options, prefix_to_match=None, parent_id=None, forced_account_id=None):
|
||||
# construct a dictionary:
|
||||
# {(account_id, asset_id): {col_group_key: {expression_label_1: value, expression_label_2: value, ...}}}
|
||||
all_asset_ids = set()
|
||||
all_lines_data = {}
|
||||
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
|
||||
# the lines returned are already sorted by account_id!
|
||||
lines_query_results = self._query_lines(column_group_options, prefix_to_match=prefix_to_match, forced_account_id=forced_account_id)
|
||||
for account_id, asset_id, cols_by_expr_label in lines_query_results:
|
||||
line_id = (account_id, asset_id)
|
||||
all_asset_ids.add(asset_id)
|
||||
if line_id not in all_lines_data:
|
||||
all_lines_data[line_id] = {column_group_key: []}
|
||||
all_lines_data[line_id][column_group_key] = cols_by_expr_label
|
||||
|
||||
column_names = [
|
||||
'assets_date_from', 'assets_plus', 'assets_minus', 'assets_date_to', 'depre_date_from',
|
||||
'depre_plus', 'depre_minus', 'depre_date_to', 'balance'
|
||||
]
|
||||
totals_by_column_group = defaultdict(lambda: dict.fromkeys(column_names, 0.0))
|
||||
|
||||
# Browse all the necessary assets in one go, to minimize the number of queries
|
||||
assets_cache = {asset.id: asset for asset in self.env['account.asset'].browse(all_asset_ids)}
|
||||
|
||||
# construct the lines, 1 at a time
|
||||
lines = []
|
||||
company_currency = self.env.company.currency_id
|
||||
for (account_id, asset_id), col_group_totals in all_lines_data.items():
|
||||
all_columns = []
|
||||
for column_data in options['columns']:
|
||||
col_group_key = column_data['column_group_key']
|
||||
expr_label = column_data['expression_label']
|
||||
if col_group_key not in col_group_totals or expr_label not in col_group_totals[col_group_key]:
|
||||
all_columns.append(report._build_column_dict(None, None))
|
||||
continue
|
||||
|
||||
col_value = col_group_totals[col_group_key][expr_label]
|
||||
col_data = None if col_value is None else column_data
|
||||
|
||||
all_columns.append(report._build_column_dict(col_value, col_data, options=options))
|
||||
|
||||
# add to the total line
|
||||
if column_data['figure_type'] == 'monetary':
|
||||
totals_by_column_group[column_data['column_group_key']][column_data['expression_label']] += col_value
|
||||
|
||||
name = assets_cache[asset_id].name
|
||||
line = {
|
||||
'id': report._get_generic_line_id('account.asset', asset_id, parent_line_id=parent_id),
|
||||
'level': 2,
|
||||
'name': name,
|
||||
'columns': all_columns,
|
||||
'unfoldable': False,
|
||||
'unfolded': False,
|
||||
'caret_options': 'account_asset_line',
|
||||
'assets_account_id': account_id,
|
||||
}
|
||||
if parent_id:
|
||||
line['parent_id'] = parent_id
|
||||
if len(name) >= MAX_NAME_LENGTH:
|
||||
line['title_hover'] = name
|
||||
lines.append(line)
|
||||
|
||||
return lines, totals_by_column_group
|
||||
|
||||
def _caret_options_initializer(self):
|
||||
# Use 'caret_option_open_record_form' defined in account_reports rather than a custom function
|
||||
return {
|
||||
'account_asset_line': [
|
||||
{'name': _("Open Asset"), 'action': 'caret_option_open_record_form'},
|
||||
]
|
||||
}
|
||||
|
||||
def _custom_options_initializer(self, report, options, previous_options=None):
|
||||
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
||||
column_group_options_map = report._split_options_per_column_group(options)
|
||||
|
||||
for col in options['columns']:
|
||||
column_group_options = column_group_options_map[col['column_group_key']]
|
||||
# Dynamic naming of columns containing dates
|
||||
if col['expression_label'] == 'balance':
|
||||
col['name'] = '' # The column label will be displayed in the subheader
|
||||
if col['expression_label'] in ['assets_date_from', 'depre_date_from']:
|
||||
col['name'] = format_date(self.env, column_group_options['date']['date_from'])
|
||||
elif col['expression_label'] in ['assets_date_to', 'depre_date_to']:
|
||||
col['name'] = format_date(self.env, column_group_options['date']['date_to'])
|
||||
|
||||
options['custom_columns_subheaders'] = [
|
||||
{"name": _("Characteristics"), "colspan": 4},
|
||||
{"name": _("Assets"), "colspan": 4},
|
||||
{"name": _("Depreciation"), "colspan": 4},
|
||||
{"name": _("Book Value"), "colspan": 1}
|
||||
]
|
||||
|
||||
# Group by account by default
|
||||
groupby_activated = (previous_options or {}).get('assets_groupby_account', True)
|
||||
options['assets_groupby_account'] = groupby_activated
|
||||
# If group by account is activated, activate the hierarchy (which will group by account group as well) if
|
||||
# the company has at least one account group, otherwise only group by account
|
||||
has_account_group = self.env['account.group'].search_count([('company_id', '=', self.env.company.id)], limit=1)
|
||||
hierarchy_activated = (previous_options or {}).get('hierarchy', True)
|
||||
options['hierarchy'] = has_account_group and hierarchy_activated or False
|
||||
|
||||
def _with_context_company2code2account(self, report):
|
||||
if self.env.context.get('company2code2account') is not None:
|
||||
return report
|
||||
|
||||
company2code2account = defaultdict(dict)
|
||||
for account in self.env['account.account'].search([]):
|
||||
company2code2account[account.company_id.id][account.code] = account
|
||||
|
||||
return report.with_context(company2code2account=company2code2account)
|
||||
|
||||
def _query_lines(self, options, prefix_to_match=None, forced_account_id=None):
|
||||
"""
|
||||
Returns a list of tuples: [(asset_id, account_id, [{expression_label: value}])]
|
||||
"""
|
||||
lines = []
|
||||
asset_lines = self._query_values(options, prefix_to_match=prefix_to_match, forced_account_id=forced_account_id)
|
||||
|
||||
# Assign the gross increases sub assets to their main asset (parent)
|
||||
parent_lines = []
|
||||
children_lines = defaultdict(list)
|
||||
for al in asset_lines:
|
||||
if al['parent_id']:
|
||||
children_lines[al['parent_id']] += [al]
|
||||
else:
|
||||
parent_lines += [al]
|
||||
|
||||
for al in parent_lines:
|
||||
# Compute the depreciation rate string
|
||||
if al['asset_method'] == 'linear' and al['asset_method_number']: # some assets might have 0 depreciations because they dont lose value
|
||||
total_months = int(al['asset_method_number']) * int(al['asset_method_period'])
|
||||
months = total_months % 12
|
||||
years = total_months // 12
|
||||
asset_depreciation_rate = " ".join(part for part in [
|
||||
years and _("%s y", years),
|
||||
months and _("%s m", months),
|
||||
] if part)
|
||||
elif al['asset_method'] == 'linear':
|
||||
asset_depreciation_rate = '0.00 %'
|
||||
else:
|
||||
asset_depreciation_rate = ('{:.2f} %').format(float(al['asset_method_progress_factor']) * 100)
|
||||
|
||||
# Manage the opening of the asset
|
||||
opening = (al['asset_acquisition_date'] or al['asset_date']) < fields.Date.to_date(options['date']['date_from'])
|
||||
|
||||
# Get the main values of the board for the asset
|
||||
depreciation_opening = al['depreciated_before']
|
||||
depreciation_add = al['depreciated_during']
|
||||
depreciation_minus = 0.0
|
||||
|
||||
asset_disposal_value = al['asset_disposal_value'] if al['asset_disposal_date'] and al['asset_disposal_date'] <= fields.Date.to_date(options['date']['date_to']) else 0.0
|
||||
|
||||
asset_opening = al['asset_original_value'] if opening else 0.0
|
||||
asset_add = 0.0 if opening else al['asset_original_value']
|
||||
asset_minus = 0.0
|
||||
asset_salvage_value = al.get('asset_salvage_value', 0.0)
|
||||
|
||||
# Add the main values of the board for all the sub assets (gross increases)
|
||||
for child in children_lines[al['asset_id']]:
|
||||
depreciation_opening += child['depreciated_before']
|
||||
depreciation_add += child['depreciated_during']
|
||||
|
||||
opening = (child['asset_acquisition_date'] or child['asset_date']) < fields.Date.to_date(options['date']['date_from'])
|
||||
asset_opening += child['asset_original_value'] if opening else 0.0
|
||||
asset_add += 0.0 if opening else child['asset_original_value']
|
||||
|
||||
# Compute the closing values
|
||||
asset_closing = asset_opening + asset_add - asset_minus
|
||||
depreciation_closing = depreciation_opening + depreciation_add - depreciation_minus
|
||||
al_currency = self.env['res.currency'].browse(al['asset_currency_id'])
|
||||
|
||||
# Manage the closing of the asset
|
||||
if (
|
||||
al['asset_state'] == 'close'
|
||||
and al['asset_disposal_date']
|
||||
and al['asset_disposal_date'] <= fields.Date.to_date(options['date']['date_to'])
|
||||
and al_currency.compare_amounts(depreciation_closing, asset_closing - asset_salvage_value) == 0
|
||||
):
|
||||
depreciation_add -= asset_disposal_value
|
||||
depreciation_minus += depreciation_closing - asset_disposal_value
|
||||
depreciation_closing = 0.0
|
||||
asset_minus += asset_closing
|
||||
asset_closing = 0.0
|
||||
|
||||
# Manage negative assets (credit notes)
|
||||
if al['asset_original_value'] < 0:
|
||||
asset_add, asset_minus = -asset_minus, -asset_add
|
||||
depreciation_add, depreciation_minus = -depreciation_minus, -depreciation_add
|
||||
|
||||
# Format the data
|
||||
columns_by_expr_label = {
|
||||
"acquisition_date": al["asset_acquisition_date"] and format_date(self.env, al["asset_acquisition_date"]) or "", # Characteristics
|
||||
"first_depreciation": al["asset_date"] and format_date(self.env, al["asset_date"]) or "",
|
||||
"method": (al["asset_method"] == "linear" and _("Linear")) or (al["asset_method"] == "degressive" and _("Declining")) or _("Dec. then Straight"),
|
||||
"duration_rate": asset_depreciation_rate,
|
||||
"assets_date_from": asset_opening,
|
||||
"assets_plus": asset_add,
|
||||
"assets_minus": asset_minus,
|
||||
"assets_date_to": asset_closing,
|
||||
"depre_date_from": depreciation_opening,
|
||||
"depre_plus": depreciation_add,
|
||||
"depre_minus": depreciation_minus,
|
||||
"depre_date_to": depreciation_closing,
|
||||
"balance": asset_closing - depreciation_closing,
|
||||
}
|
||||
|
||||
lines.append((al['account_id'], al['asset_id'], columns_by_expr_label))
|
||||
return lines
|
||||
|
||||
def _group_by_account(self, report, lines, options):
|
||||
"""
|
||||
This function adds the grouping lines on top of each group of account.asset
|
||||
It iterates over the lines, change the line_id of each line to include the account.account.id and the
|
||||
account.asset.id.
|
||||
"""
|
||||
if not lines:
|
||||
return lines
|
||||
|
||||
line_vals_per_account_id = {}
|
||||
for line in lines:
|
||||
parent_account_id = line.get('assets_account_id')
|
||||
|
||||
model, res_id = report._get_model_info_from_id(line['id'])
|
||||
assert model == 'account.asset'
|
||||
|
||||
# replace the line['id'] to add the account.account.id
|
||||
line['id'] = report._build_line_id([
|
||||
(None, 'account.account', parent_account_id),
|
||||
(None, 'account.asset', res_id)
|
||||
])
|
||||
|
||||
line_vals_per_account_id.setdefault(parent_account_id, {
|
||||
# We don't assign a name to the line yet, so that we can batch the browsing of account.account objects
|
||||
'id': report._build_line_id([(None, 'account.account', parent_account_id)]),
|
||||
'columns': [], # Filled later
|
||||
'unfoldable': True,
|
||||
'unfolded': options.get('unfold_all', False),
|
||||
'level': 1,
|
||||
|
||||
# This value is stored here for convenience; it will be removed from the result
|
||||
'group_lines': [],
|
||||
})['group_lines'].append(line)
|
||||
|
||||
# Generate the result
|
||||
idx_monetary_columns = [idx_col for idx_col, col in enumerate(options['columns']) if col['figure_type'] == 'monetary']
|
||||
accounts = self.env['account.account'].browse(line_vals_per_account_id.keys())
|
||||
rslt_lines = []
|
||||
for account in accounts:
|
||||
account_line_vals = line_vals_per_account_id[account.id]
|
||||
account_line_vals['name'] = f"{account.code} {account.name}"
|
||||
|
||||
rslt_lines.append(account_line_vals)
|
||||
|
||||
group_totals = {column_index: 0 for column_index in idx_monetary_columns}
|
||||
group_lines = report._regroup_lines_by_name_prefix(
|
||||
options,
|
||||
account_line_vals.pop('group_lines'),
|
||||
'_report_expand_unfoldable_line_assets_report_prefix_group',
|
||||
account_line_vals['level'],
|
||||
parent_line_dict_id=account_line_vals['id'],
|
||||
)
|
||||
|
||||
for account_subline in group_lines:
|
||||
# Add this line to the group totals
|
||||
for column_index in idx_monetary_columns:
|
||||
group_totals[column_index] += account_subline['columns'][column_index].get('no_format', 0)
|
||||
|
||||
# Setup the parent and add the line to the result
|
||||
account_subline['parent_id'] = account_line_vals['id']
|
||||
rslt_lines.append(account_subline)
|
||||
|
||||
# Add totals (columns) to the account line
|
||||
for column_index in range(len(options['columns'])):
|
||||
account_line_vals['columns'].append(report._build_column_dict(
|
||||
group_totals.get(column_index, ''),
|
||||
options['columns'][column_index],
|
||||
options=options,
|
||||
))
|
||||
|
||||
return rslt_lines
|
||||
|
||||
def _query_values(self, options, prefix_to_match=None, forced_account_id=None):
|
||||
"Get the data from the database"
|
||||
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
self.env['account.asset'].check_access_rights('read')
|
||||
|
||||
move_filter = f"""move.state {"!= 'cancel'" if options.get('all_entries') else "= 'posted'"}"""
|
||||
|
||||
query_params = {
|
||||
'date_to': options['date']['date_to'],
|
||||
'date_from': options['date']['date_from'],
|
||||
'company_ids': tuple(self.env['account.report'].get_report_company_ids(options)),
|
||||
'include_draft': options.get('all_entries', False),
|
||||
}
|
||||
|
||||
prefix_query = ''
|
||||
if prefix_to_match:
|
||||
prefix_query = "AND asset.name ILIKE %(prefix_to_match)s"
|
||||
query_params['prefix_to_match'] = f"{prefix_to_match}%"
|
||||
|
||||
account_query = ''
|
||||
if forced_account_id:
|
||||
account_query = "AND account.id = %(forced_account_id)s"
|
||||
query_params['forced_account_id'] = forced_account_id
|
||||
|
||||
analytical_query = ''
|
||||
analytic_account_ids = []
|
||||
if options.get('analytic_accounts') and not any(x in options.get('analytic_accounts_list', []) for x in options['analytic_accounts']):
|
||||
analytic_account_ids += [[str(account_id) for account_id in options['analytic_accounts']]]
|
||||
if options.get('analytic_accounts_list'):
|
||||
analytic_account_ids += [[str(account_id) for account_id in options.get('analytic_accounts_list')]]
|
||||
if analytic_account_ids:
|
||||
analytical_query = 'AND asset.analytic_distribution ?| array[%(analytic_account_ids)s]'
|
||||
query_params['analytic_account_ids'] = analytic_account_ids
|
||||
|
||||
sql = f"""
|
||||
SELECT asset.id AS asset_id,
|
||||
asset.parent_id AS parent_id,
|
||||
asset.name AS asset_name,
|
||||
asset.original_value AS asset_original_value,
|
||||
asset.currency_id AS asset_currency_id,
|
||||
COALESCE(asset.salvage_value, 0) as asset_salvage_value,
|
||||
MIN(move.date) AS asset_date,
|
||||
asset.disposal_date AS asset_disposal_date,
|
||||
asset.acquisition_date AS asset_acquisition_date,
|
||||
asset.method AS asset_method,
|
||||
asset.method_number AS asset_method_number,
|
||||
asset.method_period AS asset_method_period,
|
||||
asset.method_progress_factor AS asset_method_progress_factor,
|
||||
asset.state AS asset_state,
|
||||
asset.company_id AS company_id,
|
||||
account.code AS account_code,
|
||||
account.name AS account_name,
|
||||
account.id AS account_id,
|
||||
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date < %(date_from)s AND {move_filter}), 0) + COALESCE(asset.already_depreciated_amount_import, 0) AS depreciated_before,
|
||||
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s AND {move_filter}), 0) AS depreciated_during,
|
||||
COALESCE(SUM(move.depreciation_value) FILTER (WHERE move.date BETWEEN %(date_from)s AND %(date_to)s AND {move_filter} AND move.asset_number_days IS NULL), 0) AS asset_disposal_value
|
||||
FROM account_asset AS asset
|
||||
LEFT JOIN account_account AS account ON asset.account_asset_id = account.id
|
||||
LEFT JOIN account_move move ON move.asset_id = asset.id
|
||||
LEFT JOIN account_move reversal ON reversal.reversed_entry_id = move.id
|
||||
WHERE asset.company_id in %(company_ids)s
|
||||
AND (asset.acquisition_date <= %(date_to)s OR move.date <= %(date_to)s)
|
||||
AND (asset.disposal_date >= %(date_from)s OR asset.disposal_date IS NULL)
|
||||
AND (asset.state not in ('model', 'draft', 'cancelled') OR (asset.state = 'draft' AND %(include_draft)s))
|
||||
AND asset.active = 't'
|
||||
AND reversal.id IS NULL
|
||||
{prefix_query}
|
||||
{account_query}
|
||||
{analytical_query}
|
||||
GROUP BY asset.id, account.id
|
||||
ORDER BY account.code, asset.acquisition_date;
|
||||
"""
|
||||
|
||||
self._cr.execute(sql, query_params)
|
||||
results = self._cr.dictfetchall()
|
||||
return results
|
||||
|
||||
def _report_expand_unfoldable_line_assets_report_prefix_group(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
|
||||
matched_prefix = self.env['account.report']._get_prefix_groups_matched_prefix_from_line_id(line_dict_id)
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
|
||||
lines, _totals_by_column_group = self._generate_report_lines_without_grouping(
|
||||
report,
|
||||
options,
|
||||
prefix_to_match=matched_prefix,
|
||||
parent_id=line_dict_id,
|
||||
forced_account_id=self.env['account.report']._get_res_id_from_line_id(line_dict_id, 'account.account'),
|
||||
)
|
||||
|
||||
lines = report._regroup_lines_by_name_prefix(
|
||||
options,
|
||||
lines,
|
||||
'_report_expand_unfoldable_line_assets_report_prefix_group',
|
||||
len(matched_prefix),
|
||||
matched_prefix=matched_prefix,
|
||||
parent_line_dict_id=line_dict_id,
|
||||
)
|
||||
|
||||
return {
|
||||
'lines': lines,
|
||||
'offset_increment': len(lines),
|
||||
'has_more': False,
|
||||
}
|
||||
|
||||
|
||||
class AssetsReport(models.Model):
|
||||
_inherit = 'account.report'
|
||||
|
||||
def _get_caret_option_view_map(self):
|
||||
view_map = super()._get_caret_option_view_map()
|
||||
view_map['account.asset.line'] = 'account_asset.view_account_asset_expense_form'
|
||||
return view_map
|
||||
@@ -0,0 +1,361 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _, _lt, Command
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools.misc import formatLang
|
||||
from collections import defaultdict, namedtuple
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
asset_id = fields.Many2one('account.asset', string='Asset', index=True, ondelete='cascade', copy=False, domain="[('company_id', '=', company_id)]")
|
||||
asset_remaining_value = fields.Monetary(string='Depreciable Value', compute='_compute_depreciation_cumulative_value')
|
||||
asset_depreciated_value = fields.Monetary(string='Cumulative Depreciation', compute='_compute_depreciation_cumulative_value')
|
||||
# true when this move is the result of the changing of value of an asset
|
||||
asset_value_change = fields.Boolean()
|
||||
# how many days of depreciation this entry corresponds to
|
||||
asset_number_days = fields.Integer(string="Number of days", copy=False)
|
||||
asset_depreciation_beginning_date = fields.Date(string="Date of the beginning of the depreciation", copy=False) # technical field stating when the depreciation associated with this entry has begun
|
||||
depreciation_value = fields.Monetary(
|
||||
string="Depreciation",
|
||||
compute="_compute_depreciation_value", inverse="_inverse_depreciation_value", store=True,
|
||||
)
|
||||
|
||||
asset_ids = fields.One2many('account.asset', string='Assets', compute="_compute_asset_ids")
|
||||
asset_id_display_name = fields.Char(compute="_compute_asset_ids") # just a button label. That's to avoid a plethora of different buttons defined in xml
|
||||
count_asset = fields.Integer(compute="_compute_asset_ids")
|
||||
draft_asset_exists = fields.Boolean(compute="_compute_asset_ids")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@api.depends('asset_id', 'depreciation_value', 'asset_id.total_depreciable_value', 'asset_id.already_depreciated_amount_import')
|
||||
def _compute_depreciation_cumulative_value(self):
|
||||
self.asset_depreciated_value = 0
|
||||
self.asset_remaining_value = 0
|
||||
|
||||
# make sure to protect all the records being assigned, because the
|
||||
# assignments invoke method write() on non-protected records, which may
|
||||
# cause an infinite recursion in case method write() needs to read one
|
||||
# of these fields (like in case of a base automation)
|
||||
fields = [type(self).asset_remaining_value, type(self).asset_depreciated_value]
|
||||
with self.env.protecting(fields, self.asset_id.depreciation_move_ids):
|
||||
for asset in self.asset_id:
|
||||
depreciated = 0
|
||||
remaining = asset.total_depreciable_value - asset.already_depreciated_amount_import
|
||||
for move in asset.depreciation_move_ids.sorted(lambda mv: (mv.date, mv._origin.id)):
|
||||
remaining -= move.depreciation_value
|
||||
depreciated += move.depreciation_value
|
||||
move.asset_remaining_value = remaining
|
||||
move.asset_depreciated_value = depreciated
|
||||
|
||||
@api.depends('line_ids.balance')
|
||||
def _compute_depreciation_value(self):
|
||||
for move in self:
|
||||
asset = move.asset_id or move.reversed_entry_id.asset_id # reversed moves are created before being assigned to the asset
|
||||
if asset:
|
||||
account_internal_group = 'expense'
|
||||
asset_depreciation = sum(
|
||||
move.line_ids.filtered(lambda l: l.account_id.internal_group == account_internal_group or l.account_id == asset.account_depreciation_expense_id).mapped('balance')
|
||||
)
|
||||
# Special case of closing entry - only disposed assets of type 'purchase' should match this condition
|
||||
if any(
|
||||
line.account_id == asset.account_asset_id
|
||||
and float_compare(-line.balance, asset.original_value, precision_rounding=asset.currency_id.rounding) == 0
|
||||
for line in move.line_ids
|
||||
):
|
||||
account = asset.account_depreciation_id
|
||||
asset_depreciation = (
|
||||
asset.original_value
|
||||
- asset.salvage_value
|
||||
- sum(
|
||||
move.line_ids.filtered(lambda l: l.account_id == account).mapped(
|
||||
'debit' if asset.original_value > 0 else 'credit'
|
||||
)
|
||||
) * (-1 if asset.original_value < 0 else 1)
|
||||
)
|
||||
else:
|
||||
asset_depreciation = 0
|
||||
move.depreciation_value = asset_depreciation
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# INVERSE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
def _inverse_depreciation_value(self):
|
||||
for move in self:
|
||||
asset = move.asset_id
|
||||
amount = abs(move.depreciation_value)
|
||||
account = asset.account_depreciation_expense_id
|
||||
move.write({'line_ids': [
|
||||
Command.update(line.id, {
|
||||
'balance': amount if line.account_id == account else -amount,
|
||||
})
|
||||
for line in move.line_ids
|
||||
]})
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINT METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@api.constrains('state', 'asset_id')
|
||||
def _constrains_check_asset_state(self):
|
||||
for move in self.filtered(lambda mv: mv.asset_id):
|
||||
asset_id = move.asset_id
|
||||
if asset_id.state == 'draft' and move.state == 'posted':
|
||||
raise ValidationError(_("You can't post an entry related to a draft asset. Please post the asset before."))
|
||||
|
||||
def _post(self, soft=True):
|
||||
# OVERRIDE
|
||||
posted = super()._post(soft)
|
||||
|
||||
# log the post of a depreciation
|
||||
posted._log_depreciation_asset()
|
||||
|
||||
# look for any asset to create, in case we just posted a bill on an account
|
||||
# configured to automatically create assets
|
||||
posted.sudo()._auto_create_asset()
|
||||
|
||||
return posted
|
||||
|
||||
def _reverse_moves(self, default_values_list=None, cancel=False):
|
||||
if default_values_list is None:
|
||||
default_values_list = [{} for _i in self]
|
||||
for move, default_values in zip(self, default_values_list):
|
||||
# Report the value of this move to the next draft move or create a new one
|
||||
if move.asset_id:
|
||||
# Recompute the status of the asset for all depreciations posted after the reversed entry
|
||||
|
||||
first_draft = min(move.asset_id.depreciation_move_ids.filtered(lambda m: m.state == 'draft'), key=lambda m: m.date, default=None)
|
||||
if first_draft:
|
||||
# If there is a draft, simply move/add the depreciation amount here
|
||||
first_draft.depreciation_value += move.depreciation_value
|
||||
else:
|
||||
# If there was no draft move left, create one
|
||||
last_date = max(move.asset_id.depreciation_move_ids.mapped('date'))
|
||||
method_period = move.asset_id.method_period
|
||||
|
||||
self.create(self._prepare_move_for_asset_depreciation({
|
||||
'asset_id': move.asset_id,
|
||||
'amount': move.depreciation_value,
|
||||
'depreciation_beginning_date': last_date + (relativedelta(months=1) if method_period == "1" else relativedelta(years=1)),
|
||||
'date': last_date + (relativedelta(months=1) if method_period == "1" else relativedelta(years=1)),
|
||||
'asset_number_days': 0
|
||||
}))
|
||||
|
||||
msg = _('Depreciation entry %s reversed (%s)', move.name, formatLang(self.env, move.depreciation_value, currency_obj=move.company_id.currency_id))
|
||||
move.asset_id.message_post(body=msg)
|
||||
default_values['asset_id'] = move.asset_id.id
|
||||
default_values['asset_number_days'] = -move.asset_number_days
|
||||
default_values['asset_depreciation_beginning_date'] = default_values.get('date', move.date)
|
||||
|
||||
return super(AccountMove, self)._reverse_moves(default_values_list, cancel)
|
||||
|
||||
def button_cancel(self):
|
||||
# OVERRIDE
|
||||
res = super(AccountMove, self).button_cancel()
|
||||
self.env['account.asset'].sudo().search([('original_move_line_ids.move_id', 'in', self.ids)]).write({'active': False})
|
||||
return res
|
||||
|
||||
def button_draft(self):
|
||||
for move in self:
|
||||
if any(asset_id.state != 'draft' for asset_id in move.asset_ids):
|
||||
raise UserError(_('You cannot reset to draft an entry related to a posted asset'))
|
||||
# Remove any draft asset that could be linked to the account move being reset to draft
|
||||
move.asset_ids.filtered(lambda x: x.state == 'draft').unlink()
|
||||
return super(AccountMove, self).button_draft()
|
||||
|
||||
def _log_depreciation_asset(self):
|
||||
for move in self.filtered(lambda m: m.asset_id):
|
||||
asset = move.asset_id
|
||||
msg = _('Depreciation entry %s posted (%s)', move.name, formatLang(self.env, move.depreciation_value, currency_obj=move.company_id.currency_id))
|
||||
asset.message_post(body=msg)
|
||||
|
||||
def _auto_create_asset(self):
|
||||
create_list = []
|
||||
invoice_list = []
|
||||
auto_validate = []
|
||||
for move in self:
|
||||
if not move.is_invoice():
|
||||
continue
|
||||
|
||||
for move_line in move.line_ids:
|
||||
if (
|
||||
move_line.account_id
|
||||
and (move_line.account_id.can_create_asset)
|
||||
and move_line.account_id.create_asset != "no"
|
||||
and not (move_line.currency_id or move.currency_id).is_zero(move_line.price_total)
|
||||
and not move_line.asset_ids
|
||||
and not move_line.tax_line_id
|
||||
and move_line.price_total > 0
|
||||
and not (move.move_type in ('out_invoice', 'out_refund') and move_line.account_id.internal_group == 'asset')
|
||||
):
|
||||
if not move_line.name:
|
||||
raise UserError(_('Journal Items of %(account)s should have a label in order to generate an asset', account=move_line.account_id.display_name))
|
||||
if move_line.account_id.multiple_assets_per_line:
|
||||
# decimal quantities are not supported, quantities are rounded to the lower int
|
||||
units_quantity = max(1, int(move_line.quantity))
|
||||
else:
|
||||
units_quantity = 1
|
||||
vals = {
|
||||
'name': move_line.name,
|
||||
'company_id': move_line.company_id.id,
|
||||
'currency_id': move_line.company_currency_id.id,
|
||||
'analytic_distribution': move_line.analytic_distribution,
|
||||
'original_move_line_ids': [(6, False, move_line.ids)],
|
||||
'state': 'draft',
|
||||
'acquisition_date': move.invoice_date if not move.reversed_entry_id else move.reversed_entry_id.invoice_date,
|
||||
}
|
||||
model_id = move_line.account_id.asset_model
|
||||
if model_id:
|
||||
vals.update({
|
||||
'model_id': model_id.id,
|
||||
})
|
||||
auto_validate.extend([move_line.account_id.create_asset == 'validate'] * units_quantity)
|
||||
invoice_list.extend([move] * units_quantity)
|
||||
for i in range(1, units_quantity + 1):
|
||||
if units_quantity > 1:
|
||||
vals['name'] = move_line.name + _(" (%s of %s)", i, units_quantity)
|
||||
create_list.extend([vals.copy()])
|
||||
|
||||
assets = self.env['account.asset'].with_context({}).create(create_list)
|
||||
for asset, vals, invoice, validate in zip(assets, create_list, invoice_list, auto_validate):
|
||||
if 'model_id' in vals:
|
||||
asset._onchange_model_id()
|
||||
if validate:
|
||||
asset.validate()
|
||||
if invoice:
|
||||
asset.message_post(body=_('Asset created from invoice: %s', invoice._get_html_link()))
|
||||
asset._post_non_deductible_tax_value()
|
||||
return assets
|
||||
|
||||
@api.model
|
||||
def _prepare_move_for_asset_depreciation(self, vals):
|
||||
missing_fields = {'asset_id', 'amount', 'depreciation_beginning_date', 'date', 'asset_number_days'} - set(vals)
|
||||
if missing_fields:
|
||||
raise UserError(_('Some fields are missing %s', ', '.join(missing_fields)))
|
||||
asset = vals['asset_id']
|
||||
analytic_distribution = asset.analytic_distribution
|
||||
depreciation_date = vals.get('date', fields.Date.context_today(self))
|
||||
company_currency = asset.company_id.currency_id
|
||||
current_currency = asset.currency_id
|
||||
prec = company_currency.decimal_places
|
||||
amount_currency = vals['amount']
|
||||
amount = current_currency._convert(amount_currency, company_currency, asset.company_id, depreciation_date)
|
||||
# Keep the partner on the original invoice if there is only one
|
||||
partner = asset.original_move_line_ids.mapped('partner_id')
|
||||
partner = partner[:1] if len(partner) <= 1 else self.env['res.partner']
|
||||
move_line_1 = {
|
||||
'name': asset.name,
|
||||
'partner_id': partner.id,
|
||||
'account_id': asset.account_depreciation_id.id,
|
||||
'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
|
||||
'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
|
||||
'analytic_distribution': analytic_distribution,
|
||||
'currency_id': current_currency.id,
|
||||
'amount_currency': -amount_currency,
|
||||
}
|
||||
move_line_2 = {
|
||||
'name': asset.name,
|
||||
'partner_id': partner.id,
|
||||
'account_id': asset.account_depreciation_expense_id.id,
|
||||
'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
|
||||
'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
|
||||
'analytic_distribution': analytic_distribution,
|
||||
'currency_id': current_currency.id,
|
||||
'amount_currency': amount_currency,
|
||||
}
|
||||
move_vals = {
|
||||
'partner_id': partner.id,
|
||||
'date': depreciation_date,
|
||||
'journal_id': asset.journal_id.id,
|
||||
'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
|
||||
'asset_id': asset.id,
|
||||
'ref': _("%s: Depreciation", asset.name),
|
||||
'asset_depreciation_beginning_date': vals['depreciation_beginning_date'],
|
||||
'asset_number_days': vals['asset_number_days'],
|
||||
'name': '/',
|
||||
'asset_value_change': vals.get('asset_value_change', False),
|
||||
'move_type': 'entry',
|
||||
'currency_id': current_currency.id,
|
||||
}
|
||||
return move_vals
|
||||
|
||||
@api.depends('line_ids.asset_ids')
|
||||
def _compute_asset_ids(self):
|
||||
for record in self:
|
||||
record.asset_ids = record.line_ids.asset_ids
|
||||
record.count_asset = len(record.asset_ids)
|
||||
record.asset_id_display_name = _('Asset')
|
||||
record.draft_asset_exists = bool(record.asset_ids.filtered(lambda x: x.state == "draft"))
|
||||
|
||||
def open_asset_view(self):
|
||||
return self.asset_id.open_asset(['form'])
|
||||
|
||||
def action_open_asset_ids(self):
|
||||
return self.asset_ids.open_asset(['tree', 'form'])
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
asset_ids = fields.Many2many('account.asset', 'asset_move_line_rel', 'line_id', 'asset_id', string='Related Assets', copy=False)
|
||||
non_deductible_tax_value = fields.Monetary(compute='_compute_non_deductible_tax_value', currency_field='company_currency_id')
|
||||
|
||||
def _get_computed_taxes(self):
|
||||
if self.move_id.asset_id:
|
||||
return self.tax_ids
|
||||
return super()._get_computed_taxes()
|
||||
|
||||
def turn_as_asset(self):
|
||||
ctx = self.env.context.copy()
|
||||
ctx.update({
|
||||
'default_original_move_line_ids': [(6, False, self.env.context['active_ids'])],
|
||||
'default_company_id': self.company_id.id,
|
||||
})
|
||||
if any(line.move_id.state == 'draft' for line in self):
|
||||
raise UserError(_("All the lines should be posted"))
|
||||
if any(account != self[0].account_id for account in self.mapped('account_id')):
|
||||
raise UserError(_("All the lines should be from the same account"))
|
||||
return {
|
||||
"name": _("Turn as an asset"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "account.asset",
|
||||
"views": [[False, "form"]],
|
||||
"target": "current",
|
||||
"context": ctx,
|
||||
}
|
||||
|
||||
@api.depends('tax_ids.invoice_repartition_line_ids')
|
||||
def _compute_non_deductible_tax_value(self):
|
||||
""" Handle the specific case of non deductible taxes,
|
||||
such as "50% Non Déductible - Frais de voiture (Prix Excl.)" in Belgium.
|
||||
"""
|
||||
non_deductible_tax_ids = self.tax_ids.invoice_repartition_line_ids.filtered(
|
||||
lambda line: line.repartition_type == 'tax' and not line.use_in_tax_closing
|
||||
).tax_id
|
||||
|
||||
res = {}
|
||||
if non_deductible_tax_ids:
|
||||
domain = [('move_id', 'in', self.move_id.ids)]
|
||||
tax_details_query, tax_details_params = self._get_query_tax_details_from_domain(domain)
|
||||
|
||||
self.flush_model()
|
||||
self._cr.execute(f'''
|
||||
SELECT
|
||||
tdq.base_line_id,
|
||||
SUM(tdq.tax_amount_currency)
|
||||
FROM ({tax_details_query}) AS tdq
|
||||
JOIN account_move_line aml ON aml.id = tdq.tax_line_id
|
||||
JOIN account_tax_repartition_line trl ON trl.id = tdq.tax_repartition_line_id
|
||||
WHERE tdq.base_line_id IN %s
|
||||
AND trl.use_in_tax_closing IS FALSE
|
||||
GROUP BY tdq.base_line_id
|
||||
''', tax_details_params + [tuple(self.ids)])
|
||||
|
||||
res = {row['base_line_id']: row['sum'] for row in self._cr.dictfetchall()}
|
||||
|
||||
for record in self:
|
||||
record.non_deductible_tax_value = res.get(record._origin.id, 0.0)
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
gain_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
domain="[('deprecated', '=', False)]",
|
||||
check_company=True,
|
||||
help="Account used to write the journal item in case of gain while selling an asset",
|
||||
)
|
||||
loss_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
domain="[('deprecated', '=', False)]",
|
||||
check_company=True,
|
||||
help="Account used to write the journal item in case of loss while selling an asset",
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<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>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_asset,account.asset,model_account_asset,account.group_account_readonly,1,0,0,0
|
||||
access_account_asset_manager,account.asset,model_account_asset,account.group_account_manager,1,1,1,1
|
||||
access_account_asset_invoicing_payment,account.asset,model_account_asset,account.group_account_invoice,1,0,1,0
|
||||
access_asset_modify,access.asset.modify,model_asset_modify,account.group_account_user,1,1,1,0
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 896 B |
@@ -0,0 +1 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M37 17h11v13H37V17Z" fill="#088BF5"/><path d="M0 34h4v4H0v-4Z" fill="#2EBCFA"/><path d="M44 33h4v5h-4v-5Z" fill="#144496"/><path d="M0 38a4 4 0 0 1 4-4h44v4a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4Z" fill="#144496"/><path d="M4.421 8C1.979 8 0 9.959 0 12.375V34h46c2.442 0 4-1.584 4-4v-4a2 2 0 0 0-2-2h-6c-1.831 0-3-1.188-3-3s1.169-3 3-3h6a2 2 0 0 0 2-2v-3.625C50 9.959 48.02 8 45.579 8H4.42Z" fill="#2EBCFA"/></svg>
|
||||
|
After Width: | Height: | Size: 497 B |
@@ -0,0 +1,37 @@
|
||||
.account_report.depreciation_schedule {
|
||||
table.striped {
|
||||
> thead > tr:not(:first-child) {
|
||||
> th:nth-child(2n+3) { background: inherit }
|
||||
> th[data-expression_label='assets_date_from'],
|
||||
> th[data-expression_label='assets_plus'],
|
||||
> th[data-expression_label='assets_minus'],
|
||||
> th[data-expression_label='assets_date_to'],
|
||||
> th[data-expression_label='balance'] {
|
||||
background: $o-gray-100
|
||||
}
|
||||
}
|
||||
> tbody {
|
||||
> tr:not(.line_level_0):not(.empty) {
|
||||
> td:nth-child(2n+3) { background: inherit }
|
||||
> td[data-expression_label='assets_date_from'],
|
||||
> td[data-expression_label='assets_plus'],
|
||||
> td[data-expression_label='assets_minus'],
|
||||
> td[data-expression_label='assets_date_to'],
|
||||
> td[data-expression_label='balance'] {
|
||||
background: $o-gray-100
|
||||
}
|
||||
}
|
||||
> tr.line_level_0
|
||||
{
|
||||
> td:nth-child(2n+3) { background: inherit }
|
||||
> td[data-expression_label='assets_date_from'],
|
||||
> td[data-expression_label='assets_plus'],
|
||||
> td[data-expression_label='assets_minus'],
|
||||
> td[data-expression_label='assets_date_to'],
|
||||
> td[data-expression_label='balance'] {
|
||||
background: $o-gray-300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="account_asset.DepreciationScheduleFilterExtraOptions" t-inherit="account_reports.AccountReportFilterExtraOptions">
|
||||
<xpath expr="//Dropdown" position="inside">
|
||||
<DropdownItem
|
||||
class="{ 'selected': controller.options.assets_groupby_account }"
|
||||
onSelected="() => this.toggleFilter('assets_groupby_account')"
|
||||
>
|
||||
Group By Account
|
||||
</DropdownItem>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="account_asset.DepreciationScheduleFilters" t-inherit="account_reports.AccountReportFiltersCustomizable">
|
||||
<xpath expr="//div[@id='filter_extra_options']" position="after">
|
||||
<t t-call="account_asset.DepreciationScheduleFilterExtraOptions"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class MoveReversed extends Component {}
|
||||
MoveReversed.template = "account_asset.moveReversed";
|
||||
|
||||
export const moveReversed = {
|
||||
component: MoveReversed,
|
||||
noLabel: true,
|
||||
};
|
||||
|
||||
registry.category("fields").add("deprec_lines_reversed", moveReversed);
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="account_asset.moveReversed">
|
||||
<i t-if="props.record.data[props.name].records.length" class="fa fa-exclamation-circle" title="This move has been reversed"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,8 @@
|
||||
// = ACCOUNT ASSET
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_account_reports_page.o_account_assets_report {
|
||||
--DepreciationSchedule-background-color: #{$o-gray-300};
|
||||
--DepreciationSchedule-border-color: #{$o-gray-600};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
.o_account_reports_page.o_account_assets_report .o_account_reports_table {
|
||||
|
||||
.o_account_reports_header_hierarchy {
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
.o_account_asset_report_top_header_row th:not(:first-child) {
|
||||
border: 1px solid var(--DepreciationSchedule-border-color, #{$o-gray-200});
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.o_account_report_column_header{
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
& > tr:first-child th{
|
||||
text-align: right !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_account_reports_header_hierarchy tr:nth-child(3) th:nth-child(2n+6),
|
||||
.o_account_asset_column_contrast{
|
||||
background-color: var(--DepreciationSchedule-background-color, #{$o-gray-100});
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.o_account_asset_contrast_inner{
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.o_asset_blank_if_zero_value{
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.o_account_asset_kanban_title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o_account_reports_body_print .o_account_assets_report{
|
||||
|
||||
.o_account_asset_contrast_inner{
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td.o_foldable_total {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.o_account_reports_header_hierarchy {
|
||||
white-space: nowrap;
|
||||
.o_account_asset_report_top_header_row th:not(:first-child) {
|
||||
border: 1px solid lightgrey;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
tr:nth-child(3) {
|
||||
th:nth-child(2), th:nth-child(3), th:nth-child(4), th:nth-child(5) {
|
||||
table > tbody > tr > td{
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
th > table > tbody > tr > td {
|
||||
font-weight: normal;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_account_asset
|
||||
from . import test_board_compute
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user