forked from Mapan/odoo17e
423 lines
18 KiB
Python
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 []
|