# Fusion Accounting - Trial Balance Report Handler from odoo import api, models, _, fields from odoo.tools import float_compare from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT # Sentinel key used for end-balance columns which are computed client-side # and never generate their own SQL column group. _END_COL_GROUP_SENTINEL = '_trial_balance_end_column_group' class TrialBalanceCustomHandler(models.AbstractModel): """Wraps the General Ledger handler to produce a Trial Balance. The trial balance adds initial-balance and end-balance column groups around the regular period columns and collapses each account's detail into a single non-foldable row. """ _name = 'account.trial.balance.report.handler' _inherit = 'account.report.custom.handler' _description = 'Trial Balance Custom Handler' # ------------------------------------------------------------------ # Dynamic lines # ------------------------------------------------------------------ def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None): """Delegate to the GL handler and then post-process rows to collapse debit/credit, compute end-balance columns, and remove expand functions.""" def _set_cell(row, idx, amount): row['columns'][idx]['no_format'] = amount row['columns'][idx]['is_zero'] = self.env.company.currency_id.is_zero(amount) def _collapse_debit_credit(row, dr_idx, cr_idx, bal_idx=None): """Net debit and credit: whichever is larger keeps the difference; the other becomes zero. Optionally write balance too.""" dr_val = row['columns'][dr_idx]['no_format'] if dr_idx is not None else False cr_val = row['columns'][cr_idx]['no_format'] if cr_idx is not None else False if dr_val and cr_val: cmp = self.env.company.currency_id.compare_amounts(dr_val, cr_val) if cmp == 1: _set_cell(row, dr_idx, dr_val - cr_val) _set_cell(row, cr_idx, 0.0) else: _set_cell(row, dr_idx, 0.0) _set_cell(row, cr_idx, (dr_val - cr_val) * -1) if bal_idx is not None: _set_cell(row, bal_idx, dr_val - cr_val) # Obtain raw GL lines gl_handler = self.env['account.general.ledger.report.handler'] raw = [ row[1] for row in gl_handler._dynamic_lines_generator( report, options, all_column_groups_expression_totals, warnings=warnings, ) ] # Locate column indices for initial / end balance col_defs = options['columns'] init_dr = next((i for i, c in enumerate(col_defs) if c.get('expression_label') == 'debit'), None) init_cr = next((i for i, c in enumerate(col_defs) if c.get('expression_label') == 'credit'), None) end_dr = next((i for i, c in enumerate(col_defs) if c.get('expression_label') == 'debit' and c.get('column_group_key') == _END_COL_GROUP_SENTINEL), None) end_cr = next((i for i, c in enumerate(col_defs) if c.get('expression_label') == 'credit' and c.get('column_group_key') == _END_COL_GROUP_SENTINEL), None) end_bal = next((i for i, c in enumerate(col_defs) if c.get('expression_label') == 'balance' and c.get('column_group_key') == _END_COL_GROUP_SENTINEL), None) cur = self.env.company.currency_id # Process every account row (all except the last = total line) for row in raw[:-1]: _collapse_debit_credit(row, init_dr, init_cr) # End balance = sum of all debit columns except the end one itself if end_dr is not None: dr_sum = sum( cur.round(cell['no_format']) for idx, cell in enumerate(row['columns']) if cell.get('expression_label') == 'debit' and idx != end_dr and cell['no_format'] is not None ) _set_cell(row, end_dr, dr_sum) if end_cr is not None: cr_sum = sum( cur.round(cell['no_format']) for idx, cell in enumerate(row['columns']) if cell.get('expression_label') == 'credit' and idx != end_cr and cell['no_format'] is not None ) _set_cell(row, end_cr, cr_sum) _collapse_debit_credit(row, end_dr, end_cr, end_bal) # Remove GL expand-related keys row.pop('expand_function', None) row.pop('groupby', None) row['unfoldable'] = False row['unfolded'] = False mdl = report._get_model_info_from_id(row['id'])[0] if mdl == 'account.account': row['caret_options'] = 'trial_balance' # Recompute totals on the total line if raw: total_row = raw[-1] for idx in (init_dr, init_cr, end_dr, end_cr): if idx is not None: total_row['columns'][idx]['no_format'] = sum( cur.round(r['columns'][idx]['no_format']) for r in raw[:-1] if report._get_model_info_from_id(r['id'])[0] == 'account.account' ) return [(0, row) for row in raw] # ------------------------------------------------------------------ # Caret options # ------------------------------------------------------------------ def _caret_options_initializer(self): return { 'trial_balance': [ {'name': _("General Ledger"), 'action': 'caret_option_open_general_ledger'}, {'name': _("Journal Items"), 'action': 'open_journal_items'}, ], } # ------------------------------------------------------------------ # Column group management # ------------------------------------------------------------------ def _get_column_group_creation_data(self, report, options, previous_options=None): """Declare which extra column groups to add and on which side of the report they appear.""" return ( (self._build_initial_balance_col_group, 'left'), (self._build_end_balance_col_group, 'right'), ) @api.model def _create_and_append_column_group( self, report, options, header_label, forced_opts, side, group_vals, exclude_initial_balance=False, append_col_groups=True, ): """Helper: generate a new column group and append it to *side*.""" header_elem = [{'name': header_label, 'forced_options': forced_opts}] full_headers = [header_elem, *options['column_headers'][1:]] cg_vals = report._generate_columns_group_vals_recursively(full_headers, group_vals) if exclude_initial_balance: for cg in cg_vals: cg['forced_options']['general_ledger_strict_range'] = True cols, col_groups = report._build_columns_from_column_group_vals(forced_opts, cg_vals) side['column_headers'] += header_elem if append_col_groups: side['column_groups'] |= col_groups side['columns'] += cols # ------------------------------------------------------------------ # Options # ------------------------------------------------------------------ def _custom_options_initializer(self, report, options, previous_options): """Insert initial-balance and end-balance column groups around the standard period columns.""" default_gv = {'horizontal_groupby_element': {}, 'forced_options': {}} lhs = {'column_headers': [], 'column_groups': {}, 'columns': []} rhs = {'column_headers': [], 'column_groups': {}, 'columns': []} # Mid-period columns should use strict range for cg in options['column_groups'].values(): cg['forced_options']['general_ledger_strict_range'] = True if options.get('comparison') and not options['comparison'].get('periods'): options['comparison']['period_order'] = 'ascending' for factory_fn, side_label in self._get_column_group_creation_data(report, options, previous_options): target = lhs if side_label == 'left' else rhs factory_fn(report, options, previous_options, default_gv, target) options['column_headers'][0] = lhs['column_headers'] + options['column_headers'][0] + rhs['column_headers'] options['column_groups'].update(lhs['column_groups']) options['column_groups'].update(rhs['column_groups']) options['columns'] = lhs['columns'] + options['columns'] + rhs['columns'] options['ignore_totals_below_sections'] = True # Force a shared currency-table period for all middle columns shared_period_key = '_trial_balance_middle_periods' for cg in options['column_groups'].values(): dt = cg['forced_options'].get('date') if dt: dt['currency_table_period_key'] = shared_period_key report._init_options_order_column(options, previous_options) def _custom_line_postprocessor(self, report, options, lines): """Add contrast styling to hierarchy group lines when hierarchy is enabled.""" if options.get('hierarchy'): for ln in lines: mdl, _ = report._get_model_info_from_id(ln['id']) if mdl == 'account.group': ln['class'] = ln.get('class', '') + ' o_account_coa_column_contrast_hierarchy' return lines # ------------------------------------------------------------------ # Column group builders # ------------------------------------------------------------------ def _build_initial_balance_col_group(self, report, options, previous_options, default_gv, side): """Create the Initial Balance column group on the left.""" gl_handler = self.env['account.general.ledger.report.handler'] init_opts = gl_handler._get_options_initial_balance(options) forced = { 'date': init_opts['date'], 'include_current_year_in_unaff_earnings': init_opts['include_current_year_in_unaff_earnings'], 'no_impact_on_currency_table': True, } self._create_and_append_column_group( report, options, _("Initial Balance"), forced, side, default_gv, ) def _build_end_balance_col_group(self, report, options, previous_options, default_gv, side): """Create the End Balance column group on the right. No actual SQL is run for this group; its values are computed by summing all other groups during line post-processing. """ to_dt = options['date']['date_to'] from_dt = ( options['comparison']['periods'][-1]['date_from'] if options.get('comparison', {}).get('periods') else options['date']['date_from'] ) forced = { 'date': report._get_dates_period( fields.Date.from_string(from_dt), fields.Date.from_string(to_dt), 'range', ), } self._create_and_append_column_group( report, options, _("End Balance"), forced, side, default_gv, append_col_groups=False, ) # Mark end-balance columns with the sentinel key num_report_cols = len(report.column_ids) for col in side['columns'][-num_report_cols:]: col['column_group_key'] = _END_COL_GROUP_SENTINEL