This commit is contained in:
Songb
2024-05-06 08:13:30 +08:00
parent f5b00b5b1b
commit 06e12e85ac
141 changed files with 163055 additions and 0 deletions
+5
View File
@@ -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
+44
View File
@@ -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>
+69
View File
@@ -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>
+8
View File
@@ -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>
+25
View File
@@ -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
+8
View File
@@ -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
+35
View File
@@ -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
+361
View File
@@ -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)
+20
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_asset account.asset model_account_asset account.group_account_readonly 1 0 0 0
3 access_account_asset_manager account.asset model_account_asset account.group_account_manager 1 1 1 1
4 access_account_asset_invoicing_payment account.asset model_account_asset account.group_account_invoice 1 0 1 0
5 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;
}
}
}
}
+5
View File
@@ -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