1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_consolidation/report/builder/abstract.py
2024-12-10 09:04:09 +07:00

423 lines
18 KiB
Python

from abc import ABC
from odoo import _
from odoo.tools.float_utils import float_is_zero
from ..handler.show_zero import ShowZeroHandler
class AbstractBuilder(ABC):
def __init__(self, env, value_formatter):
"""
Instantiate a builder that will be responsible to create the report lines.
:param env: the env object in which this builder is used
:param value_formatter: a function that will be used to format float values in report
"""
self.env = env
self.value_formatter = value_formatter
def _get_lines(self, period_ids: list, options: dict, line_id: str = None) -> list:
"""
Return the report lines based on selected period ids, the report options and the line from which the report is
print.
:param period_ids: list containing the ids of the selected periods
:type period_ids: list
:param options: options of the report
:type options: dict
:param line_id: the id of the line from which the report is print (or None if whole report is print)
:type line_id: str
:return: list of dict representing the report lines
:rtype:list
"""
if self._output_will_be_empty(period_ids, options, line_id):
return []
params = self._get_params(period_ids, options, line_id)
if options.get('consolidation_hierarchy'):
return self._get_hierarchy(options, line_id, **params)
else:
return self._get_plain(options, **params)
def _output_will_be_empty(self, period_ids: list, options: dict, line_id: str = None) -> bool:
"""
Determine with the initial parameters given to the builder if the output would be empty.
:param period_ids: the period ids used to generate the report
:type period_ids: list
:param options: options of the report
:type options: dict
:param line_id: the line id from which this method is called (when you unfold a line)
:type line_id: str
:return: True if the result will be empty in any case, False otherwise
:type: bool
"""
return False
def _get_params(self, period_ids: list, options: dict, line_id: str = None) -> dict:
"""
Get the parameters to give to call stack for the builder. It's mainly useful for children overriding so that
needed parameters are propagated to all other methods called.
:param period_ids: the period ids used to generate the report
:type period_ids: list
:param options: options of the report
:type options: dict
:param line_id: the line id from which this method is called (when you unfold a line)
:type line_id: str
:return: a dict of parameters useful for all other methods of the builder to work correctly
:type: dict
"""
return {
'period_ids': period_ids
}
def _get_plain(self, options: dict, **kwargs) -> list:
"""
Return the report lines without any hierarchy. It loads all the accounts of the chart and process them all.
:param options: options of the report
:type options: dict
:return: list of dict representing the report lines
:rtype: list
"""
accounts = self._get_all_accounts(options, **kwargs)
totals, lines = self._handle_accounts(accounts, options, 3, **kwargs)
if totals:
lines.append(self._build_total_line(totals, options, **kwargs))
return lines
def _get_hierarchy(self, options: dict, line_id: str = None, **kwargs) -> list:
"""
Return the report lines with the proper hierarchy. These are the main steps of the algorithm :
1) - If no line_id is given, it starts by getting all accounts without parent ("orphans") and process them to
get their totals and the lines to add in the output. Then it fetches all the sections without parents
("root sections"), these will be used later in the algorithm.
- If a line_id is given, then the line_id corresponds to a line of a section and is formatted with
account_reports._get_generic_line_id(). It parses this ID and get the section with is used later in the algorithm.
2) It processes recursively the children accounts/sections of the computed sections in step 1.
3) It computes and add the final total line if line_id is not given
:param options: options of the report
:type options: dict
:param line_id: the line id from which this method is called (when you unfold a line)
:type line_id: str
:return: list of dict representing the report lines
:rtype: list
"""
super_totals = None
lines = []
if line_id is None:
# HANDLE ORPHAN ACCOUNTS
level = 0
orphan_totals, orphan_lines = self._handle_orphan_accounts(options, level, **kwargs)
super_totals = [x + y for x, y in zip(super_totals, orphan_totals)] if super_totals is not None else orphan_totals
lines += orphan_lines
# FETCH ALL ROOT SECTIONS
sections = self._get_root_sections(options, **kwargs)
else:
from_section = self.env['consolidation.group'].browse(int(line_id.split('-')[1]))
level = len(from_section.parent_path.split('/'))
# For convenience in the following
sections = [from_section]
# PROCESS COMPUTED SECTIONS
if len(sections) > 0:
section_totals, section_lines = self._handle_sections(sections, options, level, **kwargs)
super_totals = [x + y for x, y in zip(super_totals, section_totals)] if super_totals is not None else section_totals
lines += section_lines
if line_id is None and super_totals:
lines.append(self._build_total_line(super_totals, options, **kwargs))
return lines
def _handle_sections(self, sections, options: dict, level: int, **kwargs) -> tuple:
"""
Handle the creation of given sections lines
:param sections: a list or recordset of section objects
:param options: options of the report
:type options: dict
:param level: the level of the line (to allow indentation to be kept)
:type level: int
:return: A couple (totals, lines) where :
- totals the list of the column totals
- lines the list of generated report line corresponding to given sections
:rtype: tuple
"""
all_totals = None
lines = []
for section in sections:
section_totals, section_lines = self._build_section_line(section, level, options, **kwargs)
# Handle TOTALS
all_totals = [x + y for x, y in zip(all_totals, section_totals)] if all_totals is not None else section_totals
# Handle LINES
if ShowZeroHandler.section_line_should_be_added(section_lines, options):
lines += section_lines
return all_totals, lines
def _handle_orphan_accounts(self, options: dict, level: int = 1, **kwargs) -> tuple:
"""
Handle the creation of all orphan account lines for given charts
:param options: options of the report
:type options: dict
:param level: the level of the line (to allow indentation to be kept)
:type level: int
:return: A couple (totals, lines) where :
- totals the list of the column totals
- lines the list of generated report line corresponding to orphan accounts
:rtype: tuple
"""
orphan_accounts = self._get_orphan_accounts(options, **kwargs)
return self._handle_accounts(orphan_accounts, options, level, **kwargs)
def _handle_accounts(self, accounts, options: dict, level: int, **kwargs) -> tuple:
"""
Handle the creation of report lines for given accounts
:param accounts: a recordset containing all the accounts to handle
:param options: options of the report
:type options: dict
:param level: the level of the line (to allow indentation to be kept)
:type level: int
:return: A couple (totals, lines) where :
- totals the list of the column totals
- lines the list of generated report line corresponding to given accounts
:rtype: tuple
"""
all_totals = None
lines = []
if accounts and len(accounts) > 0:
for account in accounts:
totals = self._compute_account_totals(account, **kwargs)
if len(totals) > 0:
# Handle TOTALS
all_totals = [x + y for x, y in zip(all_totals, totals)] if all_totals is not None else totals
# Handle LINES
account_line = self._format_account_line(account, None, level, totals, options, **kwargs)
if self._account_line_is_shown(account_line, options):
lines.append(account_line)
return all_totals, lines
def _account_line_is_shown(self, account_line: dict, options: dict) -> bool:
"""
Determine if an account line should be shown
:param account_line: the account line
:type account_line: dict
:param options: options of the report
:type options: dict
:return: True if the account line should be shown, False otherwise
:rtype: bool
"""
return ShowZeroHandler.account_line_should_be_added(account_line, options)
def _get_all_accounts(self, options: dict, **kwargs):
"""
Get all consolidation accounts, filtered on given chart_ids if given in kwargs
:param options: options of the report
:type options: dict
:return: a recordset of all accounts found
"""
domain = []
if kwargs.get('chart_ids', False):
domain.append(('chart_id', 'in', kwargs['chart_ids']))
return self.env['consolidation.account'].search(domain)
def _get_root_sections(self, options: dict, **kwargs):
"""
Get all root sections (= without parent), filtered on given chart_ids if given in kwargs
:param options: options of the report
:type options: dict
:return: a recordset of all root sections found
"""
domain = [('parent_id', '=', False)]
if kwargs.get('chart_ids', False):
domain.append(('chart_id', 'in', kwargs['chart_ids']))
return self.env['consolidation.group'].search(domain)
def _get_orphan_accounts(self, options: dict, **kwargs):
"""
Get all orphan accounts (= not attached to a section) for given consolidation charts
:param options: options of the report
:type options: dict
:return: a recordset of all orphan accounts found
"""
domain = [('group_id', '=', False)]
if kwargs.get('chart_ids', False):
domain.append(('chart_id', 'in', kwargs['chart_ids']))
return self.env['consolidation.account'].search(domain)
def _compute_account_totals(self, account, **kwargs) -> list:
"""
Compute the totals for a given consolidation account and given periods
:param account_id: the id of the consolidation account
:param periods: a recordset containing the periods
:return: a list of float representing the totals for the account, first cell is for first column, second cell
for second column, ...
"""
return []
def _format_account_line(self, account, parent_id, level: int, totals: list, options: dict, **kwargs) -> dict:
"""
Build an account line.
:param account: the account object
:param level: the level of the line (to allow indentation to be kept)
:type level: int
:param totals: the already computed totals for the account
:param options: options of the report
:type options: dict
:return: a formatted dict representing the account line
:rtype: dict
"""
# Columns
cols = [{
'name': self.value_formatter(total),
'no_format': total,
'figure_type': 'monetary',
'class': 'number' + (' muted' if float_is_zero(total, 6) else ''),
'is_zero': not total,
'auditable': True,
}
for total in totals]
# The last column 'total' must not be auditable
cols[-1]['auditable'] = False
report = self.env['account.report'].browse(options['report_id'])
# Line
name = account.display_name
if account.group_id:
account_line_id = report._get_generic_line_id(None, None, markup=f'{account.id}', parent_line_id=parent_id)
account_line_parent_id = parent_id
else:
account_line_id = report._get_generic_line_id(None, None, markup=account.id)
account_line_parent_id = None
account_line = {
'id': account_line_id,
'name': len(name) > 40 and options['export_mode'] != 'print' and name[:40] + '...' or name,
'title_hover': _("%s (%s Currency Conversion Method)") % (account.name, account.get_display_currency_mode()),
'columns': cols,
'level': level,
}
if account_line_parent_id:
account_line['parent_id'] = account_line_parent_id
return account_line
def _build_section_line(self, section, level: int, options: dict, **kwargs) -> tuple:
"""
Build a section line and all its descendants lines (if any).
:param section: the section object
:param level: the level of the line (to allow indentation to be kept)
:type level: int
:param options: options of the report
:type options: dict
:return: a tuple with :
- a list of formatted dict containing the section line itself and all the descendant lines of this
(so that the section line is the first dict of the list)
- the totals of the section line
:rtype: tuple
"""
report = self.env['account.report'].browse(options['report_id'])
section_parent_id = ''
parent_line_id = section.parent_id
while parent_line_id:
new_section_parent_id = report._get_generic_line_id(None, None, markup=f'section_{parent_line_id.id}')
if section_parent_id:
section_parent_id = report._build_subline_id(new_section_parent_id, report._build_line_id(report._parse_line_id(section_parent_id)[1:]))
else:
section_parent_id = new_section_parent_id
parent_line_id = parent_line_id.parent_id
section_id = report._get_generic_line_id(None, None, markup=f'section_{section.id}', parent_line_id=section_parent_id)
section_line = {
'id': section_id,
'name': section.name,
'level': level,
'unfoldable': True,
'unfolded': options.get('unfold_all', False) or section_id in options.get('unfolded_lines', []),
}
if section_parent_id:
section_line['parent_id'] = section_parent_id
lines = [section_line]
if not level:
level += 1
# HANDLE CHILDREN
section_totals = None
if len(section.child_ids) > 0:
for child_section in section.child_ids:
# This will return the section line THEN all subsequent lines
child_totals, descendant_lines = self._build_section_line(child_section, level + 2, options, **kwargs)
section_totals = [x + y for x, y in zip(section_totals, child_totals)] if section_totals is not None else child_totals
if ShowZeroHandler.section_line_should_be_added(descendant_lines, options):
lines += descendant_lines
# HANDLE ACCOUNTS
if len(section.account_ids) > 0:
for child_account in section.account_ids:
account_totals = self._compute_account_totals(child_account, **kwargs)
account_line = self._format_account_line(child_account, section_id, level + 2, account_totals, options, **kwargs)
section_totals = [x + y for x, y in zip(section_totals, account_totals)] if section_totals is not None else account_totals
if ShowZeroHandler.account_line_should_be_added(account_line, options):
lines.append(account_line)
if section_totals is None:
section_totals = self._get_default_line_totals(options, **kwargs)
section_line['columns'] = [{
'name': self.value_formatter(total),
'no_format': total,
'figure_type': 'monetary',
'is_zero': not total,
'class': 'number' + (' muted' if float_is_zero(total, 6) else ''),
} for total in section_totals]
return section_totals, lines
def _build_total_line(self, totals: list, options: dict, **kwargs) -> dict:
"""
Build the total line, based on given totals list. Values are formatted using self value formatter.
:param totals: the list of totals amounts
:type totals: list
:param options: options of the report
:type options: dict
:return: a formatted dict representing the total line to be displayed on report
:rtype: dict
"""
cols = [{
'name': self.value_formatter(total), 'no_format': total,
'class': 'number' + (' text-danger' if not float_is_zero(total, 6) else ''),
'figure_type': 'monetary',
} for total in totals]
report = self.env['account.report'].browse(options['report_id'])
return {
'id': report._get_generic_line_id(None, None, markup='grouped_accounts_total'),
'name': _('Total'),
'class': 'total',
'columns': cols,
'level': 0,
}
def _get_default_line_totals(self, options: dict, **kwargs) -> list:
"""
Create a line with default values, this is called when no values have been found to create a line.
:param options: options of the report
:type options: dict
:return: a list of float representing the default values of a line
:rtype: list
"""
return []