From d56be74d87be8f6adebbeb812dc4fc0f5da104c6 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Tue, 20 Jan 2026 13:15:43 +0700 Subject: [PATCH] feat: Implement 'Show All Accounts' in reports by adding missing non-summary accounts and refining general ledger balance detection logic. --- models/account_general_ledger.py | 42 ++++---- models/account_report.py | 166 ++++++++++++++++++++++++++++--- 2 files changed, 176 insertions(+), 32 deletions(-) diff --git a/models/account_general_ledger.py b/models/account_general_ledger.py index 9a75e98..7875519 100644 --- a/models/account_general_ledger.py +++ b/models/account_general_ledger.py @@ -14,31 +14,33 @@ class GeneralLedgerCustomHandler(models.AbstractModel): for account, values in results: if account.code and account.code.endswith('0'): has_balance = False + + # Check ALL column groups for col_group_key, col_group_values in values.items(): - # Check if this column group is strict range (period activity) or cumulative (balance sheet) - col_group_options = options.get('column_groups', {}).get(col_group_key, {}) - forced_options = col_group_options.get('forced_options', {}) - is_strict_range = forced_options.get('general_ledger_strict_range') or options.get('general_ledger_strict_range') + # For GL, we usually care if there is ANY activity or balance in the period/range displayed. + + # Check 'sum' (Period Activity/Balance for the column) + sum_vals = col_group_values.get('sum', {}) + if not self.env.company.currency_id.is_zero(sum_vals.get('balance', 0.0)) or \ + not self.env.company.currency_id.is_zero(sum_vals.get('debit', 0.0)) or \ + not self.env.company.currency_id.is_zero(sum_vals.get('credit', 0.0)): + has_balance = True + break - for key in ['sum', 'initial_balance', 'unaffected_earnings']: - if key in col_group_values: - # 1. Always check Balance - if not self.env.company.currency_id.is_zero(col_group_values[key].get('balance', 0.0)): - has_balance = True - break - - # 2. If Strict Range (Period Activity), check Debit/Credit too - # If Cumulative, we ignore Debit/Credit because they are lifetime sums which might be non-zero even if balance is zero. - if is_strict_range: - if not self.env.company.currency_id.is_zero(col_group_values[key].get('debit', 0.0)) or \ - not self.env.company.currency_id.is_zero(col_group_values[key].get('credit', 0.0)): - has_balance = True - break - if has_balance: + # Check 'initial_balance' + init_vals = col_group_values.get('initial_balance', {}) + if not self.env.company.currency_id.is_zero(init_vals.get('balance', 0.0)): + has_balance = True + break + + # Check 'unaffected_earnings' (current year earnings usually) + unaff_vals = col_group_values.get('unaffected_earnings', {}) + if not self.env.company.currency_id.is_zero(unaff_vals.get('balance', 0.0)): + has_balance = True break if not has_balance: - continue # Skip this account + continue # Skip this account if no relevant numbers found filtered_results.append((account, values)) diff --git a/models/account_report.py b/models/account_report.py index 3939043..5ae51b3 100644 --- a/models/account_report.py +++ b/models/account_report.py @@ -7,25 +7,34 @@ class AccountReport(models.Model): # Get the standard lines lines = super()._get_lines(options, all_column_groups_expression_totals, warnings=warnings) - # Filter logic to hide accounts ending in '0' if they have zero balance + # 1. Filter Logic & Collection of Seen Accounts filtered_lines = [] + seen_account_ids = set() + + # We need a template line to construct missing account lines later + line_template = None + for line in lines: keep_line = True + is_account_line = line.get('caret_options') in ('account.account', 'trial_balance') - # Only check lines that represent accounts - if line.get('caret_options') == 'account.account': - # Attempt to get the code from the name. - # Account names usually start with the code (e.g., "110100 Stock Valuation") - name = line.get('name', '').strip() - code_part = name.split(' ')[0] + if is_account_line: + # Store potential template (first valid account line we find) + if not line_template: + line_template = line - # Check if code appears to be numeric and ends with '0' - # We interpret this as a summary/view account that should be hidden if unused + # Attempt to get the code + name = line.get('name', '').strip() + parts = name.split(' ', 1) + code_part = parts[0] if parts else '' + + # Logic: Hide if code ends with '0' AND has no balance if code_part.isdigit() and code_part.endswith('0'): has_balance = False for col in line.get('columns', []): - val = col.get('no_format', 0.0) - if val is None: val = 0.0 + val = col.get('no_format') + if val is None or val == '': + val = 0.0 if not self.env.company.currency_id.is_zero(val): has_balance = True @@ -33,8 +42,141 @@ class AccountReport(models.Model): if not has_balance: keep_line = False - + + # If we keep it, track the account ID + # Usually report lines for accounts have 'res_id' pointing to account.account id + if keep_line and line.get('res_model') == 'account.account': + seen_account_ids.add(line.get('res_id')) + elif keep_line: + # Fallback for complex IDs or different formats + line_id = line.get('id', '') + + # 1. Standard Odoo 'account.account_ID' + if line_id.startswith('account.account_'): + try: + acc_id = int(line_id.split('_')[-1]) + seen_account_ids.add(acc_id) + except: pass + + # 2. Complex Report Engine ID: e.g. ~account.report~12|~account.account~3743 + elif '~account.account~' in line_id: + try: + # Split by | and find the part with account.account + # This is a bit manual but robust enough for this format + parts = line_id.split('|') + for p in parts: + if '~account.account~' in p: + # p might be '~account.account~3743' + acc_id_str = p.split('~account.account~')[-1] + acc_id = int(acc_id_str) + seen_account_ids.add(acc_id) + except: pass + if keep_line: filtered_lines.append(line) + # 2. Add Missing Accounts ("Show All") + # Find accounts that are NOT in seen_account_ids and do NOT end in '0' + # (Accounts ending in '0' are "view" accounts, we don't want to force show them if they are empty) + + if line_template: + domain = [ + ('company_id', 'in', self.env.companies.ids), + ('id', 'not in', list(seen_account_ids)), + ('code', 'not like', '%0') + ] + missing_accounts = self.env['account.account'].search(domain) + + # Create a zero-column structure + zero_columns = [] + for col in line_template.get('columns', []): + # reconstruct a zero column + zero_col = {'name': '', 'no_format': 0.0, 'class': 'number'} + zero_columns.append(zero_col) + + # Separate existing lines into accounts and others (e.g. Total) + # The 'Total' line usually comes last. + # We want to insert our new accounts into the account list and sort them, + # but keep the Total at the bottom. + + account_lines = [] + other_lines = [] # Headers, Totals, Sections + + for line in filtered_lines: + if line.get('caret_options') in ('account.account', 'trial_balance') and not line.get('class') == 'total': + account_lines.append(line) + else: + other_lines.append(line) + + # Add missing accounts to account_lines + for account in missing_accounts: + new_line = { + 'id': f'account.account_{account.id}', + 'name': f'{account.code} {account.name}', + 'columns': zero_columns, + 'level': line_template.get('level', 2), + 'caret_options': 'account.account', + 'res_id': account.id, + 'res_model': 'account.account', + 'parent_id': line_template.get('parent_id'), + 'unfoldable': False, + 'unfolded': False, + } + account_lines.append(new_line) + + # Sort account lines by code + def get_code(l): + n = l.get('name', '').strip() + return n.split(' ')[0] + + account_lines.sort(key=get_code) + + # Reassemble: Account Lines first, then Total/Other lines + # NOTE: If 'other_lines' contains headers that should be at top, this logic might be too simple. + # But usually Trial Balance is simple. + # If GL has sections, this whole "Show All" logic is risky without knowing section structure. + # Assuming Trial Balance for now as per user request context. + # If 'other_lines' are at the end (Total), appending is correct. + # If 'other_lines' are at start, they need to be prepended. + # Heuristic: Check where they came from. + + # Re-construct based on original position roughly? + # Creating a list of (index, line) and sorting might work if we knew where to put new ones. + # But new ones belong in the "middle". + + # Safer Approach for TB: + # - Group headers/top lines + # - Group account lines + # - Group total/bottom lines + + # Simple Heuristic: 'Total' line usually has class='total' or similar. + # Let's assume other_lines are footer/total for TB. + + # However, check if any 'other_lines' appeared BEFORE the first account line. + passed_first_account = False + top_lines = [] + bottom_lines = [] + + # Re-scan filtered_lines to split into top/account/bottom + for line in filtered_lines: + is_acc = line.get('caret_options') in ('account.account', 'trial_balance') and not line.get('class') == 'total' + if is_acc: + passed_first_account = True + # account_lines already collected above (but we need to clear it and re-collect to be safe? + # No, we have account_lines populated above. + # Use the logic: + # If we haven't seen an account yet, it's a top line. + # If we have, and it's not an account, it's a bottom line? NOT ALWAYS (Sections). + pass + else: + if not passed_first_account: + top_lines.append(line) + else: + bottom_lines.append(line) + + # Re-sort the account_lines (which includes the newly added ones + existing ones) + account_lines.sort(key=get_code) + + filtered_lines = top_lines + account_lines + bottom_lines + return filtered_lines