Initial commit
This commit is contained in:
753
Fusion Accounting/models/account_general_ledger.py
Normal file
753
Fusion Accounting/models/account_general_ledger.py
Normal file
@@ -0,0 +1,753 @@
|
||||
# Fusion Accounting - General Ledger Report Handler
|
||||
|
||||
import json
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.tools.misc import format_date
|
||||
from odoo.tools import get_lang, SQL
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class GeneralLedgerCustomHandler(models.AbstractModel):
|
||||
"""Produces the General Ledger report.
|
||||
|
||||
Aggregates journal items by account and period, handles initial balances,
|
||||
unaffected-earnings allocation, and optional tax-declaration sections.
|
||||
"""
|
||||
|
||||
_name = 'account.general.ledger.report.handler'
|
||||
_inherit = 'account.report.custom.handler'
|
||||
_description = 'General Ledger Custom Handler'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Display configuration
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_custom_display_config(self):
|
||||
return {
|
||||
'templates': {
|
||||
'AccountReportLineName': 'fusion_accounting.GeneralLedgerLineName',
|
||||
},
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Options
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _custom_options_initializer(self, report, options, previous_options):
|
||||
"""Strip the multi-currency column when the user lacks the group,
|
||||
and auto-unfold when printing."""
|
||||
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
||||
|
||||
if self.env.user.has_group('base.group_multi_currency'):
|
||||
options['multi_currency'] = True
|
||||
else:
|
||||
options['columns'] = [
|
||||
c for c in options['columns']
|
||||
if c['expression_label'] != 'amount_currency'
|
||||
]
|
||||
|
||||
# When printing the whole report, unfold everything unless the user
|
||||
# explicitly selected specific lines.
|
||||
options['unfold_all'] = (
|
||||
(options['export_mode'] == 'print' and not options.get('unfolded_lines'))
|
||||
or options['unfold_all']
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dynamic lines
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
"""Return ``[(seq, line_dict), ...]`` for every account row plus
|
||||
an optional tax-declaration block and a grand-total row."""
|
||||
result_lines = []
|
||||
period_start = fields.Date.from_string(options['date']['date_from'])
|
||||
comp_currency = self.env.company.currency_id
|
||||
|
||||
running_totals = defaultdict(lambda: {'debit': 0, 'credit': 0, 'balance': 0})
|
||||
|
||||
for account_rec, col_grp_vals in self._aggregate_account_values(report, options):
|
||||
per_col = {}
|
||||
any_current = False
|
||||
|
||||
for col_key, bucket in col_grp_vals.items():
|
||||
main = bucket.get('sum', {})
|
||||
unaff = bucket.get('unaffected_earnings', {})
|
||||
|
||||
dr = main.get('debit', 0.0) + unaff.get('debit', 0.0)
|
||||
cr = main.get('credit', 0.0) + unaff.get('credit', 0.0)
|
||||
bal = main.get('balance', 0.0) + unaff.get('balance', 0.0)
|
||||
|
||||
per_col[col_key] = {
|
||||
'amount_currency': main.get('amount_currency', 0.0) + unaff.get('amount_currency', 0.0),
|
||||
'debit': dr,
|
||||
'credit': cr,
|
||||
'balance': bal,
|
||||
}
|
||||
|
||||
latest_date = main.get('max_date')
|
||||
if latest_date and latest_date >= period_start:
|
||||
any_current = True
|
||||
|
||||
running_totals[col_key]['debit'] += dr
|
||||
running_totals[col_key]['credit'] += cr
|
||||
running_totals[col_key]['balance'] += bal
|
||||
|
||||
result_lines.append(
|
||||
self._build_account_header_line(report, options, account_rec, any_current, per_col)
|
||||
)
|
||||
|
||||
# Round the accumulated balance
|
||||
for totals in running_totals.values():
|
||||
totals['balance'] = comp_currency.round(totals['balance'])
|
||||
|
||||
# Tax-declaration section (single column group + single journal of sale/purchase type)
|
||||
active_journals = report._get_options_journals(options)
|
||||
if (
|
||||
len(options['column_groups']) == 1
|
||||
and len(active_journals) == 1
|
||||
and active_journals[0]['type'] in ('sale', 'purchase')
|
||||
):
|
||||
result_lines += self._produce_tax_declaration_lines(
|
||||
report, options, active_journals[0]['type']
|
||||
)
|
||||
|
||||
# Grand total
|
||||
result_lines.append(self._build_grand_total_line(report, options, running_totals))
|
||||
|
||||
return [(0, ln) for ln in result_lines]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Batch unfold helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
|
||||
"""Pre-load data for all accounts that need unfolding so the engine
|
||||
does not issue per-account queries."""
|
||||
target_acct_ids = []
|
||||
for line_info in lines_to_expand_by_function.get('_report_expand_unfoldable_line_general_ledger', []):
|
||||
mdl, mdl_id = report._get_model_info_from_id(line_info['id'])
|
||||
if mdl == 'account.account':
|
||||
target_acct_ids.append(mdl_id)
|
||||
|
||||
page_size = report.load_more_limit if report.load_more_limit and not options.get('export_mode') else None
|
||||
overflow_flags = {}
|
||||
|
||||
full_aml_data = self._fetch_aml_data(report, options, target_acct_ids)[0]
|
||||
|
||||
if page_size:
|
||||
trimmed_aml_data = {}
|
||||
for acct_id, acct_rows in full_aml_data.items():
|
||||
page = {}
|
||||
for key, val in acct_rows.items():
|
||||
if len(page) >= page_size:
|
||||
overflow_flags[acct_id] = True
|
||||
break
|
||||
page[key] = val
|
||||
trimmed_aml_data[acct_id] = page
|
||||
else:
|
||||
trimmed_aml_data = full_aml_data
|
||||
|
||||
return {
|
||||
'initial_balances': self._fetch_opening_balances(report, target_acct_ids, options),
|
||||
'aml_results': trimmed_aml_data,
|
||||
'has_more': overflow_flags,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tax declaration
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _produce_tax_declaration_lines(self, report, options, tax_type):
|
||||
"""Append a Tax Declaration section when viewing a single
|
||||
sale / purchase journal."""
|
||||
header_labels = {
|
||||
'debit': _("Base Amount"),
|
||||
'credit': _("Tax Amount"),
|
||||
}
|
||||
|
||||
output = [
|
||||
{
|
||||
'id': report._get_generic_line_id(None, None, markup='tax_decl_header_1'),
|
||||
'name': _('Tax Declaration'),
|
||||
'columns': [{} for _ in options['columns']],
|
||||
'level': 1,
|
||||
'unfoldable': False,
|
||||
'unfolded': False,
|
||||
},
|
||||
{
|
||||
'id': report._get_generic_line_id(None, None, markup='tax_decl_header_2'),
|
||||
'name': _('Name'),
|
||||
'columns': [
|
||||
{'name': header_labels.get(c['expression_label'], '')}
|
||||
for c in options['columns']
|
||||
],
|
||||
'level': 3,
|
||||
'unfoldable': False,
|
||||
'unfolded': False,
|
||||
},
|
||||
]
|
||||
|
||||
tax_report = self.env.ref('account.generic_tax_report')
|
||||
tax_opts = tax_report.get_options({
|
||||
**options,
|
||||
'selected_variant_id': tax_report.id,
|
||||
'forced_domain': [('tax_line_id.type_tax_use', '=', tax_type)],
|
||||
})
|
||||
tax_lines = tax_report._get_lines(tax_opts)
|
||||
parent_marker = tax_report._get_generic_line_id(None, None, markup=tax_type)
|
||||
|
||||
for tl in tax_lines:
|
||||
if tl.get('parent_id') != parent_marker:
|
||||
continue
|
||||
src_cols = tl['columns']
|
||||
mapped = {
|
||||
'debit': src_cols[0],
|
||||
'credit': src_cols[1],
|
||||
}
|
||||
tl['columns'] = [mapped.get(c['expression_label'], {}) for c in options['columns']]
|
||||
output.append(tl)
|
||||
|
||||
return output
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Core queries
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _aggregate_account_values(self, report, options):
|
||||
"""Execute summary queries and assign unaffected-earnings.
|
||||
|
||||
Returns ``[(account_record, {col_group_key: {...}, ...}), ...]``
|
||||
"""
|
||||
combined_sql = self._build_summary_query(report, options)
|
||||
if not combined_sql:
|
||||
return []
|
||||
|
||||
by_account = {}
|
||||
by_company = {}
|
||||
|
||||
self.env.cr.execute(combined_sql)
|
||||
for row in self.env.cr.dictfetchall():
|
||||
if row['groupby'] is None:
|
||||
continue
|
||||
|
||||
cg = row['column_group_key']
|
||||
bucket = row['key']
|
||||
|
||||
if bucket == 'sum':
|
||||
by_account.setdefault(row['groupby'], {k: {} for k in options['column_groups']})
|
||||
by_account[row['groupby']][cg][bucket] = row
|
||||
elif bucket == 'initial_balance':
|
||||
by_account.setdefault(row['groupby'], {k: {} for k in options['column_groups']})
|
||||
by_account[row['groupby']][cg][bucket] = row
|
||||
elif bucket == 'unaffected_earnings':
|
||||
by_company.setdefault(row['groupby'], {k: {} for k in options['column_groups']})
|
||||
by_company[row['groupby']][cg] = row
|
||||
|
||||
# Assign unaffected earnings to the equity_unaffected account
|
||||
if by_company:
|
||||
candidate_accounts = self.env['account.account'].search([
|
||||
('display_name', 'ilike', options.get('filter_search_bar')),
|
||||
*self.env['account.account']._check_company_domain(list(by_company.keys())),
|
||||
('account_type', '=', 'equity_unaffected'),
|
||||
])
|
||||
for comp_id, comp_data in by_company.items():
|
||||
target_acct = candidate_accounts.filtered(
|
||||
lambda a: self.env['res.company'].browse(comp_id).root_id in a.company_ids
|
||||
)
|
||||
if not target_acct:
|
||||
continue
|
||||
|
||||
for cg in options['column_groups']:
|
||||
by_account.setdefault(
|
||||
target_acct.id,
|
||||
{k: {'unaffected_earnings': {}} for k in options['column_groups']},
|
||||
)
|
||||
unaff = comp_data.get(cg)
|
||||
if not unaff:
|
||||
continue
|
||||
existing = by_account[target_acct.id][cg].get('unaffected_earnings')
|
||||
if existing:
|
||||
for fld in ('amount_currency', 'debit', 'credit', 'balance'):
|
||||
existing[fld] = existing.get(fld, 0.0) + unaff[fld]
|
||||
else:
|
||||
by_account[target_acct.id][cg]['unaffected_earnings'] = unaff
|
||||
|
||||
if by_account:
|
||||
accounts = self.env['account.account'].search([('id', 'in', list(by_account.keys()))])
|
||||
else:
|
||||
accounts = self.env['account.account']
|
||||
|
||||
return [(acct, by_account[acct.id]) for acct in accounts]
|
||||
|
||||
def _build_summary_query(self, report, options) -> SQL:
|
||||
"""Construct the UNION ALL query that retrieves period sums and
|
||||
unaffected-earnings sums for every account."""
|
||||
per_col = report._split_options_per_column_group(options)
|
||||
parts = []
|
||||
|
||||
for col_key, grp_opts in per_col.items():
|
||||
# Decide date scope
|
||||
scope = 'strict_range' if grp_opts.get('general_ledger_strict_range') else 'from_beginning'
|
||||
|
||||
domain_extra = []
|
||||
if not grp_opts.get('general_ledger_strict_range'):
|
||||
fy_start = fields.Date.from_string(grp_opts['date']['date_from'])
|
||||
fy_dates = self.env.company.compute_fiscalyear_dates(fy_start)
|
||||
domain_extra += [
|
||||
'|',
|
||||
('date', '>=', fy_dates['date_from']),
|
||||
('account_id.include_initial_balance', '=', True),
|
||||
]
|
||||
|
||||
if grp_opts.get('export_mode') == 'print' and grp_opts.get('filter_search_bar'):
|
||||
domain_extra.append(('account_id', 'ilike', grp_opts['filter_search_bar']))
|
||||
|
||||
if grp_opts.get('include_current_year_in_unaff_earnings'):
|
||||
domain_extra += [('account_id.include_initial_balance', '=', True)]
|
||||
|
||||
qry = report._get_report_query(grp_opts, scope, domain=domain_extra)
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
account_move_line.account_id AS groupby,
|
||||
'sum' AS key,
|
||||
MAX(account_move_line.date) AS max_date,
|
||||
%(col_key)s AS column_group_key,
|
||||
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
%(fx)s
|
||||
WHERE %(cond)s
|
||||
GROUP BY account_move_line.account_id
|
||||
""",
|
||||
col_key=col_key,
|
||||
tbl=qry.from_clause,
|
||||
dr=report._currency_table_apply_rate(SQL("account_move_line.debit")),
|
||||
cr=report._currency_table_apply_rate(SQL("account_move_line.credit")),
|
||||
bal=report._currency_table_apply_rate(SQL("account_move_line.balance")),
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
# Unaffected earnings sub-query
|
||||
if not grp_opts.get('general_ledger_strict_range'):
|
||||
unaff_opts = self._get_options_unaffected_earnings(grp_opts)
|
||||
unaff_domain = [('account_id.include_initial_balance', '=', False)]
|
||||
unaff_qry = report._get_report_query(unaff_opts, 'strict_range', domain=unaff_domain)
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
account_move_line.company_id AS groupby,
|
||||
'unaffected_earnings' AS key,
|
||||
NULL AS max_date,
|
||||
%(col_key)s AS column_group_key,
|
||||
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
%(fx)s
|
||||
WHERE %(cond)s
|
||||
GROUP BY account_move_line.company_id
|
||||
""",
|
||||
col_key=col_key,
|
||||
tbl=unaff_qry.from_clause,
|
||||
dr=report._currency_table_apply_rate(SQL("account_move_line.debit")),
|
||||
cr=report._currency_table_apply_rate(SQL("account_move_line.credit")),
|
||||
bal=report._currency_table_apply_rate(SQL("account_move_line.balance")),
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
cond=unaff_qry.where_clause,
|
||||
))
|
||||
|
||||
return SQL(" UNION ALL ").join(parts)
|
||||
|
||||
def _get_options_unaffected_earnings(self, options):
|
||||
"""Return modified options for computing prior-year unaffected
|
||||
earnings (P&L accounts before the current fiscal year)."""
|
||||
modified = options.copy()
|
||||
modified.pop('filter_search_bar', None)
|
||||
|
||||
fy = self.env.company.compute_fiscalyear_dates(
|
||||
fields.Date.from_string(options['date']['date_from'])
|
||||
)
|
||||
cutoff = (
|
||||
fields.Date.from_string(modified['date']['date_to'])
|
||||
if options.get('include_current_year_in_unaff_earnings')
|
||||
else fy['date_from'] - timedelta(days=1)
|
||||
)
|
||||
modified['date'] = self.env['account.report']._get_dates_period(None, cutoff, 'single')
|
||||
return modified
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# AML detail queries
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _fetch_aml_data(self, report, options, account_ids, offset=0, limit=None):
|
||||
"""Load individual move lines for the given accounts.
|
||||
|
||||
Returns ``({account_id: {(aml_id, date): {col_grp: row}}}, has_more)``
|
||||
"""
|
||||
container = {aid: {} for aid in account_ids}
|
||||
raw_sql = self._build_aml_query(report, options, account_ids, offset=offset, limit=limit)
|
||||
self.env.cr.execute(raw_sql)
|
||||
|
||||
row_count = 0
|
||||
overflow = False
|
||||
for row in self.env.cr.dictfetchall():
|
||||
row_count += 1
|
||||
if row_count == limit:
|
||||
overflow = True
|
||||
break
|
||||
|
||||
# Build a display-friendly communication field
|
||||
if row['ref'] and row['account_type'] != 'asset_receivable':
|
||||
row['communication'] = f"{row['ref']} - {row['name']}"
|
||||
else:
|
||||
row['communication'] = row['name']
|
||||
|
||||
composite_key = (row['id'], row['date'])
|
||||
acct_bucket = container[row['account_id']]
|
||||
|
||||
if composite_key not in acct_bucket:
|
||||
acct_bucket[composite_key] = {cg: {} for cg in options['column_groups']}
|
||||
|
||||
prior = acct_bucket[composite_key][row['column_group_key']]
|
||||
if prior:
|
||||
prior['debit'] += row['debit']
|
||||
prior['credit'] += row['credit']
|
||||
prior['balance'] += row['balance']
|
||||
prior['amount_currency'] += row['amount_currency']
|
||||
else:
|
||||
acct_bucket[composite_key][row['column_group_key']] = row
|
||||
|
||||
return container, overflow
|
||||
|
||||
def _build_aml_query(self, report, options, account_ids, offset=0, limit=None) -> SQL:
|
||||
"""SQL for individual move lines within the strict period range."""
|
||||
extra_domain = [('account_id', 'in', account_ids)] if account_ids is not None else None
|
||||
fragments = []
|
||||
journal_label = self.env['account.journal']._field_to_sql('journal', 'name')
|
||||
|
||||
for col_key, grp_opts in report._split_options_per_column_group(options).items():
|
||||
qry = report._get_report_query(grp_opts, domain=extra_domain, date_scope='strict_range')
|
||||
acct_a = qry.join(
|
||||
lhs_alias='account_move_line', lhs_column='account_id',
|
||||
rhs_table='account_account', rhs_column='id', link='account_id',
|
||||
)
|
||||
code_f = self.env['account.account']._field_to_sql(acct_a, 'code', qry)
|
||||
name_f = self.env['account.account']._field_to_sql(acct_a, 'name')
|
||||
type_f = self.env['account.account']._field_to_sql(acct_a, 'account_type')
|
||||
|
||||
fragments.append(SQL(
|
||||
'''
|
||||
SELECT
|
||||
account_move_line.id,
|
||||
account_move_line.date,
|
||||
account_move_line.date_maturity,
|
||||
account_move_line.name,
|
||||
account_move_line.ref,
|
||||
account_move_line.company_id,
|
||||
account_move_line.account_id,
|
||||
account_move_line.payment_id,
|
||||
account_move_line.partner_id,
|
||||
account_move_line.currency_id,
|
||||
account_move_line.amount_currency,
|
||||
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
|
||||
account_move_line.date AS date,
|
||||
%(dr)s AS debit,
|
||||
%(cr)s AS credit,
|
||||
%(bal)s AS balance,
|
||||
mv.name AS move_name,
|
||||
co.currency_id AS company_currency_id,
|
||||
prt.name AS partner_name,
|
||||
mv.move_type AS move_type,
|
||||
%(code_f)s AS account_code,
|
||||
%(name_f)s AS account_name,
|
||||
%(type_f)s AS account_type,
|
||||
journal.code AS journal_code,
|
||||
%(journal_label)s AS journal_name,
|
||||
fr.id AS full_rec_name,
|
||||
%(col_key)s AS column_group_key
|
||||
FROM %(tbl)s
|
||||
JOIN account_move mv ON mv.id = account_move_line.move_id
|
||||
%(fx)s
|
||||
LEFT JOIN res_company co ON co.id = account_move_line.company_id
|
||||
LEFT JOIN res_partner prt ON prt.id = account_move_line.partner_id
|
||||
LEFT JOIN account_journal journal ON journal.id = account_move_line.journal_id
|
||||
LEFT JOIN account_full_reconcile fr ON fr.id = account_move_line.full_reconcile_id
|
||||
WHERE %(cond)s
|
||||
ORDER BY account_move_line.date, account_move_line.move_name, account_move_line.id
|
||||
''',
|
||||
code_f=code_f,
|
||||
name_f=name_f,
|
||||
type_f=type_f,
|
||||
journal_label=journal_label,
|
||||
col_key=col_key,
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
dr=report._currency_table_apply_rate(SQL("account_move_line.debit")),
|
||||
cr=report._currency_table_apply_rate(SQL("account_move_line.credit")),
|
||||
bal=report._currency_table_apply_rate(SQL("account_move_line.balance")),
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
combined = SQL(" UNION ALL ").join(SQL("(%s)", f) for f in fragments)
|
||||
|
||||
if offset:
|
||||
combined = SQL('%s OFFSET %s ', combined, offset)
|
||||
if limit:
|
||||
combined = SQL('%s LIMIT %s ', combined, limit)
|
||||
|
||||
return combined
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Initial balance
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _fetch_opening_balances(self, report, account_ids, options):
|
||||
"""Compute the opening balance per account at the start of the
|
||||
reporting period."""
|
||||
parts = []
|
||||
for col_key, grp_opts in report._split_options_per_column_group(options).items():
|
||||
init_opts = self._get_options_initial_balance(grp_opts)
|
||||
domain = [('account_id', 'in', account_ids)]
|
||||
|
||||
if not init_opts.get('general_ledger_strict_range'):
|
||||
domain += [
|
||||
'|',
|
||||
('date', '>=', init_opts['date']['date_from']),
|
||||
('account_id.include_initial_balance', '=', True),
|
||||
]
|
||||
if init_opts.get('include_current_year_in_unaff_earnings'):
|
||||
domain += [('account_id.include_initial_balance', '=', True)]
|
||||
|
||||
qry = report._get_report_query(init_opts, 'from_beginning', domain=domain)
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
account_move_line.account_id AS groupby,
|
||||
'initial_balance' AS key,
|
||||
NULL AS max_date,
|
||||
%(col_key)s AS column_group_key,
|
||||
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
%(fx)s
|
||||
WHERE %(cond)s
|
||||
GROUP BY account_move_line.account_id
|
||||
""",
|
||||
col_key=col_key,
|
||||
tbl=qry.from_clause,
|
||||
dr=report._currency_table_apply_rate(SQL("account_move_line.debit")),
|
||||
cr=report._currency_table_apply_rate(SQL("account_move_line.credit")),
|
||||
bal=report._currency_table_apply_rate(SQL("account_move_line.balance")),
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
self.env.cr.execute(SQL(" UNION ALL ").join(parts))
|
||||
|
||||
init_map = {
|
||||
aid: {cg: {} for cg in options['column_groups']}
|
||||
for aid in account_ids
|
||||
}
|
||||
for row in self.env.cr.dictfetchall():
|
||||
init_map[row['groupby']][row['column_group_key']] = row
|
||||
|
||||
accts = self.env['account.account'].browse(account_ids)
|
||||
return {a.id: (a, init_map[a.id]) for a in accts}
|
||||
|
||||
def _get_options_initial_balance(self, options):
|
||||
"""Derive an options dict whose date range ends just before the
|
||||
report's ``date_from``, suitable for computing opening balances."""
|
||||
derived = options.copy()
|
||||
|
||||
# End date
|
||||
raw_to = (
|
||||
derived['comparison']['periods'][-1]['date_from']
|
||||
if derived.get('comparison', {}).get('periods')
|
||||
else derived['date']['date_from']
|
||||
)
|
||||
end_dt = fields.Date.from_string(raw_to) - timedelta(days=1)
|
||||
|
||||
# Start date: if date_from aligns with a fiscal-year boundary take the
|
||||
# previous FY; otherwise use the current FY start.
|
||||
start_dt = fields.Date.from_string(derived['date']['date_from'])
|
||||
fy = self.env.company.compute_fiscalyear_dates(start_dt)
|
||||
|
||||
if start_dt == fy['date_from']:
|
||||
prev_fy = self.env.company.compute_fiscalyear_dates(start_dt - timedelta(days=1))
|
||||
begin_dt = prev_fy['date_from']
|
||||
include_curr_yr = True
|
||||
else:
|
||||
begin_dt = fy['date_from']
|
||||
include_curr_yr = False
|
||||
|
||||
derived['date'] = self.env['account.report']._get_dates_period(begin_dt, end_dt, 'range')
|
||||
derived['include_current_year_in_unaff_earnings'] = include_curr_yr
|
||||
return derived
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Line builders
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_account_header_line(self, report, options, account, has_entries, col_data):
|
||||
"""Produce the foldable account-level line."""
|
||||
cols = []
|
||||
for col_def in options['columns']:
|
||||
expr = col_def['expression_label']
|
||||
raw = col_data.get(col_def['column_group_key'], {}).get(expr)
|
||||
|
||||
display_val = (
|
||||
None
|
||||
if raw is None or (expr == 'amount_currency' and not account.currency_id)
|
||||
else raw
|
||||
)
|
||||
cols.append(report._build_column_dict(
|
||||
display_val, col_def, options=options,
|
||||
currency=account.currency_id if expr == 'amount_currency' else None,
|
||||
))
|
||||
|
||||
lid = report._get_generic_line_id('account.account', account.id)
|
||||
is_unfolded = any(
|
||||
report._get_res_id_from_line_id(ul, 'account.account') == account.id
|
||||
for ul in options.get('unfolded_lines')
|
||||
)
|
||||
|
||||
return {
|
||||
'id': lid,
|
||||
'name': account.display_name,
|
||||
'columns': cols,
|
||||
'level': 1,
|
||||
'unfoldable': has_entries,
|
||||
'unfolded': has_entries and (is_unfolded or options.get('unfold_all')),
|
||||
'expand_function': '_report_expand_unfoldable_line_general_ledger',
|
||||
}
|
||||
|
||||
def _get_aml_line(self, report, parent_line_id, options, col_dict, running_bal):
|
||||
"""Build a single move-line row under a given account header."""
|
||||
cols = []
|
||||
for col_def in options['columns']:
|
||||
expr = col_def['expression_label']
|
||||
raw = col_dict[col_def['column_group_key']].get(expr)
|
||||
cur = None
|
||||
|
||||
if raw is not None:
|
||||
if expr == 'amount_currency':
|
||||
cur = self.env['res.currency'].browse(col_dict[col_def['column_group_key']]['currency_id'])
|
||||
raw = None if cur == self.env.company.currency_id else raw
|
||||
elif expr == 'balance':
|
||||
raw += (running_bal[col_def['column_group_key']] or 0)
|
||||
|
||||
cols.append(report._build_column_dict(raw, col_def, options=options, currency=cur))
|
||||
|
||||
aml_id = None
|
||||
move_label = None
|
||||
caret = None
|
||||
row_date = None
|
||||
for grp_data in col_dict.values():
|
||||
aml_id = grp_data.get('id', '')
|
||||
if aml_id:
|
||||
caret = 'account.payment' if grp_data.get('payment_id') else 'account.move.line'
|
||||
move_label = grp_data['move_name']
|
||||
row_date = str(grp_data.get('date', ''))
|
||||
break
|
||||
|
||||
return {
|
||||
'id': report._get_generic_line_id(
|
||||
'account.move.line', aml_id,
|
||||
parent_line_id=parent_line_id, markup=row_date,
|
||||
),
|
||||
'caret_options': caret,
|
||||
'parent_id': parent_line_id,
|
||||
'name': move_label,
|
||||
'columns': cols,
|
||||
'level': 3,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _build_grand_total_line(self, report, options, col_totals):
|
||||
"""Build the bottom total row."""
|
||||
cols = []
|
||||
for col_def in options['columns']:
|
||||
raw = col_totals[col_def['column_group_key']].get(col_def['expression_label'])
|
||||
cols.append(report._build_column_dict(raw if raw is not None else None, col_def, options=options))
|
||||
|
||||
return {
|
||||
'id': report._get_generic_line_id(None, None, markup='total'),
|
||||
'name': _('Total'),
|
||||
'level': 1,
|
||||
'columns': cols,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Caret / expand handlers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def caret_option_audit_tax(self, options, params):
|
||||
return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(options, params)
|
||||
|
||||
def _report_expand_unfoldable_line_general_ledger(
|
||||
self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None,
|
||||
):
|
||||
"""Called when an account line is unfolded. Returns initial-balance,
|
||||
individual AML lines, and load-more metadata."""
|
||||
|
||||
def _extract_running_balance(line_dict):
|
||||
return {
|
||||
c['column_group_key']: lc.get('no_format', 0)
|
||||
for c, lc in zip(options['columns'], line_dict['columns'])
|
||||
if c['expression_label'] == 'balance'
|
||||
}
|
||||
|
||||
report = self.env.ref('fusion_accounting.general_ledger_report')
|
||||
mdl, mdl_id = report._get_model_info_from_id(line_dict_id)
|
||||
if mdl != 'account.account':
|
||||
raise UserError(_("Invalid line ID for general ledger expansion: %s", line_dict_id))
|
||||
|
||||
lines = []
|
||||
|
||||
# Opening balance (only on first page)
|
||||
if offset == 0:
|
||||
if unfold_all_batch_data:
|
||||
acct_rec, init_by_cg = unfold_all_batch_data['initial_balances'][mdl_id]
|
||||
else:
|
||||
acct_rec, init_by_cg = self._fetch_opening_balances(report, [mdl_id], options)[mdl_id]
|
||||
|
||||
opening_line = report._get_partner_and_general_ledger_initial_balance_line(
|
||||
options, line_dict_id, init_by_cg, acct_rec.currency_id,
|
||||
)
|
||||
if opening_line:
|
||||
lines.append(opening_line)
|
||||
progress = _extract_running_balance(opening_line)
|
||||
|
||||
# Move lines
|
||||
page_size = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None
|
||||
if unfold_all_batch_data:
|
||||
aml_rows = unfold_all_batch_data['aml_results'][mdl_id]
|
||||
has_more = unfold_all_batch_data['has_more'].get(mdl_id, False)
|
||||
else:
|
||||
aml_rows, has_more = self._fetch_aml_data(report, options, [mdl_id], offset=offset, limit=page_size)
|
||||
aml_rows = aml_rows[mdl_id]
|
||||
|
||||
running = progress
|
||||
for entry in aml_rows.values():
|
||||
row_line = self._get_aml_line(report, line_dict_id, options, entry, running)
|
||||
lines.append(row_line)
|
||||
running = _extract_running_balance(row_line)
|
||||
|
||||
return {
|
||||
'lines': lines,
|
||||
'offset_increment': report.load_more_limit,
|
||||
'has_more': has_more,
|
||||
'progress': running,
|
||||
}
|
||||
Reference in New Issue
Block a user