Files
odoo-addons/addons/at_accounting/models/account_cash_flow_report.py

713 lines
40 KiB
Python

from odoo import models, _
from odoo.tools import SQL, Query
class CashFlowReportCustomHandler(models.AbstractModel):
_name = 'account.cash.flow.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Cash Flow Report Custom Handler'
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
# Compute the cash flow report using the direct method: https://www.investopedia.com/terms/d/direct_method.asp
lines = []
layout_data = self._get_layout_data()
report_data = self._get_report_data(report, options, layout_data)
for layout_line_id, layout_line_data in layout_data.items():
lines.append((0, self._get_layout_line(report, options, layout_line_id, layout_line_data, report_data)))
if layout_line_id in report_data and 'aml_groupby_account' in report_data[layout_line_id]:
aml_data_values = report_data[layout_line_id]['aml_groupby_account'].values()
aml_data_values_with_account_code = []
aml_data_values_without_account_code = []
for aml_data in aml_data_values:
if aml_data['account_code'] is not None:
aml_data_values_with_account_code.append(aml_data)
else:
aml_data_values_without_account_code.append(aml_data)
for aml_data in (sorted(aml_data_values_with_account_code, key=lambda x: x['account_code'])
+ aml_data_values_without_account_code):
lines.append((0, self._get_aml_line(report, options, aml_data)))
unexplained_difference_line = self._get_unexplained_difference_line(report, options, report_data)
if unexplained_difference_line:
lines.append((0, unexplained_difference_line))
return lines
def _custom_options_initializer(self, report, options, previous_options):
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'))])
def _get_report_data(self, report, options, layout_data):
report_data = {}
payment_account_ids = self._get_account_ids(report, options)
if not payment_account_ids:
return report_data
# Compute 'Cash and cash equivalents, beginning of period'
for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'to_beginning_of_period'):
self._add_report_data('opening_balance', aml_data, layout_data, report_data)
self._add_report_data('closing_balance', aml_data, layout_data, report_data)
# Compute 'Cash and cash equivalents, closing balance'
for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'strict_range'):
self._add_report_data('closing_balance', aml_data, layout_data, report_data)
tags_ids = self._get_tags_ids()
cashflow_tag_ids = self._get_cashflow_tag_ids()
# Process liquidity moves
for aml_groupby_account in self._get_liquidity_moves(report, options, payment_account_ids, cashflow_tag_ids):
for aml_data in aml_groupby_account.values():
self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data)
# Process reconciled moves
for aml_groupby_account in self._get_reconciled_moves(report, options, payment_account_ids, cashflow_tag_ids):
for aml_data in aml_groupby_account.values():
self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data)
return report_data
def _add_report_data(self, layout_line_id, aml_data, layout_data, report_data):
"""
Add or update the report_data dictionnary with aml_data.
report_data is a dictionnary where the keys are keys from _cash_flow_report_get_layout_data() (used for mapping)
and the values can contain 2 dictionnaries:
* (required) 'balance' where the key is the column_group_key and the value is the balance of the line
* (optional) 'aml_groupby_account' where the key is an account_id and the values are the aml data
"""
def _report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data):
# Update the balance in report_data of the parent of the layout_line_id recursively (Stops when the line has no parent)
if 'parent_line_id' in layout_data[layout_line_id]:
parent_line_id = layout_data[layout_line_id]['parent_line_id']
report_data.setdefault(parent_line_id, {'balance': {}})
report_data[parent_line_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[parent_line_id]['balance'][aml_column_group_key] += aml_balance
_report_update_parent(parent_line_id, aml_column_group_key, aml_balance, layout_data, report_data)
aml_column_group_key = aml_data['column_group_key']
aml_account_id = aml_data['account_id']
aml_account_code = aml_data['account_code']
aml_account_name = aml_data['account_name']
aml_balance = aml_data['balance']
aml_account_tag = aml_data.get('account_tag_id', None)
if self.env.company.currency_id.is_zero(aml_balance):
return
report_data.setdefault(layout_line_id, {
'balance': {},
'aml_groupby_account': {},
})
report_data[layout_line_id]['aml_groupby_account'].setdefault(aml_account_id, {
'parent_line_id': layout_line_id,
'account_id': aml_account_id,
'account_code': aml_account_code,
'account_name': aml_account_name,
'account_tag_id': aml_account_tag,
'level': layout_data[layout_line_id]['level'] + 1,
'balance': {},
})
report_data[layout_line_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[layout_line_id]['balance'][aml_column_group_key] += aml_balance
report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'][aml_column_group_key] += aml_balance
_report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data)
def _get_tags_ids(self):
''' Get a dict to pass on to _dispatch_aml_data containing information mapping account tags to report lines. '''
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 _get_cashflow_tag_ids(self):
''' Get the list of account tags that are relevant for the cash flow report. '''
return self._get_tags_ids().values()
def _dispatch_aml_data(self, tags_ids, aml_data, layout_data, report_data):
# Dispatch the aml_data in the correct layout_line
if aml_data['account_account_type'] == 'asset_receivable':
self._add_report_data('advance_payments_customer', aml_data, layout_data, report_data)
elif aml_data['account_account_type'] == 'liability_payable':
self._add_report_data('advance_payments_suppliers', aml_data, layout_data, report_data)
elif aml_data['balance'] < 0:
if aml_data['account_tag_id'] == tags_ids['operating']:
self._add_report_data('paid_operating_activities', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['investing']:
self._add_report_data('investing_activities_cash_out', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['financing']:
self._add_report_data('financing_activities_cash_out', aml_data, layout_data, report_data)
else:
self._add_report_data('unclassified_activities_cash_out', aml_data, layout_data, report_data)
elif aml_data['balance'] > 0:
if aml_data['account_tag_id'] == tags_ids['operating']:
self._add_report_data('received_operating_activities', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['investing']:
self._add_report_data('investing_activities_cash_in', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['financing']:
self._add_report_data('financing_activities_cash_in', aml_data, layout_data, report_data)
else:
self._add_report_data('unclassified_activities_cash_in', aml_data, layout_data, report_data)
# -------------------------------------------------------------------------
# QUERIES
# -------------------------------------------------------------------------
def _get_account_ids(self, report, options):
''' Retrieve all accounts to be part of the cash flow statement and also the accounts making them.
:param options: The report options.
:return: payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
'''
# Fetch liquidity accounts:
# Accounts being used by at least one bank/cash journal.
selected_journal_ids = [j['id'] for j in report._get_options_journals(options)]
where_clause = "account_journal.id IN %s" if selected_journal_ids else "account_journal.type IN ('bank', 'cash', 'general')"
where_params = [tuple(selected_journal_ids)] if selected_journal_ids else []
self._cr.execute(f'''
SELECT
array_remove(ARRAY_AGG(DISTINCT account_account.id), NULL),
array_remove(ARRAY_AGG(DISTINCT account_payment_method_line.payment_account_id), NULL)
FROM account_journal
JOIN res_company
ON account_journal.company_id = res_company.id
LEFT JOIN account_payment_method_line
ON account_journal.id = account_payment_method_line.journal_id
LEFT JOIN account_account
ON account_journal.default_account_id = account_account.id
AND account_account.account_type IN ('asset_cash', 'liability_credit_card')
WHERE {where_clause}
''', where_params)
res = self._cr.fetchall()[0]
payment_account_ids = set((res[0] or []) + (res[1] or []))
if not payment_account_ids:
return ()
return tuple(payment_account_ids)
def _get_move_ids_query(self, report, payment_account_ids, column_group_options) -> SQL:
''' Get all liquidity moves to be part of the cash flow statement.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: query: The SQL query to retrieve the move IDs.
'''
query = report._get_report_query(column_group_options, 'strict_range', [('account_id', 'in', list(payment_account_ids))])
return SQL(
'''
SELECT
array_agg(DISTINCT account_move_line.move_id) AS move_id
FROM %(table_references)s
WHERE %(search_condition)s
''',
table_references=query.from_clause,
search_condition=query.where_clause,
)
def _compute_liquidity_balance(self, report, options, payment_account_ids, date_scope):
''' Compute the balance of all liquidity accounts to populate the following sections:
'Cash and cash equivalents, beginning of period' and 'Cash and cash equivalents, closing balance'.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, balance).
'''
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
query = report._get_report_query(column_group_options, date_scope, domain=[('account_id', 'in', payment_account_ids)])
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.account_id, account_code, account_name
''',
column_group_key=column_group_key,
account_code=account_code,
account_name=account_name,
table_references=query.from_clause,
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
currency_table_join=report._currency_table_aml_join(column_group_options),
search_condition=query.where_clause,
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
return self._cr.dictfetchall()
def _get_liquidity_moves(self, report, options, payment_account_ids, cash_flow_tag_ids):
''' Fetch all information needed to compute lines from liquidity moves.
The difficulty is to represent only the not-reconciled part of balance.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, account_type, amount).
'''
reconciled_aml_groupby_account = {}
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options)
query = Query(self.env, 'account_move_line')
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_type = SQL.identifier(account_alias, 'account_type')
queries.append(SQL(
'''
(WITH payment_move_ids AS (%(move_ids_query)s)
-- Credit amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(partial_amount_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Debit amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
-SUM(%(partial_amount_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Total amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id AS account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(aml_balance_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
GROUP BY account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id)
''',
column_group_key=column_group_key,
move_ids_query=move_ids_query,
account_code=account_code,
account_name=account_name,
account_type=account_type,
from_clause=query.from_clause,
currency_table_join=report._currency_table_aml_join(column_group_options),
partial_amount_select=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
aml_balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cash_flow_tag_ids=tuple(cash_flow_tag_ids),
payment_account_ids=payment_account_ids,
date_from=column_group_options['date']['date_from'],
date_to=column_group_options['date']['date_to'],
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
reconciled_aml_groupby_account.setdefault(aml_data['account_id'], {})
reconciled_aml_groupby_account[aml_data['account_id']].setdefault(aml_data['column_group_key'], {
'column_group_key': aml_data['column_group_key'],
'account_id': aml_data['account_id'],
'account_code': aml_data['account_code'],
'account_name': aml_data['account_name'],
'account_account_type': aml_data['account_account_type'],
'account_tag_id': aml_data['account_tag_id'],
'balance': 0.0,
})
reconciled_aml_groupby_account[aml_data['account_id']][aml_data['column_group_key']]['balance'] -= aml_data['balance']
return list(reconciled_aml_groupby_account.values())
def _get_reconciled_moves(self, report, options, payment_account_ids, cash_flow_tag_ids):
''' Retrieve all moves being not a liquidity move to be shown in the cash flow statement.
Each amount must be valued at the percentage of what is actually paid.
E.g. An invoice of 1000 being paid at 50% must be valued at 500.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, account_type, amount).
'''
reconciled_account_ids = {column_group_key: set() for column_group_key in options['column_groups']}
reconciled_percentage_per_move = {column_group_key: {} for column_group_key in options['column_groups']}
currency_table = report._get_currency_table(options)
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options)
queries.append(SQL(
'''
(WITH payment_move_ids AS (%(move_ids_query)s)
SELECT
%(column_group_key)s AS column_group_key,
debit_line.move_id,
debit_line.account_id,
SUM(%(partial_amount)s) AS balance
FROM account_move_line AS credit_line
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = credit_line.id
JOIN %(currency_table)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
INNER JOIN account_move_line AS debit_line
ON debit_line.id = account_partial_reconcile.debit_move_id
WHERE credit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND credit_line.account_id NOT IN %(payment_account_ids)s
AND credit_line.credit > 0.0
AND debit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY debit_line.move_id, debit_line.account_id
UNION ALL
SELECT
%(column_group_key)s AS column_group_key,
credit_line.move_id,
credit_line.account_id,
-SUM(%(partial_amount)s) AS balance
FROM account_move_line AS debit_line
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = debit_line.id
JOIN %(currency_table)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
INNER JOIN account_move_line AS credit_line
ON credit_line.id = account_partial_reconcile.credit_move_id
WHERE debit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND debit_line.account_id NOT IN %(payment_account_ids)s
AND debit_line.debit > 0.0
AND credit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY credit_line.move_id, credit_line.account_id)
''',
move_ids_query=move_ids_query,
column_group_key=column_group_key,
payment_account_ids=payment_account_ids,
date_from=column_group_options['date']['date_from'],
date_to=column_group_options['date']['date_to'],
currency_table=currency_table,
partial_amount=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
reconciled_percentage_per_move[aml_data['column_group_key']].setdefault(aml_data['move_id'], {})
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']].setdefault(aml_data['account_id'], [0.0, 0.0])
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][0] += aml_data['balance']
reconciled_account_ids[aml_data['column_group_key']].add(aml_data['account_id'])
if not reconciled_percentage_per_move:
return []
queries = []
for column in options['columns']:
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
SUM(%(balance_select)s) AS balance
FROM account_move_line
JOIN %(currency_table)s
ON account_currency_table.company_id = account_move_line.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
WHERE account_move_line.move_id IN %(move_ids)s
AND account_move_line.account_id IN %(account_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id
''',
column_group_key=column['column_group_key'],
currency_table=currency_table,
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,),
account_ids=tuple(reconciled_account_ids[column['column_group_key']]) or (None,)
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
if aml_data['account_id'] in reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']]:
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][1] += aml_data['balance']
reconciled_aml_per_account = {}
queries = []
query = Query(self.env, 'account_move_line')
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_type = SQL.identifier(account_alias, 'account_type')
for column in options['columns']:
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(balance_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN %(move_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
''',
column_group_key=column['column_group_key'],
account_code=account_code,
account_name=account_name,
account_type=account_type,
from_clause=query.from_clause,
currency_table_join=report._currency_table_aml_join(options),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cash_flow_tag_ids=tuple(cash_flow_tag_ids),
move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,)
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
aml_column_group_key = aml_data['column_group_key']
aml_move_id = aml_data['move_id']
aml_account_id = aml_data['account_id']
aml_account_code = aml_data['account_code']
aml_account_name = aml_data['account_name']
aml_account_account_type = aml_data['account_account_type']
aml_account_tag_id = aml_data['account_tag_id']
aml_balance = aml_data['balance']
# Compute the total reconciled for the whole move.
total_reconciled_amount = 0.0
total_amount = 0.0
for reconciled_amount, amount in reconciled_percentage_per_move[aml_column_group_key][aml_move_id].values():
total_reconciled_amount += reconciled_amount
total_amount += amount
# Compute matched percentage for each account.
if total_amount and aml_account_id not in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]:
# Lines being on reconciled moves but not reconciled with any liquidity move must be valued at the
# percentage of what is actually paid.
reconciled_percentage = total_reconciled_amount / total_amount
aml_balance *= reconciled_percentage
elif not total_amount and aml_account_id in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]:
# The total amount to reconcile is 0. In that case, only add entries being on these accounts. Otherwise,
# this special case will lead to an unexplained difference equivalent to the reconciled amount on this
# account.
# E.g:
#
# Liquidity move:
# Account | Debit | Credit
# --------------------------------------
# Bank | | 100
# Receivable | 100 |
#
# Reconciled move: <- reconciled_amount=100, total_amount=0.0
# Account | Debit | Credit
# --------------------------------------
# Receivable | | 200
# Receivable | 200 | <- Only the reconciled part of this entry must be added.
aml_balance = -reconciled_percentage_per_move[aml_column_group_key][aml_move_id][aml_account_id][0]
else:
# Others lines are not considered.
continue
reconciled_aml_per_account.setdefault(aml_account_id, {})
reconciled_aml_per_account[aml_account_id].setdefault(aml_column_group_key, {
'column_group_key': aml_column_group_key,
'account_id': aml_account_id,
'account_code': aml_account_code,
'account_name': aml_account_name,
'account_account_type': aml_account_account_type,
'account_tag_id': aml_account_tag_id,
'balance': 0.0,
})
reconciled_aml_per_account[aml_account_id][aml_column_group_key]['balance'] -= aml_balance
return list(reconciled_aml_per_account.values())
# -------------------------------------------------------------------------
# COLUMNS / LINES
# -------------------------------------------------------------------------
def _get_layout_data(self):
# Indentation of the following dict reflects the structure of the report.
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 _get_layout_line(self, report, options, layout_line_id, layout_line_data, report_data):
line_id = report._get_generic_line_id(None, None, markup=layout_line_id)
unfoldable = 'aml_groupby_account' in report_data[layout_line_id] if layout_line_id in report_data else False
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
value = report_data[layout_line_id][expression_label].get(column_group_key, 0.0) if layout_line_id in report_data else 0.0
column_values.append(report._build_column_dict(value, column, options=options))
return {
'id': line_id,
'name': layout_line_data['name'],
'level': layout_line_data['level'],
'class': layout_line_data.get('class', ''),
'columns': column_values,
'unfoldable': unfoldable,
'unfolded': line_id in options['unfolded_lines'] or layout_line_data.get('unfolded') or (options.get('unfold_all') and unfoldable),
}
def _get_aml_line(self, report, options, aml_data):
parent_line_id = report._get_generic_line_id(None, None, aml_data['parent_line_id'])
line_id = report._get_generic_line_id('account.account', aml_data['account_id'], parent_line_id=parent_line_id)
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
value = aml_data[expression_label].get(column_group_key, 0.0)
column_values.append(report._build_column_dict(value, column, options=options))
return {
'id': line_id,
'name': f"{aml_data['account_code']} {aml_data['account_name']}" if aml_data['account_code'] else aml_data['account_name'],
'caret_options': 'account.account',
'level': aml_data['level'],
'parent_id': parent_line_id,
'columns': column_values,
}
def _get_unexplained_difference_line(self, report, options, report_data):
unexplained_difference = False
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
opening_balance = report_data['opening_balance'][expression_label].get(column_group_key, 0.0) if 'opening_balance' in report_data else 0.0
closing_balance = report_data['closing_balance'][expression_label].get(column_group_key, 0.0) if 'closing_balance' in report_data else 0.0
net_increase = report_data['net_increase'][expression_label].get(column_group_key, 0.0) if 'net_increase' in report_data else 0.0
balance = closing_balance - opening_balance - net_increase
if not self.env.company.currency_id.is_zero(balance):
unexplained_difference = True
column_values.append(report._build_column_dict(
balance,
{
'figure_type': 'monetary',
'expression_label': 'balance',
},
options=options,
))
if unexplained_difference:
return {
'id': report._get_generic_line_id(None, None, markup='unexplained_difference'),
'name': 'Unexplained Difference',
'level': 1,
'columns': column_values,
}