Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,854 @@
# Fusion Accounting - Cash Flow Statement Report Handler
from odoo import models, _
from odoo.tools import SQL, Query
class CashFlowReportCustomHandler(models.AbstractModel):
"""Generates the cash flow statement using the direct method.
Reference: https://www.investopedia.com/terms/d/direct_method.asp
The handler fetches liquidity journal entries, splits them into
operating / investing / financing buckets based on account tags,
and renders both section totals and per-account detail rows.
"""
_name = 'account.cash.flow.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Cash Flow Report Custom Handler'
# ------------------------------------------------------------------
# Public entry points
# ------------------------------------------------------------------
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
"""Build every line of the cash flow statement.
Returns a list of ``(sequence, line_dict)`` tuples ready for the
report engine.
"""
output_lines = []
section_structure = self._build_section_structure()
computed_data = self._compute_report_data(report, options, section_structure)
# Render each section header
for section_key, section_meta in section_structure.items():
output_lines.append(
(0, self._render_section_line(report, options, section_key, section_meta, computed_data))
)
# Render detail rows grouped by account under this section
if section_key in computed_data and 'aml_groupby_account' in computed_data[section_key]:
detail_entries = computed_data[section_key]['aml_groupby_account'].values()
# Separate entries with / without an account code for sorting
coded_entries = [e for e in detail_entries if e['account_code'] is not None]
uncoded_entries = [e for e in detail_entries if e['account_code'] is None]
sorted_details = sorted(coded_entries, key=lambda r: r['account_code']) + uncoded_entries
for detail in sorted_details:
output_lines.append((0, self._render_detail_line(report, options, detail)))
# Append an unexplained-difference line when the numbers don't tie
diff_line = self._render_unexplained_difference(report, options, computed_data)
if diff_line:
output_lines.append((0, diff_line))
return output_lines
def _custom_options_initializer(self, report, options, previous_options):
"""Restrict selectable journals to bank, cash, and general types."""
super()._custom_options_initializer(report, options, previous_options=previous_options)
report._init_options_journals(
options,
previous_options=previous_options,
additional_journals_domain=[('type', 'in', ('bank', 'cash', 'general'))],
)
# ------------------------------------------------------------------
# Data computation
# ------------------------------------------------------------------
def _compute_report_data(self, report, options, section_structure):
"""Aggregate all cash-flow numbers into *report_data*.
The returned dictionary maps section keys (from
``_build_section_structure``) to balance and per-account detail
dictionaries.
"""
report_data = {}
liquidity_acct_ids = self._fetch_liquidity_account_ids(report, options)
if not liquidity_acct_ids:
return report_data
# Beginning-of-period balances
for row in self._query_liquidity_balances(report, options, liquidity_acct_ids, 'to_beginning_of_period'):
self._merge_into_report_data('opening_balance', row, section_structure, report_data)
self._merge_into_report_data('closing_balance', row, section_structure, report_data)
# Period movements
for row in self._query_liquidity_balances(report, options, liquidity_acct_ids, 'strict_range'):
self._merge_into_report_data('closing_balance', row, section_structure, report_data)
tag_map = self._resolve_cashflow_tags()
cf_tag_ids = self._list_cashflow_tag_ids()
# Liquidity-side entries
for grouped_rows in self._fetch_liquidity_side_entries(report, options, liquidity_acct_ids, cf_tag_ids):
for row_data in grouped_rows.values():
self._route_entry_to_section(tag_map, row_data, section_structure, report_data)
# Reconciled counterpart entries
for grouped_rows in self._fetch_reconciled_counterparts(report, options, liquidity_acct_ids, cf_tag_ids):
for row_data in grouped_rows.values():
self._route_entry_to_section(tag_map, row_data, section_structure, report_data)
return report_data
def _merge_into_report_data(self, section_key, row, section_structure, report_data):
"""Insert or accumulate *row* into *report_data* under *section_key*.
Also propagates the balance upward through parent sections so that
all ancestor totals stay correct.
The *report_data* dictionary uses two sub-keys per section:
* ``balance`` a ``{column_group_key: float}`` mapping
* ``aml_groupby_account`` per-account detail rows
"""
def _propagate_to_parent(sec_key, col_grp, amount, structure, data):
"""Walk the parent chain and add *amount* to every ancestor."""
parent_ref = structure[sec_key].get('parent_line_id')
if parent_ref:
data.setdefault(parent_ref, {'balance': {}})
data[parent_ref]['balance'].setdefault(col_grp, 0.0)
data[parent_ref]['balance'][col_grp] += amount
_propagate_to_parent(parent_ref, col_grp, amount, structure, data)
col_grp = row['column_group_key']
acct_id = row['account_id']
acct_code = row['account_code']
acct_label = row['account_name']
amt = row['balance']
tag_ref = row.get('account_tag_id')
if self.env.company.currency_id.is_zero(amt):
return
report_data.setdefault(section_key, {
'balance': {},
'aml_groupby_account': {},
})
report_data[section_key]['aml_groupby_account'].setdefault(acct_id, {
'parent_line_id': section_key,
'account_id': acct_id,
'account_code': acct_code,
'account_name': acct_label,
'account_tag_id': tag_ref,
'level': section_structure[section_key]['level'] + 1,
'balance': {},
})
report_data[section_key]['balance'].setdefault(col_grp, 0.0)
report_data[section_key]['balance'][col_grp] += amt
acct_entry = report_data[section_key]['aml_groupby_account'][acct_id]
acct_entry['balance'].setdefault(col_grp, 0.0)
acct_entry['balance'][col_grp] += amt
_propagate_to_parent(section_key, col_grp, amt, section_structure, report_data)
# ------------------------------------------------------------------
# Tag helpers
# ------------------------------------------------------------------
def _resolve_cashflow_tags(self):
"""Return a mapping of activity type to account.account.tag ID."""
return {
'operating': self.env.ref('account.account_tag_operating').id,
'investing': self.env.ref('account.account_tag_investing').id,
'financing': self.env.ref('account.account_tag_financing').id,
}
def _list_cashflow_tag_ids(self):
"""Return an iterable of all cash-flow-relevant tag IDs."""
return self._resolve_cashflow_tags().values()
def _route_entry_to_section(self, tag_map, entry, section_structure, report_data):
"""Determine the correct report section for a single entry and
merge it into *report_data*.
Receivable / payable lines go to advance-payment sections.
Other lines are classified by tag + sign (cash in vs cash out).
"""
acct_type = entry['account_account_type']
amt = entry['balance']
if acct_type == 'asset_receivable':
target = 'advance_payments_customer'
elif acct_type == 'liability_payable':
target = 'advance_payments_suppliers'
elif amt < 0:
tag_id = entry.get('account_tag_id')
if tag_id == tag_map['operating']:
target = 'paid_operating_activities'
elif tag_id == tag_map['investing']:
target = 'investing_activities_cash_out'
elif tag_id == tag_map['financing']:
target = 'financing_activities_cash_out'
else:
target = 'unclassified_activities_cash_out'
elif amt > 0:
tag_id = entry.get('account_tag_id')
if tag_id == tag_map['operating']:
target = 'received_operating_activities'
elif tag_id == tag_map['investing']:
target = 'investing_activities_cash_in'
elif tag_id == tag_map['financing']:
target = 'financing_activities_cash_in'
else:
target = 'unclassified_activities_cash_in'
else:
return
self._merge_into_report_data(target, entry, section_structure, report_data)
# ------------------------------------------------------------------
# SQL queries
# ------------------------------------------------------------------
def _fetch_liquidity_account_ids(self, report, options):
"""Return a tuple of account IDs used by liquidity journals.
Includes default accounts of bank/cash journals as well as any
payment-method-specific accounts.
"""
chosen_journal_ids = [j['id'] for j in report._get_options_journals(options)]
if chosen_journal_ids:
where_fragment = "aj.id IN %s"
where_args = [tuple(chosen_journal_ids)]
else:
where_fragment = "aj.type IN ('bank', 'cash', 'general')"
where_args = []
self.env.cr.execute(f'''
SELECT
array_remove(ARRAY_AGG(DISTINCT aa.id), NULL),
array_remove(ARRAY_AGG(DISTINCT apml.payment_account_id), NULL)
FROM account_journal aj
JOIN res_company rc ON aj.company_id = rc.id
LEFT JOIN account_payment_method_line apml
ON aj.id = apml.journal_id
LEFT JOIN account_account aa
ON aj.default_account_id = aa.id
AND aa.account_type IN ('asset_cash', 'liability_credit_card')
WHERE {where_fragment}
''', where_args)
fetched = self.env.cr.fetchone()
combined = set((fetched[0] or []) + (fetched[1] or []))
return tuple(combined) if combined else ()
def _build_move_ids_subquery(self, report, liquidity_acct_ids, col_group_opts) -> SQL:
"""Build a sub-select that returns move IDs touching liquidity accounts."""
base_query = report._get_report_query(
col_group_opts, 'strict_range',
[('account_id', 'in', list(liquidity_acct_ids))],
)
return SQL(
'''
SELECT array_agg(DISTINCT account_move_line.move_id) AS move_id
FROM %(tbl_refs)s
WHERE %(conditions)s
''',
tbl_refs=base_query.from_clause,
conditions=base_query.where_clause,
)
def _query_liquidity_balances(self, report, options, liquidity_acct_ids, scope):
"""Compute per-account balances for liquidity accounts.
*scope* is either ``'to_beginning_of_period'`` (opening) or
``'strict_range'`` (period movement).
"""
sql_parts = []
for col_key, col_opts in report._split_options_per_column_group(options).items():
qry = report._get_report_query(col_opts, scope, domain=[('account_id', 'in', liquidity_acct_ids)])
acct_alias = qry.join(
lhs_alias='account_move_line', lhs_column='account_id',
rhs_table='account_account', rhs_column='id', link='account_id',
)
code_sql = self.env['account.account']._field_to_sql(acct_alias, 'code', qry)
name_sql = self.env['account.account']._field_to_sql(acct_alias, 'name')
sql_parts.append(SQL(
'''
SELECT
%(col_key)s AS column_group_key,
account_move_line.account_id,
%(code_sql)s AS account_code,
%(name_sql)s AS account_name,
SUM(%(bal_expr)s) AS balance
FROM %(tbl_refs)s
%(fx_join)s
WHERE %(conditions)s
GROUP BY account_move_line.account_id, account_code, account_name
''',
col_key=col_key,
code_sql=code_sql,
name_sql=name_sql,
tbl_refs=qry.from_clause,
bal_expr=report._currency_table_apply_rate(SQL("account_move_line.balance")),
fx_join=report._currency_table_aml_join(col_opts),
conditions=qry.where_clause,
))
self.env.cr.execute(SQL(' UNION ALL ').join(sql_parts))
return self.env.cr.dictfetchall()
def _fetch_liquidity_side_entries(self, report, options, liquidity_acct_ids, cf_tag_ids):
"""Retrieve the non-liquidity side of moves that touch liquidity accounts.
Three sub-queries per column group capture:
1. Credit-side partial reconciliation amounts
2. Debit-side partial reconciliation amounts
3. Full line balances (for unreconciled portions)
Returns a list of dicts keyed by ``(account_id, column_group_key)``.
"""
aggregated = {}
sql_parts = []
for col_key, col_opts in report._split_options_per_column_group(options).items():
move_sub = self._build_move_ids_subquery(report, liquidity_acct_ids, col_opts)
q = Query(self.env, 'account_move_line')
acct_alias = q.join(
lhs_alias='account_move_line', lhs_column='account_id',
rhs_table='account_account', rhs_column='id', link='account_id',
)
code_sql = self.env['account.account']._field_to_sql(acct_alias, 'code', q)
name_sql = self.env['account.account']._field_to_sql(acct_alias, 'name')
type_sql = SQL.identifier(acct_alias, 'account_type')
sql_parts.append(SQL(
'''
(WITH liq_moves AS (%(move_sub)s)
-- 1) Credit-side partial amounts
SELECT
%(col_key)s AS column_group_key,
account_move_line.account_id,
%(code_sql)s AS account_code,
%(name_sql)s AS account_name,
%(type_sql)s AS account_account_type,
aat.account_account_tag_id AS account_tag_id,
SUM(%(partial_bal)s) AS balance
FROM %(from_cl)s
%(fx_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag aat
ON aat.account_account_id = account_move_line.account_id
AND aat.account_account_tag_id IN %(cf_tags)s
WHERE account_move_line.move_id IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND account_move_line.account_id NOT IN %(liq_accts)s
AND account_partial_reconcile.max_date BETWEEN %(dt_from)s AND %(dt_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id,
account_code, account_name, account_account_type,
aat.account_account_tag_id
UNION ALL
-- 2) Debit-side partial amounts (negated)
SELECT
%(col_key)s AS column_group_key,
account_move_line.account_id,
%(code_sql)s AS account_code,
%(name_sql)s AS account_name,
%(type_sql)s AS account_account_type,
aat.account_account_tag_id AS account_tag_id,
-SUM(%(partial_bal)s) AS balance
FROM %(from_cl)s
%(fx_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag aat
ON aat.account_account_id = account_move_line.account_id
AND aat.account_account_tag_id IN %(cf_tags)s
WHERE account_move_line.move_id IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND account_move_line.account_id NOT IN %(liq_accts)s
AND account_partial_reconcile.max_date BETWEEN %(dt_from)s AND %(dt_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id,
account_code, account_name, account_account_type,
aat.account_account_tag_id
UNION ALL
-- 3) Full line balances
SELECT
%(col_key)s AS column_group_key,
account_move_line.account_id,
%(code_sql)s AS account_code,
%(name_sql)s AS account_name,
%(type_sql)s AS account_account_type,
aat.account_account_tag_id AS account_tag_id,
SUM(%(line_bal)s) AS balance
FROM %(from_cl)s
%(fx_join)s
LEFT JOIN account_account_account_tag aat
ON aat.account_account_id = account_move_line.account_id
AND aat.account_account_tag_id IN %(cf_tags)s
WHERE account_move_line.move_id IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND account_move_line.account_id NOT IN %(liq_accts)s
GROUP BY account_move_line.account_id, account_code, account_name,
account_account_type, aat.account_account_tag_id)
''',
col_key=col_key,
move_sub=move_sub,
code_sql=code_sql,
name_sql=name_sql,
type_sql=type_sql,
from_cl=q.from_clause,
fx_join=report._currency_table_aml_join(col_opts),
partial_bal=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
line_bal=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cf_tags=tuple(cf_tag_ids),
liq_accts=liquidity_acct_ids,
dt_from=col_opts['date']['date_from'],
dt_to=col_opts['date']['date_to'],
))
self.env.cr.execute(SQL(' UNION ALL ').join(sql_parts))
for rec in self.env.cr.dictfetchall():
acct_id = rec['account_id']
aggregated.setdefault(acct_id, {})
aggregated[acct_id].setdefault(rec['column_group_key'], {
'column_group_key': rec['column_group_key'],
'account_id': acct_id,
'account_code': rec['account_code'],
'account_name': rec['account_name'],
'account_account_type': rec['account_account_type'],
'account_tag_id': rec['account_tag_id'],
'balance': 0.0,
})
aggregated[acct_id][rec['column_group_key']]['balance'] -= rec['balance']
return list(aggregated.values())
def _fetch_reconciled_counterparts(self, report, options, liquidity_acct_ids, cf_tag_ids):
"""Retrieve moves reconciled with liquidity moves but that are not
themselves liquidity moves.
Each amount is valued proportionally to what has actually been paid,
so a partially-paid invoice appears at the paid percentage.
"""
reconciled_acct_ids_by_col = {cg: set() for cg in options['column_groups']}
pct_map = {cg: {} for cg in options['column_groups']}
fx_table = report._get_currency_table(options)
# Step 1 gather reconciliation amounts per move / account
step1_parts = []
for col_key, col_opts in report._split_options_per_column_group(options).items():
move_sub = self._build_move_ids_subquery(report, liquidity_acct_ids, col_opts)
step1_parts.append(SQL(
'''
(WITH liq_moves AS (%(move_sub)s)
SELECT
%(col_key)s AS column_group_key,
dr.move_id, dr.account_id,
SUM(%(partial_amt)s) AS balance
FROM account_move_line AS cr
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = cr.id
JOIN %(fx_tbl)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current'
INNER JOIN account_move_line AS dr
ON dr.id = account_partial_reconcile.debit_move_id
WHERE cr.move_id IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND cr.account_id NOT IN %(liq_accts)s
AND cr.credit > 0.0
AND dr.move_id NOT IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND account_partial_reconcile.max_date BETWEEN %(dt_from)s AND %(dt_to)s
GROUP BY dr.move_id, dr.account_id
UNION ALL
SELECT
%(col_key)s AS column_group_key,
cr2.move_id, cr2.account_id,
-SUM(%(partial_amt)s) AS balance
FROM account_move_line AS dr2
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = dr2.id
JOIN %(fx_tbl)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current'
INNER JOIN account_move_line AS cr2
ON cr2.id = account_partial_reconcile.credit_move_id
WHERE dr2.move_id IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND dr2.account_id NOT IN %(liq_accts)s
AND dr2.debit > 0.0
AND cr2.move_id NOT IN (SELECT unnest(liq_moves.move_id) FROM liq_moves)
AND account_partial_reconcile.max_date BETWEEN %(dt_from)s AND %(dt_to)s
GROUP BY cr2.move_id, cr2.account_id)
''',
move_sub=move_sub,
col_key=col_key,
liq_accts=liquidity_acct_ids,
dt_from=col_opts['date']['date_from'],
dt_to=col_opts['date']['date_to'],
fx_tbl=fx_table,
partial_amt=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
))
self.env.cr.execute(SQL(' UNION ALL ').join(step1_parts))
for rec in self.env.cr.dictfetchall():
cg = rec['column_group_key']
pct_map[cg].setdefault(rec['move_id'], {})
pct_map[cg][rec['move_id']].setdefault(rec['account_id'], [0.0, 0.0])
pct_map[cg][rec['move_id']][rec['account_id']][0] += rec['balance']
reconciled_acct_ids_by_col[cg].add(rec['account_id'])
if not any(pct_map.values()):
return []
# Step 2 total balance per move / reconciled account
step2_parts = []
for col in options['columns']:
cg = col['column_group_key']
mv_ids = tuple(pct_map[cg].keys()) or (None,)
ac_ids = tuple(reconciled_acct_ids_by_col[cg]) or (None,)
step2_parts.append(SQL(
'''
SELECT
%(col_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
SUM(%(bal_expr)s) AS balance
FROM account_move_line
JOIN %(fx_tbl)s
ON account_currency_table.company_id = account_move_line.company_id
AND account_currency_table.rate_type = 'current'
WHERE account_move_line.move_id IN %(mv_ids)s
AND account_move_line.account_id IN %(ac_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id
''',
col_key=cg,
fx_tbl=fx_table,
bal_expr=report._currency_table_apply_rate(SQL("account_move_line.balance")),
mv_ids=mv_ids,
ac_ids=ac_ids,
))
self.env.cr.execute(SQL(' UNION ALL ').join(step2_parts))
for rec in self.env.cr.dictfetchall():
cg = rec['column_group_key']
mv = rec['move_id']
ac = rec['account_id']
if ac in pct_map[cg].get(mv, {}):
pct_map[cg][mv][ac][1] += rec['balance']
# Step 3 fetch full detail with account type & tag, then apply pct
result_map = {}
detail_q = Query(self.env, 'account_move_line')
acct_a = detail_q.join(
lhs_alias='account_move_line', lhs_column='account_id',
rhs_table='account_account', rhs_column='id', link='account_id',
)
code_fld = self.env['account.account']._field_to_sql(acct_a, 'code', detail_q)
name_fld = self.env['account.account']._field_to_sql(acct_a, 'name')
type_fld = SQL.identifier(acct_a, 'account_type')
step3_parts = []
for col in options['columns']:
cg = col['column_group_key']
step3_parts.append(SQL(
'''
SELECT
%(col_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
%(code_fld)s AS account_code,
%(name_fld)s AS account_name,
%(type_fld)s AS account_account_type,
aat.account_account_tag_id AS account_tag_id,
SUM(%(bal_expr)s) AS balance
FROM %(from_cl)s
%(fx_join)s
LEFT JOIN account_account_account_tag aat
ON aat.account_account_id = account_move_line.account_id
AND aat.account_account_tag_id IN %(cf_tags)s
WHERE account_move_line.move_id IN %(mv_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id,
account_code, account_name, account_account_type,
aat.account_account_tag_id
''',
col_key=cg,
code_fld=code_fld,
name_fld=name_fld,
type_fld=type_fld,
from_cl=detail_q.from_clause,
fx_join=report._currency_table_aml_join(options),
bal_expr=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cf_tags=tuple(cf_tag_ids),
mv_ids=tuple(pct_map[cg].keys()) or (None,),
))
self.env.cr.execute(SQL(' UNION ALL ').join(step3_parts))
for rec in self.env.cr.dictfetchall():
cg = rec['column_group_key']
mv = rec['move_id']
ac = rec['account_id']
line_bal = rec['balance']
# Sum reconciled & total for the whole move
sum_reconciled = 0.0
sum_total = 0.0
for r_amt, t_amt in pct_map[cg][mv].values():
sum_reconciled += r_amt
sum_total += t_amt
# Compute the applicable portion
if sum_total and ac not in pct_map[cg][mv]:
ratio = sum_reconciled / sum_total
line_bal *= ratio
elif not sum_total and ac in pct_map[cg][mv]:
line_bal = -pct_map[cg][mv][ac][0]
else:
continue
result_map.setdefault(ac, {})
result_map[ac].setdefault(cg, {
'column_group_key': cg,
'account_id': ac,
'account_code': rec['account_code'],
'account_name': rec['account_name'],
'account_account_type': rec['account_account_type'],
'account_tag_id': rec['account_tag_id'],
'balance': 0.0,
})
result_map[ac][cg]['balance'] -= line_bal
return list(result_map.values())
# ------------------------------------------------------------------
# Line rendering
# ------------------------------------------------------------------
def _build_section_structure(self):
"""Define the hierarchical layout of the cash flow statement.
Returns an ordered dictionary whose keys identify each section and
whose values carry the display name, nesting level, parent reference,
and optional CSS class.
"""
return {
'opening_balance': {
'name': _('Cash and cash equivalents, beginning of period'),
'level': 0,
},
'net_increase': {
'name': _('Net increase in cash and cash equivalents'),
'level': 0,
'unfolded': True,
},
'operating_activities': {
'name': _('Cash flows from operating activities'),
'level': 2,
'parent_line_id': 'net_increase',
'class': 'fw-bold',
'unfolded': True,
},
'advance_payments_customer': {
'name': _('Advance Payments received from customers'),
'level': 4,
'parent_line_id': 'operating_activities',
},
'received_operating_activities': {
'name': _('Cash received from operating activities'),
'level': 4,
'parent_line_id': 'operating_activities',
},
'advance_payments_suppliers': {
'name': _('Advance payments made to suppliers'),
'level': 4,
'parent_line_id': 'operating_activities',
},
'paid_operating_activities': {
'name': _('Cash paid for operating activities'),
'level': 4,
'parent_line_id': 'operating_activities',
},
'investing_activities': {
'name': _('Cash flows from investing & extraordinary activities'),
'level': 2,
'parent_line_id': 'net_increase',
'class': 'fw-bold',
'unfolded': True,
},
'investing_activities_cash_in': {
'name': _('Cash in'),
'level': 4,
'parent_line_id': 'investing_activities',
},
'investing_activities_cash_out': {
'name': _('Cash out'),
'level': 4,
'parent_line_id': 'investing_activities',
},
'financing_activities': {
'name': _('Cash flows from financing activities'),
'level': 2,
'parent_line_id': 'net_increase',
'class': 'fw-bold',
'unfolded': True,
},
'financing_activities_cash_in': {
'name': _('Cash in'),
'level': 4,
'parent_line_id': 'financing_activities',
},
'financing_activities_cash_out': {
'name': _('Cash out'),
'level': 4,
'parent_line_id': 'financing_activities',
},
'unclassified_activities': {
'name': _('Cash flows from unclassified activities'),
'level': 2,
'parent_line_id': 'net_increase',
'class': 'fw-bold',
'unfolded': True,
},
'unclassified_activities_cash_in': {
'name': _('Cash in'),
'level': 4,
'parent_line_id': 'unclassified_activities',
},
'unclassified_activities_cash_out': {
'name': _('Cash out'),
'level': 4,
'parent_line_id': 'unclassified_activities',
},
'closing_balance': {
'name': _('Cash and cash equivalents, closing balance'),
'level': 0,
},
}
def _render_section_line(self, report, options, section_key, section_meta, report_data):
"""Produce a single section / header line dictionary."""
line_id = report._get_generic_line_id(None, None, markup=section_key)
has_detail = (
section_key in report_data
and 'aml_groupby_account' in report_data[section_key]
)
col_vals = []
for col in options['columns']:
expr = col['expression_label']
cg = col['column_group_key']
raw = (
report_data[section_key][expr].get(cg, 0.0)
if section_key in report_data
else 0.0
)
col_vals.append(report._build_column_dict(raw, col, options=options))
return {
'id': line_id,
'name': section_meta['name'],
'level': section_meta['level'],
'class': section_meta.get('class', ''),
'columns': col_vals,
'unfoldable': has_detail,
'unfolded': (
line_id in options['unfolded_lines']
or section_meta.get('unfolded')
or (options.get('unfold_all') and has_detail)
),
}
def _render_detail_line(self, report, options, detail):
"""Produce a per-account detail line under a section."""
parent_id = report._get_generic_line_id(None, None, detail['parent_line_id'])
line_id = report._get_generic_line_id(
'account.account', detail['account_id'], parent_line_id=parent_id,
)
col_vals = []
for col in options['columns']:
expr = col['expression_label']
cg = col['column_group_key']
raw = detail[expr].get(cg, 0.0)
col_vals.append(report._build_column_dict(raw, col, options=options))
display_name = (
f"{detail['account_code']} {detail['account_name']}"
if detail['account_code']
else detail['account_name']
)
return {
'id': line_id,
'name': display_name,
'caret_options': 'account.account',
'level': detail['level'],
'parent_id': parent_id,
'columns': col_vals,
}
def _render_unexplained_difference(self, report, options, report_data):
"""If closing != opening + net_increase, emit an extra line showing
the gap so the user can investigate."""
found_gap = False
col_vals = []
for col in options['columns']:
expr = col['expression_label']
cg = col['column_group_key']
opening = (
report_data['opening_balance'][expr].get(cg, 0.0)
if 'opening_balance' in report_data else 0.0
)
closing = (
report_data['closing_balance'][expr].get(cg, 0.0)
if 'closing_balance' in report_data else 0.0
)
net_chg = (
report_data['net_increase'][expr].get(cg, 0.0)
if 'net_increase' in report_data else 0.0
)
gap = closing - opening - net_chg
if not self.env.company.currency_id.is_zero(gap):
found_gap = True
col_vals.append(report._build_column_dict(
gap,
{'figure_type': 'monetary', 'expression_label': 'balance'},
options=options,
))
if found_gap:
return {
'id': report._get_generic_line_id(None, None, markup='unexplained_difference'),
'name': _('Unexplained Difference'),
'level': 1,
'columns': col_vals,
}
return None