1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_reports/models/account_journal_report.py
2024-12-10 09:04:09 +07:00

1159 lines
65 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.tools import format_date, date_utils, get_lang
from collections import defaultdict
from odoo.exceptions import UserError, RedirectWarning
import json
import datetime
class JournalReportCustomHandler(models.AbstractModel):
_name = 'account.journal.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Journal Report Custom Handler'
def _get_custom_display_config(self):
return {
'css_custom_class': 'journal_report',
'components': {
'AccountReportLine': 'account_reports.JournalReportLine',
},
'templates': {
'AccountReportFilters': 'account_reports.JournalReportFilters',
'AccountReportHeader': 'account_reports.JournalReportHeader',
'AccountReportLineName': 'account_reports.JournalReportLineName',
},
'pdf_export': {
'pdf_export_main_table_header': 'account_reports.journal_report_pdf_export_main_table_header',
'pdf_export_filters': 'account_reports.journal_report_pdf_export_filters',
'pdf_export_main_table_body': 'account_reports.journal_report_pdf_export_main_table_body',
},
}
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
""" Returns the first level of the report, journal lines. """
journal_query_res = self._query_journal(options)
# Set the options with the journals that should be unfolded by default.
lines = []
unfolded_journals = []
for journal_index, (journal_id, journal_vals) in enumerate(journal_query_res.items()):
journal_key = report._get_generic_line_id('account.journal', journal_id)
unfolded = journal_key in options.get('unfolded_lines') or options.get('unfold_all')
if unfolded:
unfolded_journals.append(unfolded)
lines.append(self._get_journal_line(options, journal_key, journal_vals, unfolded, is_first_journal=len(unfolded_journals) == 1))
global_tax_summary_lines = self._get_global_tax_summary_lines(options)
lines.extend(global_tax_summary_lines)
return [(0, line) for line in lines]
def _get_global_tax_summary_lines(self, options):
# It is faster to first check that we need a tax section; this avoids computing a tax report for nothing.
aml_has_tax_domain = [('tax_ids', '!=', False)]
if options.get('date', {}).get('date_from'):
aml_has_tax_domain.append(('date', '>=', options['date']['date_from']))
if options.get('date', {}).get('date_to'):
aml_has_tax_domain.append(('date', '<=', options['date']['date_to']))
has_tax = bool(self.env['account.move.line'].search_count(aml_has_tax_domain, limit=1))
if not has_tax:
return []
report = self.env.ref('account_reports.journal_report')
tax_data = {
'date_from': options.get('date', {}).get('date_from'),
'date_to': options.get('date', {}).get('date_to'),
}
# This is a special line with a special template to render it.
# It will contain two tables, which are the tax report and tax grid summary sections.
tax_report_lines = self._get_generic_tax_summary_for_sections(options, tax_data)
tax_non_deductible_column = any(line.get('tax_non_deductible_no_format') for lines_per_country in tax_report_lines.values() for line in lines_per_country)
tax_deductible_column = any(line.get('tax_deductible_no_format') for lines_per_country in tax_report_lines.values() for line in lines_per_country)
tax_due_column = any(line.get('tax_due_no_format') for lines_per_country in tax_report_lines.values() for line in lines_per_country)
extra_columns = int(tax_non_deductible_column) + int(tax_deductible_column) + int(tax_due_column)
tax_grid_summary_lines = self._get_tax_grids_summary(options, tax_data)
if not tax_report_lines and not tax_grid_summary_lines:
return []
return [
{
'id': report._get_generic_line_id(False, False, markup='tax_report_section_heading'),
'name': _('Global Tax Summary'),
'level': 0,
'columns': [],
'unfoldable': False,
'page_break': True,
'colspan': len(options['columns']) + 1 # We want it to take the whole line. It makes it easier to unfold it.
},
{
'id': report._get_generic_line_id(False, False, markup='tax_report_section'),
'name': '',
'is_tax_section_line': True,
'tax_report_lines': tax_report_lines,
'tax_non_deductible_column': tax_non_deductible_column,
'tax_deductible_column': tax_deductible_column,
'tax_due_column': tax_due_column,
'extra_columns': extra_columns,
'tax_grid_summary_lines': tax_grid_summary_lines,
'date_from': tax_data['date_from'],
'date_to': tax_data['date_to'],
'columns': [],
'colspan': len(options['columns']) + 1,
'level': 3,
'class': 'o_account_reports_ja_subtable',
},
]
def _custom_options_initializer(self, report, options, previous_options=None):
""" Initialize the options for the journal report. """
# This dictionnary makes it possible to make options unavailable for the report. The structure of the dictionnary is
# { <type: str - Name of the disabled option>: <type: tuple - tuple containing striclty two elements, a and b> }
# with a <type: list - list of the keys that must be reached in the options dictionnary to get the value to test>
# and b <type: list - list of the values that are authorized for the said option>
restricted_options = {
_("Analytic Accounts Groupby"): (['analytic_accounts_groupby'], []),
_("Analytic Plans Groupby"): (['analytic_plans_groupby'], []),
_("Horizontal Grouping"): (['selected_horizontal_group_id'], []),
_("Period comparison"): (['comparison', 'filter'], ['no_comparison']),
}
for name, (path, authorized_value) in restricted_options.items():
option = options
while path and option:
option = option.get(path.pop(0))
if option and (not authorized_value or option not in authorized_value):
raise UserError(name + _(" is not supported by the Journal Report"))
super()._custom_options_initializer(report, options, previous_options=previous_options)
# Initialise the custom options for this report.
custom_filters = {
'sort_by_date': False,
'group_by_months': False,
'show_payment_lines': True,
}
for name, default_val in custom_filters.items():
options[name] = (previous_options or {}).get(name, default_val)
# if no journal is selected (so, all are) and no other line is unfolded, unfold the first journal
# Ensure that all selected journals are unfolded by default
available_journal_ids = [j['id'] for j in options['journals'] if j['model'] == 'account.journal']
selected_journal_ids = {j['id'] for j in options['journals'] if j.get('selected', False) and j['model'] == 'account.journal'}
any_unfolded_journal = any(report._parse_line_id(unfolded_line)[-1][1] == 'account.journal' for unfolded_line in options['unfolded_lines'] if report._parse_line_id(unfolded_line)[-1][2] in available_journal_ids)
unfolded_lines = options['unfolded_lines']
if selected_journal_ids:
for journal_id in selected_journal_ids:
line_id = report._get_generic_line_id('account.journal', journal_id)
if line_id not in unfolded_lines:
unfolded_lines.append(line_id)
elif not any_unfolded_journal and options['export_mode'] != 'print' and available_journal_ids:
line_id = report._get_generic_line_id('account.journal', available_journal_ids[0])
if line_id not in unfolded_lines:
unfolded_lines.append(line_id)
if self.user_has_groups('base.group_multi_currency'):
options['multi_currency'] = True
def _custom_line_postprocessor(self, report, options, lines, warnings=None):
if options['export_mode'] != 'print':
return lines
new_lines = []
for line in lines:
model_info = self.env['account.report']._get_model_info_from_id(line['id'])
if model_info[0] == 'account.journal' and line.get('unfolded', False) or model_info[0] != 'account.journal':
new_lines.append(line)
return new_lines
def _query_journal(self, options):
params = []
queries = []
report = self.env.ref('account_reports.journal_report')
if self.pool['account.journal'].name.translate:
lang = self.env.user.lang or get_lang(self.env).code
j_name = f"COALESCE(j.name->>'{lang}', j.name->>'en_US')"
else:
j_name = "j.name"
for column_group_key, options_group in report._split_options_per_column_group(options).items():
tables, where_clause, where_params = report._query_get(options_group, 'strict_range')
params.append(column_group_key)
params += where_params
queries.append(f"""
SELECT
%s as column_group_key,
j.id,
{j_name} as name,
j.code,
j.type,
j.currency_id,
journal_curr.name as currency_name,
cp.currency_id as company_currency
FROM {tables}
JOIN account_journal j ON j.id = account_move_line.journal_id
JOIN res_company cp ON cp.id = j.company_id
LEFT JOIN res_currency journal_curr on journal_curr.id = j.currency_id
WHERE {where_clause}
GROUP BY
j.id, {j_name}, j.code, j.type, j.currency_id, journal_curr.name, cp.currency_id
ORDER BY j.id
""")
rslt = {}
self._cr.execute(" UNION ALL ".join(queries), params)
for journal_res in self._cr.dictfetchall():
if journal_res['id'] not in rslt:
rslt[journal_res['id']] = {col_group_key: {} for col_group_key in options['column_groups']}
rslt[journal_res['id']][journal_res['column_group_key']] = journal_res
return rslt
##########################################################################
# Get lines methods
##########################################################################
def _get_lines_for_group(self, options, parent_line_id, journal, progress, offset):
""" Create the report lines for a group of moves. A group is either a journal, or a month if the report is grouped by month."""
def cumulate_balance(line, current_balances, is_unreconciled_payment):
# For bank journals, we want to cumulate the balances and display their evolution line by line until the end.
for column_group_key in options['column_groups']:
# For bank journals, we want to cumulate the balances and display their evolution line by line until the end.
if journal.type == 'bank' and line[column_group_key]['account_type'] not in ('liability_credit_card', 'asset_cash') and not is_unreconciled_payment:
current_balances[column_group_key] += -line[column_group_key]['balance']
line[column_group_key]['cumulated_balance'] = current_balances[column_group_key]
lines, after_load_more_lines = [], []
current_balances, next_progress = {}, {}
# Treated result count also consider the lines not rendered in the report, and is used for the query offset.
# Rendered line count does not consider the lines not rendered, and allows to stop rendering more when the quota has been reached.
treated_results_count = 0
has_more_lines = False
eval_dict = self._query_aml(options, offset, journal)
if offset == 0:
lines.append(self._get_columns_line(options, parent_line_id, journal.type))
if journal.type == 'bank':
# Get initial balance, only if the journal is of type 'bank', and we have no offset yet (first unfolding)
if offset == 0:
if journal.type == 'bank':
init_balance_by_col_group = self._get_journal_initial_balance(options, journal.id)
initial_balance_line = self._get_journal_balance_line(
options, parent_line_id, init_balance_by_col_group, is_starting_balance=True)
if initial_balance_line:
lines.append(initial_balance_line)
# For the first expansion of the line, the initial balance line gives the progress
progress = {
column['column_group_key']: line_col.get('no_format', 0.0)
for column, line_col in zip(options['columns'], initial_balance_line['columns'])
if column['expression_label'] == 'additional_col_1'
}
# Weither we just fetched them or not, the balance is now in the progress.
for column_group_key in options['column_groups']:
current_balances[column_group_key] = progress.get(column_group_key, 0.0)
# Group the lines by moves, to simplify the following code.
line_dict_grouped = self._group_lines_by_move(options, eval_dict, parent_line_id)
report = self.env.ref('account_reports.journal_report')
treated_amls_count = 0
for move_key, move_line_vals_list in line_dict_grouped.items():
# All move lines for a move will share static values, like if the move is multicurrency, the journal,..
# These can be fetched using any column groups or lines for this move.
first_move_line = move_line_vals_list[0]
general_line_vals = next(col_group_val for col_group_val in first_move_line.values())
if report.load_more_limit and len(move_line_vals_list) + treated_amls_count > report.load_more_limit and options['export_mode'] != 'print':
# This element won't generate a line now, but we use it to know that we'll need to add a load_more line.
has_more_lines = True
if treated_amls_count == 0:
# A single move lines count exceed the load more limit, we need to raise to inform the user
msg = _("The 'load more limit' setting of this report is too low to display all the lines of the entry you're trying to show.")
if self.env.user.has_group('account.group_account_manager'):
action = {
"view_mode": "form",
"res_model": "account.report",
"type": "ir.actions.act_window",
"res_id" : report.id,
"views": [[self.env.ref("account_reports.account_report_form").id, "form"]],
}
title = _('Go to report configuration')
raise RedirectWarning(msg, action, title)
raise UserError(msg)
break
is_unreconciled_payment = journal.type == 'bank' and not any(line for line in move_line_vals_list if next(col_group_val for col_group_val in line.values())['account_type'] in ('liability_credit_card', 'asset_cash'))
if journal.type == 'bank':
cumulate_balance(first_move_line, current_balances, is_unreconciled_payment)
# Do not display payments move on bank journal if the options isn't enabled.
if not options.get('show_payment_lines') and is_unreconciled_payment:
treated_results_count += len(move_line_vals_list) # used to get the offset
continue
# Create the first line separately, as we want to give it some specific behavior and styling
lines.append(self._get_first_move_line(options, parent_line_id, move_key, first_move_line, is_unreconciled_payment))
treated_amls_count += len(move_line_vals_list)
treated_results_count += 1
for line_index, move_line_vals in enumerate(move_line_vals_list[1:]):
if journal.type == 'bank':
cumulate_balance(move_line_vals, current_balances, is_unreconciled_payment)
line = self._get_aml_line(options, parent_line_id, move_line_vals, line_index, journal, is_unreconciled_payment)
treated_results_count += 1
if line:
lines.append(line)
multicurrency_name = self._get_aml_line_name(options, journal, -1, first_move_line, is_unreconciled_payment)
# Add a currency line if we have a foreign currency on the move but no place to put this info beforehand in the name of a line.
# This can happen if we have two lines only and a ref: the ref take the name of the second line and we need a new one for the currency.
if general_line_vals['is_multicurrency'] \
and len(move_line_vals_list) == 2 \
and self.user_has_groups('base.group_multi_currency') \
and lines[-1]['name'] != multicurrency_name \
and journal.type != 'bank':
lines.append({
'id': report._get_generic_line_id('account.move.line', general_line_vals['move_id'], parent_line_id=parent_line_id, markup='amount_currency_total'),
'name': multicurrency_name,
'level': 3,
'parent_id': parent_line_id,
'columns': [{} for column in options['columns']],
})
if journal.type == 'bank':
next_progress = {
column['column_group_key']: line_col.get('no_format', 0.0)
for column, line_col in zip(options['columns'], lines[-1]['columns'])
if column['expression_label'] == 'additional_col_1'
}
# If we have no offsets, check if we can create a tax line: This line will contain two tables, one for the tax summary and one for the tax grid summary.
if offset == 0:
# It is faster to first check that we need a tax section; this avoids computing a tax report for nothing.
aml_has_tax_domain = [('journal_id', '=', journal.id), ('tax_ids', '!=', False)]
if options.get('date', {}).get('date_from'):
aml_has_tax_domain.append(('date', '>=', options['date']['date_from']))
if options.get('date', {}).get('date_to'):
aml_has_tax_domain.append(('date', '<=', options['date']['date_to']))
journal_has_tax = bool(self.env['account.move.line'].search_count(aml_has_tax_domain, limit=1))
if journal_has_tax:
tax_data = {
'date_from': options.get('date', {}).get('date_from'),
'date_to': options.get('date', {}).get('date_to'),
'journal_id': journal.id,
'journal_type': journal.type,
}
# This is a special line with a special template to render it.
# It will contain two tables, which are the tax report and tax grid summary sections.
tax_report_lines = self._get_generic_tax_summary_for_sections(options, tax_data)
tax_non_deductible_column = any(line.get('tax_non_deductible_no_format') for country in tax_report_lines.values() for line in country)
tax_deductible_column = any(line.get('tax_deductible_no_format') for country in tax_report_lines.values() for line in country)
tax_due_column = any(line.get('tax_due_no_format') for country in tax_report_lines.values() for line in country)
extra_columns = int(tax_non_deductible_column) + int(tax_deductible_column) + int(tax_due_column)
tax_grid_summary_lines = self._get_tax_grids_summary(options, tax_data)
if tax_report_lines or tax_grid_summary_lines:
after_load_more_lines.append({
'id': report._get_generic_line_id(False, False, parent_line_id=parent_line_id, markup='tax_report_section'),
'name': '',
'parent_id': parent_line_id,
'journal_id': journal.id,
'is_tax_section_line': True,
'tax_report_lines': tax_report_lines,
'tax_non_deductible_column': tax_non_deductible_column,
'tax_deductible_column': tax_deductible_column,
'tax_due_column': tax_due_column,
'extra_columns': extra_columns,
'tax_grid_summary_lines': tax_grid_summary_lines,
'date_from': tax_data['date_from'],
'date_to': tax_data['date_to'],
'columns': [],
'colspan': len(options['columns']) + 1,
'level': 3,
})
return lines, after_load_more_lines, has_more_lines, treated_results_count, next_progress, current_balances
def _get_columns_line(self, options, parent_key, journal_type):
""" returns the line displaying the columns used by the journal.
The report isn't using the table header, as different journal type needs different columns.
:param options: The report options
:param parent_key: the key of the parent line, journal or month
:param journal_type: the journal type
"""
columns = []
has_multicurrency = self.user_has_groups('base.group_multi_currency')
report = self.env['account.report'].browse(options['report_id'])
# code assumes additional_col_1 & additional_col_2 are last columns, but since no sequences are set on columns,
# it might happen (i.e. after db update from 16 to 17) that that's not the case.
options['columns'] = sorted(options['columns'], key=lambda col: col.get('expression_label') in ['additional_col_1', 'additional_col_2'])
for column in options['columns']:
if column['expression_label'] == 'additional_col_1':
if journal_type in ['sale', 'purchase']:
col_value = _('Taxes')
elif journal_type == 'bank':
col_value = _('Balance')
else:
col_value = ''
elif column['expression_label'] == 'additional_col_2':
if journal_type in ['sale', 'purchase']:
col_value = _('Tax Grids')
elif journal_type == 'bank' and has_multicurrency:
col_value = _('Amount In Currency')
else:
col_value = ''
elif column['expression_label'] == 'invoice_date':
col_value = column['name'] if journal_type in ['sale', 'purchase', 'general'] else ''
else:
col_value = column['name']
columns.append(report._build_column_dict(col_value, column, options=options))
return {
'id': report._get_generic_line_id(None, None, parent_line_id=parent_key, markup='headers'),
'name': _('Name'),
'columns': columns,
'level': 3,
'parent_id': parent_key,
}
def _get_journal_line(self, options, line_id, eval_dict, unfolded, is_first_journal):
""" returns the line that is representing a journal in the report.
:param options: The report options
:param line_id: The line id for this journal
:param eval_dict: The values for this journal
:param is_first_journal: If this is the first journal in the report or not. Additional journals will have a page break used when printing in PDF.
"""
# The column group does not matter for these values: any group will share the same journal.
journal_vals = next(col_group_val for col_group_val in eval_dict.values())
has_foreign_currency = journal_vals['currency_id'] and journal_vals['currency_id'] != journal_vals['company_currency']
return {
'id': line_id,
'name': f"{journal_vals['name']} ({journal_vals['code']}){' ' + journal_vals['currency_name'] if has_foreign_currency else ''}",
'level': 0,
'columns': [],
'unfoldable': True,
'unfolded': unfolded,
'journal_id': journal_vals['id'],
'journal_type': journal_vals['type'],
'page_break': unfolded and not is_first_journal,
'expand_function': '_report_expand_unfoldable_line_journal_report' if not options['group_by_months'] else '_report_expand_unfoldable_line_journal_report_expand_journal_line_by_month',
'colspan': len(options['columns']) + 1 # We want it to take the whole line. It makes it easier to unfold it.
}
def _report_expand_unfoldable_line_journal_report(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data):
report = self.env['account.report'].browse(options['report_id'])
new_options = options.copy()
if options['group_by_months']:
# If grouped by month, we get the journal info from the parent line.
parsed_line_id = report._parse_line_id(line_dict_id)
model = parsed_line_id[-2][1]
journal_id = parsed_line_id[-2][2]
# We force the date in the options to be the ones for this month.
year, month = parsed_line_id[-1][0].split(' ')[1:]
date = datetime.date(int(year), int(month), 1)
new_options['date'] = {
'mode': 'range',
'date_from': date_utils.start_of(date, 'month'),
'date_to': date_utils.end_of(date, 'month'),
}
else:
model, journal_id = report._get_model_info_from_id(line_dict_id)
if model != 'account.journal':
raise UserError(_('Trying to use the journal line expand function on a line that is not linked to a journal.'))
lines = []
journal = self.env[model].browse(journal_id)
# Get move lines
new_lines, after_load_more_lines, has_more, treated_results_count, next_progress, ending_balance_by_col_group = self._get_lines_for_group(new_options, line_dict_id, journal, progress, offset)
lines.extend(new_lines)
if not has_more and journal.type == 'bank' and ending_balance_by_col_group:
ending_balance_line = self._get_journal_balance_line(new_options, line_dict_id, ending_balance_by_col_group, is_starting_balance=False)
if ending_balance_line:
lines.append(ending_balance_line)
return {
'lines': lines,
'after_load_more_lines': after_load_more_lines,
'offset_increment': treated_results_count,
'has_more': has_more,
'progress': next_progress,
}
def _report_expand_unfoldable_line_journal_report_expand_journal_line_by_month(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data):
model, record_id = self.env['account.report']._get_model_info_from_id(line_dict_id)
if model != 'account.journal':
raise UserError(_('Trying to use the journal line expand function on a line that is not linked to a journal.'))
lines = []
journal = self.env[model].browse(record_id)
aml_results = self._query_months(options, line_dict_id, offset, journal)
lines.extend(self._get_month_lines(options, line_dict_id, aml_results, progress, offset))
return {
'lines': lines,
}
def _get_month_lines(self, options, line_dict_id, aml_results, progress, offset):
report = self.env['account.report'].browse(options['report_id'])
lines = []
for month, months_with_vals in aml_results.items():
for month_vals in months_with_vals.values():
date = datetime.datetime.strptime(month, '%m %Y').date()
line_id = report._get_generic_line_id(None, None, parent_line_id=line_dict_id, markup=f'month_line {date.year} {date.month}')
lines.append({
'id': line_id,
'name': month_vals['display_month'],
'level': 2,
'columns': [],
'unfoldable': True,
'unfolded': line_id in options.get('unfolded_lines') or options.get('unfold_all'),
'parent_id': line_dict_id,
'colspan': len(options['columns']) + 1,
'expand_function': '_report_expand_unfoldable_line_journal_report',
})
return lines
def _get_journal_initial_balance(self, options, journal_id, date_month=False):
queries = []
params = []
report = self.env.ref('account_reports.journal_report')
for column_group_key, options_group in report._split_options_per_column_group(options).items():
new_options = self.env['account.general.ledger.report.handler']._get_options_initial_balance(options_group) # Same options as the general ledger
tables, where_clause, where_params = report._query_get(new_options, 'normal', domain=[('journal_id', '=', journal_id)])
params.append(column_group_key)
params += where_params
queries.append(f"""
SELECT
%s AS column_group_key,
sum("account_move_line".balance) as balance
FROM {tables}
JOIN account_journal journal ON journal.id = "account_move_line".journal_id AND "account_move_line".account_id = journal.default_account_id
WHERE {where_clause}
GROUP BY journal.id
""")
self._cr.execute(" UNION ALL ".join(queries), params)
init_balance_by_col_group = {column_group_key: 0.0 for column_group_key in options['column_groups']}
for result in self._cr.dictfetchall():
init_balance_by_col_group[result['column_group_key']] = result['balance']
return init_balance_by_col_group
def _get_journal_balance_line(self, options, parent_line_id, eval_dict, is_starting_balance=True):
""" Returns the line holding information about either the starting, or ending balance of a bank journal in the selected period.
:param options: dictionary containing the options for the current report
:param parent_key: the key of the parent line, either the journal or the month
:param balance: the starting/ending balance of the journal
:param is_starting_balance: whether the balance is the starting or ending balance. Used for formatting.
"""
line_columns = []
report = self.env['account.report'].browse(options['report_id'])
for column in options['columns']:
if column['expression_label'] == 'credit': # Add a text in the credit column
col_name = _('Starting Balance:') if is_starting_balance else _('Ending Balance:')
elif column['expression_label'] == 'additional_col_1':
col_name = eval_dict[column['column_group_key']]
else:
col_name = ''
line_columns.append(report._build_column_dict(col_name, column, options=options))
return {
'id': report._get_generic_line_id(None, None, parent_line_id=parent_line_id, markup='initial' if is_starting_balance else 'final'),
'name': '',
'parent_id': parent_line_id,
'columns': line_columns,
'level': 3,
}
def _get_first_move_line(self, options, parent_key, line_key, values, is_unreconciled_payment):
""" Returns the first line of a move.
It is different from the other lines, as it contains more information such as the date, partner, and a link to the move itself.
:param options: The report options.
:param parent_key: The id of the lines that should be parenting the aml lines. Should be the group line (either the journal, or month).
:param line_key: The id of the move line itself.
:param values: The values of the move line.
:param new_balance: The new balance of the move line, if any. Use to display the cumulated balance for bank journals.
"""
report = self.env['account.report'].browse(options['report_id'])
# Helps to format the line. If a line is linked to a partner but the account isn't receivable or payable, we want to display it in blue.
columns = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
values = values[column_group_key]
for column in options['columns']:
if column.get('expression_label') not in ['additional_col_1', 'additional_col_2']:
if column.get('expression_label') == 'account':
col_value = '%s %s' % (values['account_code'], values['partner_name'] or values['account_name'])
elif column.get('expression_label') == 'label':
col_value = values['name']
elif column.get('expression_label') == 'invoice_date':
if values['journal_type'] in ('sale', 'purchase'):
col_value = values['invoice_date'] if values['debit'] or values['credit'] else ''
elif values['journal_code'] == 'POSS':
col_value = values['date']
else:
col_value = ''
else:
col_value = values[column.get('expression_label')]
columns.append(report._build_column_dict(col_value, column, options=options))
else:
balance = False if column_group_options.get('show_payment_lines') and is_unreconciled_payment else values.get('cumulated_balance')
columns += self._get_move_line_additional_col(column_group_options, balance, values, is_unreconciled_payment)
break
return {
'id': line_key,
'name': values['move_name'],
'level': 3,
'date': format_date(self.env, values['date']),
'columns': columns,
'parent_id': parent_key,
'move_id': values['move_id'],
}
def _get_aml_line(self, options, parent_key, eval_dict, line_index, journal, is_unreconciled_payment):
""" Returns the line of an account move line.
:param options: The report options.
:param parent_key: The id of the lines that should be parenting the aml lines. Should be the group line (either the journal, or month).
:param values: The values of the move line.
:param current_balance: The current balance of the move line, if any. Use to display the cumulated balance for bank journals.
:param line_index: The index of the line in the move line list. Used to write additional information in the name, such as the move reference, or the ammount in currency.
"""
report = self.env['account.report'].browse(options['report_id'])
columns = []
general_vals = next(col_group_val for col_group_val in eval_dict.values())
if general_vals['journal_type'] == 'bank' and general_vals['account_type'] in ('liability_credit_card', 'asset_cash'):
# Do not display bank account lines for bank journals
return None
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
values = eval_dict[column_group_key]
for column in options['columns']:
if column.get('expression_label') not in ['additional_col_1', 'additional_col_2']:
if column.get('expression_label') == 'account':
if values['journal_type'] == 'bank': # For additional lines still showing in the bank journal, make sure to use the partner on the account if available.
col_value = '%s %s' % (values['account_code'], values['partner_name'] or values['account_name'])
elif values['journal_type'] == 'sale':
if values['debit']:
col_value = '%s %s' % (values['account_code'], values['partner_name'] or values['account_name'])
else:
col_value = '%s %s' % (values['account_code'], values['account_name'])
else:
col_value = '%s %s' % (values['account_code'], values['account_name'])
elif column.get('expression_label') == 'label':
col_value = values['name']
elif column.get('expression_label') == 'invoice_date':
col_value = ''
else:
col_value = values[column.get('expression_label')]
columns.append(report._build_column_dict(col_value, column, options=options))
else:
balance = False if column_group_options.get('show_payment_lines') and is_unreconciled_payment else values.get('cumulated_balance')
columns += self._get_move_line_additional_col(column_group_options, balance, values, is_unreconciled_payment)
break
return {
'id': report._get_generic_line_id('account.move.line', values['move_line_id'], parent_line_id=parent_key),
'name': self._get_aml_line_name(options, journal, line_index, eval_dict, is_unreconciled_payment),
'level': 3,
'parent_id': parent_key,
'columns': columns,
}
def _get_aml_line_name(self, options, journal, line_index, values, is_unreconciled_payment):
""" Returns the information to write as the name of the move lines, if needed.
Typically, this is the move reference, or the amount in currency if we are in a multicurrency environment and the move is using a foreign currency.
:param options: The report options.
:param line_index: The index of the line in the move line list. We always want the reference second if existing and the amount in currency third if needed.
:param values: The values of the move line.
"""
# Returns the first occurrence. There is only one column group anyway.
for column_group_key in options['column_groups']:
if journal.type == 'bank' or not (self.user_has_groups('base.group_multi_currency') and values[column_group_key]['is_multicurrency']):
amount_currency_name = ''
else:
amount_currency_name = _(
'Amount in currency: %s',
self.env['account.report']._format_value(
options,
values[column_group_key]['amount_currency_total'],
currency=self.env['res.currency'].browse(values[column_group_key]['move_currency']),
blank_if_zero=False,
figure_type='monetary'
)
)
if line_index == 0:
res = values[column_group_key]['reference'] or amount_currency_name
# if the invoice ref equals the payment ref then let's not repeat the information
return res if res != values[column_group_key]['move_name'] else ''
elif line_index == 1:
return values[column_group_key]['reference'] and amount_currency_name or ''
elif line_index == -1: # Only when we create a line just for the amount currency. It's the only time we always want the amount.
return amount_currency_name
else:
return ''
def _get_move_line_additional_col(self, options, current_balance, values, is_unreconciled_payment):
""" Returns the additional columns to be displayed on an account move line.
These are the column coming after the debit and credit columns.
For a sale or purchase journal, they will contain the taxes' information.
For a bank journal, they will contain the cumulated amount.
:param current_balance: The current balance of the move line, if any.
:param values: The values of the move line.
"""
report = self.env['account.report']
additional_col = [
report._build_column_dict('', {'figure_type': 'string', 'expression_label': 'additional_col_1'}, options=options),
report._build_column_dict('', {'figure_type': 'string', 'expression_label': 'additional_col_2'}, options=options),
]
if values['journal_type'] in ['sale', 'purchase']:
tax_val = ''
if values['taxes']:
# Display the taxes affecting the line, formatted as such: "T: t1, t2"
tax_val = _('T: %s', ', '.join(values['taxes']))
elif values['tax_base_amount']:
# Display the base amount on wich this tax line is based off, formatted as such: "B: $0.0"
tax_val = _('B: %s', report._format_value(options, values['tax_base_amount'], blank_if_zero=False, figure_type='monetary'))
additional_col = [
report._build_column_dict(tax_val, {'figure_type': 'string', 'expression_label': 'additional_col_1'}, options=options),
report._build_column_dict(', '.join(values['tax_grids']), {'figure_type': 'string', 'expression_label': 'additional_col_2'}, options=options),
]
elif values['journal_type'] == 'bank':
if values['account_type'] not in ('liability_credit_card', 'asset_cash') and current_balance:
additional_col = [
report._build_column_dict(current_balance, {'figure_type': 'monetary', 'expression_label': 'additional_col_1'}, options=options),
report._build_column_dict('', {'figure_type': 'string', 'expression_label': 'additional_col_2'}, options=options),
]
if self.user_has_groups('base.group_multi_currency') and values['move_line_currency'] != values['company_currency']:
amount = -values['amount_currency'] if not is_unreconciled_payment else values['amount_currency']
additional_col[-1] = report._build_column_dict(
amount,
{
'figure_type': 'monetary',
'expression_label': 'additional_col_2',
},
options=options,
currency=self.env['res.currency'].browse(values['move_line_currency']),
)
return additional_col
##########################################################################
# Helper methods
##########################################################################
def _get_generic_tax_report_options(self, options, data):
"""
Return an option dictionnary set to fetch the reports with the parameters needed for this journal.
The important bits are the journals, date, and fetch the generic tax reports that contains all taxes.
We also provide the information about wether to take all entries or only posted ones.
"""
generix_tax_report = self.env.ref('account.generic_tax_report')
previous_option = options.copy()
# Force the dates to the selected ones. Allows to get it correctly when grouped by months
previous_option.update({
'selected_variant_id': generix_tax_report.id,
'date_from': data.get('date_from'),
'date_to': data.get('date_to'),
})
tax_report_options = generix_tax_report.get_options(previous_option)
# Even though it doesn't have a journal selector, we can force a journal in the options to only get the lines for a specific journal.
if data.get('journal_id') or data.get('journal_type'):
tax_report_options['journals'] = [{
'id': data.get('journal_id'),
'model': 'account.journal',
'type': data.get('journal_type'),
'selected': True,
}]
else:
tax_report_options['journals'] = options.get('journals')
return tax_report_options
def _group_lines_by_move(self, options, eval_dict, parent_line_id):
report = self.env['account.report'].browse(options['report_id'])
grouped_dict = defaultdict(list)
for move_line_vals in eval_dict.values():
# We don't care about which column group is used for the id as it will be the same for all of them.
move_id = next(col_group_val for col_group_val in move_line_vals.values())['move_id']
move_key = report._get_generic_line_id('account.move', move_id, parent_line_id=parent_line_id)
grouped_dict[move_key].append(move_line_vals)
return grouped_dict
####################################################
# Queries
####################################################
def _query_aml(self, options, offset=0, journal=False):
params = []
queries = []
lang = self.env.user.lang or get_lang(self.env).code
acc_name = f"COALESCE(acc.name->>'{lang}', acc.name->>'en_US')" if \
self.pool['account.account'].name.translate else 'acc.name'
j_name = f"COALESCE(j.name->>'{lang}', j.name->>'en_US')" if \
self.pool['account.journal'].name.translate else 'j.name'
tax_name = f"COALESCE(tax.name->>'{lang}', tax.name->>'en_US')" if \
self.pool['account.tax'].name.translate else 'tax.name'
tag_name = f"COALESCE(tag.name->>'{lang}', tag.name->>'en_US')" if \
self.pool['account.account.tag'].name.translate else 'tag.name'
report = self.env.ref('account_reports.journal_report')
for column_group_key, options_group in report._split_options_per_column_group(options).items():
# Override any forced options: We want the ones given in the options
options_group['date'] = options['date']
tables, where_clause, where_params = report._query_get(options_group, 'strict_range', domain=[('journal_id', '=', journal.id)])
sort_by_date = options_group.get('sort_by_date')
params.append(column_group_key)
params += where_params
limit_to_load = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None
params += [limit_to_load, offset]
queries.append(f"""
SELECT
%s AS column_group_key,
"account_move_line".id as move_line_id,
"account_move_line".name,
"account_move_line".date,
"account_move_line".invoice_date,
"account_move_line".amount_currency,
"account_move_line".tax_base_amount,
"account_move_line".currency_id as move_line_currency,
"account_move_line".amount_currency,
am.id as move_id,
am.name as move_name,
am.journal_id,
am.currency_id as move_currency,
am.amount_total_in_currency_signed as amount_currency_total,
am.currency_id != cp.currency_id as is_multicurrency,
p.name as partner_name,
acc.code as account_code,
{acc_name} as account_name,
acc.account_type as account_type,
COALESCE("account_move_line".debit, 0) as debit,
COALESCE("account_move_line".credit, 0) as credit,
COALESCE("account_move_line".balance, 0) as balance,
{j_name} as journal_name,
j.code as journal_code,
j.type as journal_type,
j.currency_id as journal_currency,
journal_curr.name as journal_currency_name,
cp.currency_id as company_currency,
CASE WHEN j.type = 'sale' THEN am.payment_reference WHEN j.type = 'purchase' THEN am.ref ELSE '' END as reference,
array_remove(array_agg(DISTINCT {tax_name}), NULL) as taxes,
array_remove(array_agg(DISTINCT {tag_name}), NULL) as tax_grids
FROM {tables}
JOIN account_move am ON am.id = "account_move_line".move_id
JOIN account_account acc ON acc.id = "account_move_line".account_id
LEFT JOIN res_partner p ON p.id = "account_move_line".partner_id
JOIN account_journal j ON j.id = am.journal_id
JOIN res_company cp ON cp.id = am.company_id
LEFT JOIN account_move_line_account_tax_rel aml_at_rel ON aml_at_rel.account_move_line_id = "account_move_line".id
LEFT JOIN account_tax parent_tax ON parent_tax.id = aml_at_rel.account_tax_id and parent_tax.amount_type = 'group'
LEFT JOIN account_tax_filiation_rel tax_filiation_rel ON tax_filiation_rel.parent_tax = parent_tax.id
LEFT JOIN account_tax tax ON (tax.id = aml_at_rel.account_tax_id and tax.amount_type != 'group') or tax.id = tax_filiation_rel.child_tax
LEFT JOIN account_account_tag_account_move_line_rel tag_rel ON tag_rel.account_move_line_id = "account_move_line".id
LEFT JOIN account_account_tag tag on tag_rel.account_account_tag_id = tag.id
LEFT JOIN res_currency journal_curr on journal_curr.id = j.currency_id
WHERE {where_clause}
GROUP BY "account_move_line".id, am.id, p.id, acc.id, j.id, cp.id, journal_curr.id
ORDER BY j.id, CASE when am.name = '/' then 1 else 0 end,
{" am.date, am.name," if sort_by_date else " am.name , am.date,"}
CASE acc.account_type
WHEN 'liability_payable' THEN 1
WHEN 'asset_receivable' THEN 1
WHEN 'liability_credit_card' THEN 5
WHEN 'asset_cash' THEN 5
ELSE 2
END,
"account_move_line".tax_line_id NULLS FIRST
LIMIT %s
OFFSET %s
""")
# 1.2.Fetch data from DB
rslt = {}
self._cr.execute('(' + ') UNION ALL ('.join(queries) + ')', params)
for aml_result in self._cr.dictfetchall():
rslt.setdefault(aml_result['move_line_id'], {col_group_key: {} for col_group_key in options['column_groups']})
rslt[aml_result['move_line_id']][aml_result['column_group_key']] = aml_result
return rslt
def _query_months(self, options, line_id=False, offset=0, journal=False):
params = []
queries = []
report = self.env.ref('account_reports.journal_report')
for column_group_key, options_group in report._split_options_per_column_group(options).items():
tables, where_clause, where_params = report._query_get(options_group, 'strict_range', domain=[('journal_id', '=', journal.id)])
params.append(column_group_key)
params += where_params
# Fetch all months for which we have any move lines, ordered chronologically.
queries.append(f"""
(WITH aml_by_months AS (
SELECT DISTINCT ON (to_char("account_move_line".date, 'MM YYYY')) to_char("account_move_line".date, 'MM YYYY') AS month, to_char("account_move_line".date, 'fmMon YYYY') AS display_month, %s as column_group_key, "account_move_line".date
FROM {tables}
WHERE {where_clause}
)
SELECT column_group_key, month, display_month
FROM aml_by_months
ORDER BY date)
""")
# 1.2.Fetch data from DB
self._cr.execute(" UNION ALL ".join(queries), params)
rslt = {}
for aml_result in self._cr.dictfetchall():
rslt.setdefault(aml_result['month'], {col_group_key: {} for col_group_key in options['column_groups']})
rslt[aml_result['month']][aml_result['column_group_key']] = aml_result
return rslt
def _get_tax_grids_summary(self, options, data):
"""
Fetches the details of all grids that have been used in the provided journal.
The result is grouped by the country in which the tag exists in case of multivat environment.
Returns a dictionary with the following structure:
{
Country : {
tag_name: {+, -, impact},
tag_name: {+, -, impact},
tag_name: {+, -, impact},
...
},
Country : [
tag_name: {+, -, impact},
tag_name: {+, -, impact},
tag_name: {+, -, impact},
...
],
...
}
"""
report = self.env.ref('account_reports.journal_report')
# Use the same option as we use to get the tax details, but this time to generate the query used to fetch the
# grid information
tax_report_options = self._get_generic_tax_report_options(options, data)
tables, where_clause, where_params = report._query_get(tax_report_options, 'strict_range')
lang = self.env.user.lang or get_lang(self.env).code
country_name = f"COALESCE(country.name->>'{lang}', country.name->>'en_US')"
tag_name = f"COALESCE(tag.name->>'{lang}', tag.name->>'en_US')" if \
self.pool['account.account.tag'].name.translate else 'tag.name'
query = f"""
WITH tag_info (country_name, tag_id, tag_name, tag_sign, balance) as (
SELECT
{country_name} AS country_name,
tag.id,
{tag_name} AS name,
CASE WHEN tag.tax_negate IS TRUE THEN '-' ELSE '+' END,
SUM(COALESCE("account_move_line".balance, 0)
* CASE WHEN "account_move_line".tax_tag_invert THEN -1 ELSE 1 END
) AS balance
FROM account_account_tag tag
JOIN account_account_tag_account_move_line_rel rel ON tag.id = rel.account_account_tag_id
JOIN res_country country on country.id = tag.country_id
, {tables}
WHERE {where_clause}
AND applicability = 'taxes'
AND "account_move_line".id = rel.account_move_line_id
GROUP BY country_name, tag.id
)
SELECT
country_name,
tag_id,
REGEXP_REPLACE(tag_name, '^[+-]', '') AS name, -- Remove the sign from the grid name
balance,
tag_sign AS sign
FROM tag_info
ORDER BY country_name, name
"""
self._cr.execute(query, where_params)
query_res = self.env.cr.fetchall()
res = defaultdict(lambda: defaultdict(dict))
opposite = {'+': '-', '-': '+'}
for country_name, tag_id, name, balance, sign in query_res:
res[country_name][name].setdefault('tag_id', []).append(tag_id)
res[country_name][name][sign] = report._format_value(options, balance, blank_if_zero=False, figure_type='monetary')
# We need them formatted, to ensure they are displayed correctly in the report. (E.g. 0.0, not 0)
if not opposite[sign] in res[country_name][name]:
res[country_name][name][opposite[sign]] = report._format_value(options, 0, blank_if_zero=False, figure_type='monetary')
res[country_name][name][sign + '_no_format'] = balance
res[country_name][name]['impact'] = report._format_value(options, res[country_name][name].get('+_no_format', 0) - res[country_name][name].get('-_no_format', 0), blank_if_zero=False, figure_type='monetary')
return res
def _get_generic_tax_summary_for_sections(self, options, data):
"""
Overridden to make use of the generic tax report computation
Works by forcing specific options into the tax report to only get the lines we need.
The result is grouped by the country in which the tag exists in case of multivat environment.
Returns a dictionary with the following structure:
{
Country : [
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
...
],
Country : [
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
{name, base_amount, tax_amount, tax_non_deductible{_no_format}, tax_deductible{_no_format}, tax_due{_no_format}},
...
],
...
}
"""
report = self.env['account.report'].browse(options['report_id'])
tax_report_options = self._get_generic_tax_report_options(options, data)
tax_report_options['account_journal_report_tax_deductibility_columns'] = True
tax_report = self.env.ref('account.generic_tax_report')
tax_report_lines = tax_report._get_lines(tax_report_options)
tax_values = {}
for tax_report_line in tax_report_lines:
model, line_id = report._parse_line_id(tax_report_line.get('id'))[-1][1:]
if model == 'account.tax':
tax_values[line_id] = {
'base_amount': tax_report_line['columns'][0]['no_format'],
'tax_amount': tax_report_line['columns'][1]['no_format'],
'tax_non_deductible': tax_report_line['columns'][2]['no_format'],
'tax_deductible': tax_report_line['columns'][3]['no_format'],
'tax_due': tax_report_line['columns'][4]['no_format'],
}
# Make the final data dict that will be used by the template, using the taxes information.
taxes = self.env['account.tax'].browse(tax_values.keys())
res = defaultdict(list)
for tax in taxes:
res[tax.country_id.name].append({
'base_amount': report._format_value(options, tax_values[tax.id]['base_amount'], blank_if_zero=False, figure_type='monetary'),
'tax_amount': report._format_value(options, tax_values[tax.id]['tax_amount'], blank_if_zero=False, figure_type='monetary'),
'tax_non_deductible': report._format_value(options, tax_values[tax.id]['tax_non_deductible'], blank_if_zero=False, figure_type='monetary'),
'tax_non_deductible_no_format': tax_values[tax.id]['tax_non_deductible'],
'tax_deductible': report._format_value(options, tax_values[tax.id]['tax_deductible'], blank_if_zero=False, figure_type='monetary'),
'tax_deductible_no_format': tax_values[tax.id]['tax_deductible'],
'tax_due': report._format_value(options, tax_values[tax.id]['tax_due'], blank_if_zero=False, figure_type='monetary'),
'tax_due_no_format': tax_values[tax.id]['tax_due'],
'name': tax.name,
'line_id': report._get_generic_line_id('account.tax', tax.id)
})
# Return the result, ordered by country name
return dict(sorted(res.items()))
####################################################
# Actions
####################################################
def journal_report_tax_tag_template_open_aml(self, options, params=None):
""" returns an action to open a tree view of the account.move.line having the selected tax tag """
tag_id = params.get('tag_id')
# When grouped by month, we don't use the report dates directly, but the ones of the month. So they need to replace the ones in the options.
new_options = options.copy()
new_options['date'].update({
'date_from': params and params.get('date_from') or options.get('date', {}).get('date_from'),
'date_to': params and params.get('date_to') or options.get('date', {}).get('date_to'),
})
domain = self.env['account.report'].browse(options['report_id'])._get_options_domain(new_options, 'strict_range') + [('tax_tag_ids', 'in', tag_id)] + self.env['account.move.line']._get_tax_exigible_domain()
return {
'type': 'ir.actions.act_window',
'name': _('Journal Items for Tax Audit'),
'res_model': 'account.move.line',
'views': [[self.env.ref('account.view_move_line_tax_audit_tree').id, 'list']],
'domain': domain,
'context': self.env.context,
}
def journal_report_action_dropdown_audit_default_tax_report(self, options, params):
# See above
new_options = options.copy()
new_options['date'].update({
'date_from': params and params.get('date_from') or options.get('date', {}).get('date_from'),
'date_to': params and params.get('date_to') or options.get('date', {}).get('date_to'),
})
return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(new_options, params)
def journal_report_action_open_tax_journal_items(self, options, params):
"""
Open the journal items related to the tax on this line.
Take into account the given/options date and group by taxes then account.
:param options: the report options.
:param params: a dict containing the line params. (Dates, name, journal_id, tax_type)
:return: act_window on journal items grouped by tax or tags and accounts.
"""
ctx = {
'search_default_posted': 0 if options.get('all_entries') else 1,
'search_default_date_between': 1,
'date_from': params and params.get('date_from') or options.get('date', {}).get('date_from'),
'date_to': params and params.get('date_to') or options.get('date', {}).get('date_to'),
'search_default_journal_id': params.get('journal_id'),
'expand': 1,
}
if params and params.get('tax_type') == 'tag':
ctx.update({
'search_default_group_by_tax_tags': 1,
'search_default_group_by_account': 2,
})
elif params and params.get('tax_type') == 'tax':
ctx.update({
'search_default_group_by_taxes': 1,
'search_default_group_by_account': 2,
})
if params and 'journal_id' in params:
ctx.update({
'search_default_journal_id': [params['journal_id']],
})
if options and options.get('journals') and 'search_default_journal_id' not in ctx:
selected_journals = [journal['id'] for journal in options['journals'] if journal.get('selected')]
if len(selected_journals) == 1:
ctx['search_default_journal_id'] = selected_journals
return {
'name': params.get('name'),
'view_mode': 'tree,pivot,graph,kanban',
'res_model': 'account.move.line',
'views': [(self.env.ref('account.view_move_line_tree').id, 'list')],
'type': 'ir.actions.act_window',
'domain': [('display_type', 'not in', ('line_section', 'line_note'))],
'context': ctx,
}
def open_journal_items(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
action = report.open_journal_items(options=options, params=params)
action.get('context', {}).update({'search_default_group_by_account': 0, 'search_default_group_by_move': 1})
return action
def journal_report_action_open_business_doc(self, options, params):
model, record_id = self.env['account.report']._get_model_info_from_id(params['line_id'])
record = self.env[model].browse(record_id)
if record._name in ['account.move', 'account.move.line']:
return record.action_open_business_doc()
raise UserError(_("The selected report line does not target a Journal Entry or a Journal Item."))