Initial commit
This commit is contained in:
799
Fusion Accounting/models/account_partner_ledger.py
Normal file
799
Fusion Accounting/models/account_partner_ledger.py
Normal file
@@ -0,0 +1,799 @@
|
||||
# Fusion Accounting - Partner Ledger Report Handler
|
||||
|
||||
from odoo import api, models, _, fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import SQL
|
||||
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class PartnerLedgerCustomHandler(models.AbstractModel):
|
||||
"""Generates the Partner Ledger report.
|
||||
|
||||
Shows journal items grouped by partner, with initial balances and
|
||||
running totals. Also handles indirectly-linked entries (items
|
||||
without a partner that were reconciled with a partner's entry).
|
||||
"""
|
||||
|
||||
_name = 'account.partner.ledger.report.handler'
|
||||
_inherit = 'account.report.custom.handler'
|
||||
_description = 'Partner Ledger Custom Handler'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Display
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_custom_display_config(self):
|
||||
return {
|
||||
'css_custom_class': 'partner_ledger',
|
||||
'components': {
|
||||
'AccountReportLineCell': 'fusion_accounting.PartnerLedgerLineCell',
|
||||
},
|
||||
'templates': {
|
||||
'AccountReportFilters': 'fusion_accounting.PartnerLedgerFilters',
|
||||
'AccountReportLineName': 'fusion_accounting.PartnerLedgerLineName',
|
||||
},
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dynamic lines
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
|
||||
"""Build all partner lines and a final total line."""
|
||||
partner_rows, col_totals = self._assemble_partner_rows(report, options)
|
||||
|
||||
output = report._regroup_lines_by_name_prefix(
|
||||
options, partner_rows,
|
||||
'_report_expand_unfoldable_line_partner_ledger_prefix_group', 0,
|
||||
)
|
||||
output = [(0, ln) for ln in output]
|
||||
output.append((0, self._build_total_line(options, col_totals)))
|
||||
return output
|
||||
|
||||
def _assemble_partner_rows(self, report, options, depth_shift=0):
|
||||
"""Query partner sums and return ``(lines, totals_by_column_group)``."""
|
||||
rows = []
|
||||
|
||||
col_totals = {
|
||||
cg: {'debit': 0.0, 'credit': 0.0, 'amount': 0.0, 'balance': 0.0}
|
||||
for cg in options['column_groups']
|
||||
}
|
||||
|
||||
partner_data = self._query_partner_sums(options)
|
||||
|
||||
filter_text = options.get('filter_search_bar', '')
|
||||
accept_unknown = filter_text.lower() in self._unknown_partner_label().lower()
|
||||
|
||||
for partner_rec, col_vals in partner_data:
|
||||
# When printing with a search filter, skip the Unknown Partner row
|
||||
# unless the filter matches its label.
|
||||
if (
|
||||
options['export_mode'] == 'print'
|
||||
and filter_text
|
||||
and not partner_rec
|
||||
and not accept_unknown
|
||||
):
|
||||
continue
|
||||
|
||||
per_col = defaultdict(dict)
|
||||
for cg in options['column_groups']:
|
||||
psum = col_vals.get(cg, {})
|
||||
per_col[cg]['debit'] = psum.get('debit', 0.0)
|
||||
per_col[cg]['credit'] = psum.get('credit', 0.0)
|
||||
per_col[cg]['amount'] = psum.get('amount', 0.0)
|
||||
per_col[cg]['balance'] = psum.get('balance', 0.0)
|
||||
|
||||
for fld in ('debit', 'credit', 'amount', 'balance'):
|
||||
col_totals[cg][fld] += per_col[cg][fld]
|
||||
|
||||
rows.append(
|
||||
self._build_partner_line(options, partner_rec, per_col, depth_shift=depth_shift)
|
||||
)
|
||||
|
||||
return rows, col_totals
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Prefix-group expand
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _report_expand_unfoldable_line_partner_ledger_prefix_group(
|
||||
self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None,
|
||||
):
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
prefix = report._get_prefix_groups_matched_prefix_from_line_id(line_dict_id)
|
||||
|
||||
prefix_filter = [('partner_id.name', '=ilike', f'{prefix}%')]
|
||||
if self._unknown_partner_label().upper().startswith(prefix):
|
||||
prefix_filter = expression.OR([prefix_filter, [('partner_id', '=', None)]])
|
||||
|
||||
filtered_opts = {
|
||||
**options,
|
||||
'forced_domain': options.get('forced_domain', []) + prefix_filter,
|
||||
}
|
||||
nest_level = len(prefix) * 2
|
||||
child_lines, _ = self._assemble_partner_rows(report, filtered_opts, depth_shift=nest_level)
|
||||
|
||||
for child in child_lines:
|
||||
child['id'] = report._build_subline_id(line_dict_id, child['id'])
|
||||
child['parent_id'] = line_dict_id
|
||||
|
||||
grouped_output = report._regroup_lines_by_name_prefix(
|
||||
options, child_lines,
|
||||
'_report_expand_unfoldable_line_partner_ledger_prefix_group',
|
||||
nest_level,
|
||||
matched_prefix=prefix,
|
||||
parent_line_dict_id=line_dict_id,
|
||||
)
|
||||
return {
|
||||
'lines': grouped_output,
|
||||
'offset_increment': len(grouped_output),
|
||||
'has_more': False,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Options
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _custom_options_initializer(self, report, options, previous_options):
|
||||
super()._custom_options_initializer(report, options, previous_options=previous_options)
|
||||
|
||||
extra_domain = []
|
||||
company_ids = report.get_report_company_ids(options)
|
||||
fx_journals = self.env['res.company'].browse(company_ids).mapped('currency_exchange_journal_id')
|
||||
if fx_journals:
|
||||
extra_domain += [
|
||||
'!', '&', '&', '&',
|
||||
('credit', '=', 0.0),
|
||||
('debit', '=', 0.0),
|
||||
('amount_currency', '!=', 0.0),
|
||||
('journal_id', 'in', fx_journals.ids),
|
||||
]
|
||||
|
||||
if options['export_mode'] == 'print' and options.get('filter_search_bar'):
|
||||
extra_domain += [
|
||||
'|', ('matched_debit_ids.debit_move_id.partner_id.name', 'ilike', options['filter_search_bar']),
|
||||
'|', ('matched_credit_ids.credit_move_id.partner_id.name', 'ilike', options['filter_search_bar']),
|
||||
('partner_id.name', 'ilike', options['filter_search_bar']),
|
||||
]
|
||||
|
||||
options['forced_domain'] = options.get('forced_domain', []) + extra_domain
|
||||
|
||||
if self.env.user.has_group('base.group_multi_currency'):
|
||||
options['multi_currency'] = True
|
||||
|
||||
hidden_cols = []
|
||||
options['hide_account'] = (previous_options or {}).get('hide_account', False)
|
||||
if options['hide_account']:
|
||||
hidden_cols += ['journal_code', 'account_code', 'matching_number']
|
||||
|
||||
options['hide_debit_credit'] = (previous_options or {}).get('hide_debit_credit', False)
|
||||
if options['hide_debit_credit']:
|
||||
hidden_cols += ['debit', 'credit']
|
||||
else:
|
||||
hidden_cols += ['amount']
|
||||
|
||||
options['columns'] = [c for c in options['columns'] if c['expression_label'] not in hidden_cols]
|
||||
|
||||
options['buttons'].append({
|
||||
'name': _('Send'),
|
||||
'action': 'action_send_statements',
|
||||
'sequence': 90,
|
||||
'always_show': True,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Batch unfold
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
|
||||
partner_ids = []
|
||||
|
||||
for ld in lines_to_expand_by_function.get('_report_expand_unfoldable_line_partner_ledger', []):
|
||||
markup, mdl, mid = self.env['account.report']._parse_line_id(ld['id'])[-1]
|
||||
if mdl == 'res.partner':
|
||||
partner_ids.append(mid)
|
||||
elif markup == 'no_partner':
|
||||
partner_ids.append(None)
|
||||
|
||||
# Prefix-group expansion
|
||||
unknown_label_upper = self._unknown_partner_label().upper()
|
||||
prefix_domains = []
|
||||
for ld in lines_to_expand_by_function.get(
|
||||
'_report_expand_unfoldable_line_partner_ledger_prefix_group', [],
|
||||
):
|
||||
pfx = report._get_prefix_groups_matched_prefix_from_line_id(ld['id'])
|
||||
prefix_domains.append([('name', '=ilike', f'{pfx}%')])
|
||||
if unknown_label_upper.startswith(pfx):
|
||||
partner_ids.append(None)
|
||||
|
||||
if prefix_domains:
|
||||
partner_ids += self.env['res.partner'].with_context(active_test=False).search(
|
||||
expression.OR(prefix_domains)
|
||||
).ids
|
||||
|
||||
return {
|
||||
'initial_balances': self._fetch_initial_balances(partner_ids, options) if partner_ids else {},
|
||||
'aml_values': self._fetch_aml_data(options, partner_ids) if partner_ids else {},
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_report_send_recipients(self, options):
|
||||
preset_ids = options.get('partner_ids', [])
|
||||
if not preset_ids:
|
||||
self.env.cr.execute(self._build_partner_sums_sql(options))
|
||||
preset_ids = [r['groupby'] for r in self.env.cr.dictfetchall() if r['groupby']]
|
||||
return self.env['res.partner'].browse(preset_ids)
|
||||
|
||||
def action_send_statements(self, options):
|
||||
tpl = self.env.ref('fusion_accounting.email_template_customer_statement', False)
|
||||
return {
|
||||
'name': _("Send Partner Ledgers"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [[False, 'form']],
|
||||
'res_model': 'account.report.send',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_mail_template_id': tpl.id if tpl else False,
|
||||
'default_report_options': options,
|
||||
},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def action_open_partner(self, options, params):
|
||||
_, rec_id = self.env['account.report']._get_model_info_from_id(params['id'])
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'res_id': rec_id,
|
||||
'views': [[False, 'form']],
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SQL helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _query_partner_sums(self, options):
|
||||
"""Fetch sums grouped by partner and apply corrections for
|
||||
partnerless entries reconciled with partnered entries."""
|
||||
comp_cur = self.env.company.currency_id
|
||||
|
||||
def _assign_if_nonzero(row):
|
||||
check_fields = ['balance', 'debit', 'credit', 'amount']
|
||||
if any(not comp_cur.is_zero(row[f]) for f in check_fields):
|
||||
by_partner.setdefault(row['groupby'], defaultdict(lambda: defaultdict(float)))
|
||||
for f in check_fields:
|
||||
by_partner[row['groupby']][row['column_group_key']][f] += row[f]
|
||||
|
||||
by_partner = {}
|
||||
|
||||
self.env.cr.execute(self._build_partner_sums_sql(options))
|
||||
for rec in self.env.cr.dictfetchall():
|
||||
_assign_if_nonzero(rec)
|
||||
|
||||
# Correction: partnerless entries reconciled with a partner
|
||||
self.env.cr.execute(self._build_partnerless_correction_sql(options))
|
||||
correction_sums = {f: {cg: 0 for cg in options['column_groups']} for f in ('debit', 'credit', 'amount', 'balance')}
|
||||
|
||||
for rec in self.env.cr.dictfetchall():
|
||||
for f in ('debit', 'credit', 'amount', 'balance'):
|
||||
correction_sums[f][rec['column_group_key']] += rec[f]
|
||||
|
||||
if rec['groupby'] in by_partner:
|
||||
_assign_if_nonzero(rec)
|
||||
|
||||
# Adjust the Unknown Partner bucket
|
||||
if None in by_partner:
|
||||
for cg in options['column_groups']:
|
||||
by_partner[None][cg]['debit'] += correction_sums['credit'][cg]
|
||||
by_partner[None][cg]['credit'] += correction_sums['debit'][cg]
|
||||
by_partner[None][cg]['amount'] += correction_sums['amount'][cg]
|
||||
by_partner[None][cg]['balance'] -= correction_sums['balance'][cg]
|
||||
|
||||
if by_partner:
|
||||
partners = self.env['res.partner'].with_context(active_test=False).search_fetch(
|
||||
[('id', 'in', list(by_partner.keys()))],
|
||||
["id", "name", "trust", "company_registry", "vat"],
|
||||
)
|
||||
else:
|
||||
partners = self.env['res.partner']
|
||||
|
||||
if None in by_partner:
|
||||
partners = list(partners) + [None]
|
||||
|
||||
return [(p, by_partner[p.id if p else None]) for p in partners]
|
||||
|
||||
def _build_partner_sums_sql(self, options) -> SQL:
|
||||
"""SQL that sums debit / credit / balance by partner."""
|
||||
parts = []
|
||||
report = self.env.ref('fusion_accounting.partner_ledger_report')
|
||||
|
||||
for cg, cg_opts in report._split_options_per_column_group(options).items():
|
||||
qry = report._get_report_query(cg_opts, 'from_beginning')
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
account_move_line.partner_id AS groupby,
|
||||
%(cg)s AS column_group_key,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS amount,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
%(fx)s
|
||||
WHERE %(cond)s
|
||||
GROUP BY account_move_line.partner_id
|
||||
""",
|
||||
cg=cg,
|
||||
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")),
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(cg_opts),
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
return SQL(' UNION ALL ').join(parts)
|
||||
|
||||
def _fetch_initial_balances(self, partner_ids, options):
|
||||
"""Compute opening balances for each partner before date_from."""
|
||||
parts = []
|
||||
report = self.env.ref('fusion_accounting.partner_ledger_report')
|
||||
|
||||
for cg, cg_opts in report._split_options_per_column_group(options).items():
|
||||
init_opts = self._derive_initial_balance_options(cg_opts)
|
||||
qry = report._get_report_query(
|
||||
init_opts, 'from_beginning', domain=[('partner_id', 'in', partner_ids)],
|
||||
)
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
account_move_line.partner_id,
|
||||
%(cg)s AS column_group_key,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS amount,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
%(fx)s
|
||||
WHERE %(cond)s
|
||||
GROUP BY account_move_line.partner_id
|
||||
""",
|
||||
cg=cg,
|
||||
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")),
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(cg_opts),
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
self.env.cr.execute(SQL(" UNION ALL ").join(parts))
|
||||
|
||||
init_map = {
|
||||
pid: {cg: {} for cg in options['column_groups']}
|
||||
for pid in partner_ids
|
||||
}
|
||||
for row in self.env.cr.dictfetchall():
|
||||
init_map[row['partner_id']][row['column_group_key']] = row
|
||||
|
||||
return init_map
|
||||
|
||||
def _derive_initial_balance_options(self, options):
|
||||
"""Return a modified options dict ending the day before ``date_from``."""
|
||||
cutoff = fields.Date.from_string(options['date']['date_from']) - timedelta(days=1)
|
||||
new_date = dict(options['date'], date_from=False, date_to=fields.Date.to_string(cutoff))
|
||||
return dict(options, date=new_date)
|
||||
|
||||
def _build_partnerless_correction_sql(self, options):
|
||||
"""SQL for partnerless lines reconciled with a partner's line."""
|
||||
parts = []
|
||||
report = self.env.ref('fusion_accounting.partner_ledger_report')
|
||||
|
||||
for cg, cg_opts in report._split_options_per_column_group(options).items():
|
||||
qry = report._get_report_query(cg_opts, 'from_beginning')
|
||||
parts.append(SQL(
|
||||
"""
|
||||
SELECT
|
||||
%(cg)s AS column_group_key,
|
||||
linked.partner_id AS groupby,
|
||||
SUM(%(dr)s) AS debit,
|
||||
SUM(%(cr)s) AS credit,
|
||||
SUM(%(bal)s) AS amount,
|
||||
SUM(%(bal)s) AS balance
|
||||
FROM %(tbl)s
|
||||
JOIN account_partial_reconcile pr
|
||||
ON account_move_line.id = pr.debit_move_id
|
||||
OR account_move_line.id = pr.credit_move_id
|
||||
JOIN account_move_line linked ON
|
||||
(linked.id = pr.debit_move_id OR linked.id = pr.credit_move_id)
|
||||
AND linked.partner_id IS NOT NULL
|
||||
%(fx)s
|
||||
WHERE pr.max_date <= %(dt_to)s AND %(cond)s
|
||||
AND account_move_line.partner_id IS NULL
|
||||
GROUP BY linked.partner_id
|
||||
""",
|
||||
cg=cg,
|
||||
dr=report._currency_table_apply_rate(SQL(
|
||||
"CASE WHEN linked.balance > 0 THEN 0 ELSE pr.amount END"
|
||||
)),
|
||||
cr=report._currency_table_apply_rate(SQL(
|
||||
"CASE WHEN linked.balance < 0 THEN 0 ELSE pr.amount END"
|
||||
)),
|
||||
bal=report._currency_table_apply_rate(SQL(
|
||||
"-SIGN(linked.balance) * pr.amount"
|
||||
)),
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(cg_opts, aml_alias=SQL("linked")),
|
||||
dt_to=cg_opts['date']['date_to'],
|
||||
cond=qry.where_clause,
|
||||
))
|
||||
|
||||
return SQL(" UNION ALL ").join(parts)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# AML detail data
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_additional_column_aml_values(self):
|
||||
"""Hook for other modules to inject extra SELECT fields into the
|
||||
partner-ledger AML query."""
|
||||
return SQL()
|
||||
|
||||
def _fetch_aml_data(self, options, partner_ids, offset=0, limit=None):
|
||||
"""Load move lines for the given partners.
|
||||
|
||||
Returns ``{partner_id: [row, ...]}`` including both directly- and
|
||||
indirectly-linked entries.
|
||||
"""
|
||||
container = {pid: [] for pid in partner_ids}
|
||||
|
||||
real_ids = [x for x in partner_ids if x]
|
||||
direct_clauses = []
|
||||
indirect_clause = SQL('linked_aml.partner_id IS NOT NULL')
|
||||
|
||||
if None in partner_ids:
|
||||
direct_clauses.append(SQL('account_move_line.partner_id IS NULL'))
|
||||
if real_ids:
|
||||
direct_clauses.append(SQL('account_move_line.partner_id IN %s', tuple(real_ids)))
|
||||
indirect_clause = SQL('linked_aml.partner_id IN %s', tuple(real_ids))
|
||||
|
||||
direct_filter = SQL('(%s)', SQL(' OR ').join(direct_clauses))
|
||||
|
||||
fragments = []
|
||||
jnl_name = self.env['account.journal']._field_to_sql('journal', 'name')
|
||||
report = self.env.ref('fusion_accounting.partner_ledger_report')
|
||||
extra_cols = self._get_additional_column_aml_values()
|
||||
|
||||
for cg, grp_opts in report._split_options_per_column_group(options).items():
|
||||
qry = report._get_report_query(grp_opts, 'strict_range')
|
||||
acct_a = qry.left_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')
|
||||
|
||||
# Direct entries
|
||||
fragments.append(SQL(
|
||||
'''
|
||||
SELECT
|
||||
account_move_line.id,
|
||||
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,
|
||||
account_move_line.matching_number,
|
||||
%(extra_cols)s
|
||||
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
|
||||
%(dr)s AS debit,
|
||||
%(cr)s AS credit,
|
||||
%(bal)s AS amount,
|
||||
%(bal)s AS balance,
|
||||
mv.name AS move_name,
|
||||
mv.move_type AS move_type,
|
||||
%(code_f)s AS account_code,
|
||||
%(name_f)s AS account_name,
|
||||
journal.code AS journal_code,
|
||||
%(jnl_name)s AS journal_name,
|
||||
%(cg)s AS column_group_key,
|
||||
'directly_linked_aml' AS key,
|
||||
0 AS partial_id
|
||||
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
|
||||
WHERE %(cond)s AND %(direct_filter)s
|
||||
ORDER BY account_move_line.date, account_move_line.id
|
||||
''',
|
||||
extra_cols=extra_cols,
|
||||
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")),
|
||||
code_f=code_f,
|
||||
name_f=name_f,
|
||||
jnl_name=jnl_name,
|
||||
cg=cg,
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
cond=qry.where_clause,
|
||||
direct_filter=direct_filter,
|
||||
))
|
||||
|
||||
# Indirect (reconciled with a partner but no partner on the line)
|
||||
fragments.append(SQL(
|
||||
'''
|
||||
SELECT
|
||||
account_move_line.id,
|
||||
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,
|
||||
linked_aml.partner_id,
|
||||
account_move_line.currency_id,
|
||||
account_move_line.amount_currency,
|
||||
account_move_line.matching_number,
|
||||
%(extra_cols)s
|
||||
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
|
||||
%(dr)s AS debit,
|
||||
%(cr)s AS credit,
|
||||
%(bal)s AS amount,
|
||||
%(bal)s AS balance,
|
||||
mv.name AS move_name,
|
||||
mv.move_type AS move_type,
|
||||
%(code_f)s AS account_code,
|
||||
%(name_f)s AS account_name,
|
||||
journal.code AS journal_code,
|
||||
%(jnl_name)s AS journal_name,
|
||||
%(cg)s AS column_group_key,
|
||||
'indirectly_linked_aml' AS key,
|
||||
pr.id AS partial_id
|
||||
FROM %(tbl)s
|
||||
%(fx)s,
|
||||
account_partial_reconcile pr,
|
||||
account_move mv,
|
||||
account_move_line linked_aml,
|
||||
account_journal journal
|
||||
WHERE
|
||||
(account_move_line.id = pr.debit_move_id OR account_move_line.id = pr.credit_move_id)
|
||||
AND account_move_line.partner_id IS NULL
|
||||
AND mv.id = account_move_line.move_id
|
||||
AND (linked_aml.id = pr.debit_move_id OR linked_aml.id = pr.credit_move_id)
|
||||
AND %(indirect_clause)s
|
||||
AND journal.id = account_move_line.journal_id
|
||||
AND %(acct_alias)s.id = account_move_line.account_id
|
||||
AND %(cond)s
|
||||
AND pr.max_date BETWEEN %(dt_from)s AND %(dt_to)s
|
||||
ORDER BY account_move_line.date, account_move_line.id
|
||||
''',
|
||||
extra_cols=extra_cols,
|
||||
dr=report._currency_table_apply_rate(SQL(
|
||||
"CASE WHEN linked_aml.balance > 0 THEN 0 ELSE pr.amount END"
|
||||
)),
|
||||
cr=report._currency_table_apply_rate(SQL(
|
||||
"CASE WHEN linked_aml.balance < 0 THEN 0 ELSE pr.amount END"
|
||||
)),
|
||||
bal=report._currency_table_apply_rate(SQL("-SIGN(linked_aml.balance) * pr.amount")),
|
||||
code_f=code_f,
|
||||
name_f=name_f,
|
||||
jnl_name=jnl_name,
|
||||
cg=cg,
|
||||
tbl=qry.from_clause,
|
||||
fx=report._currency_table_aml_join(grp_opts),
|
||||
indirect_clause=indirect_clause,
|
||||
acct_alias=SQL.identifier(acct_a),
|
||||
cond=qry.where_clause,
|
||||
dt_from=grp_opts['date']['date_from'],
|
||||
dt_to=grp_opts['date']['date_to'],
|
||||
))
|
||||
|
||||
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)
|
||||
|
||||
self.env.cr.execute(combined)
|
||||
for row in self.env.cr.dictfetchall():
|
||||
if row['key'] == 'indirectly_linked_aml':
|
||||
if row['partner_id'] in container:
|
||||
container[row['partner_id']].append(row)
|
||||
if None in container:
|
||||
container[None].append({
|
||||
**row,
|
||||
'debit': row['credit'],
|
||||
'credit': row['debit'],
|
||||
'amount': row['credit'] - row['debit'],
|
||||
'balance': -row['balance'],
|
||||
})
|
||||
else:
|
||||
container[row['partner_id']].append(row)
|
||||
|
||||
return container
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Expand handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _report_expand_unfoldable_line_partner_ledger(
|
||||
self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None,
|
||||
):
|
||||
def _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.partner_ledger_report')
|
||||
_, mdl, rec_id = report._parse_line_id(line_dict_id)[-1]
|
||||
|
||||
if mdl != 'res.partner':
|
||||
raise UserError(_("Invalid line ID for partner ledger expansion: %s", line_dict_id))
|
||||
|
||||
# Count prefix-group nesting levels
|
||||
nesting = sum(
|
||||
1 for mk, _, _ in report._parse_line_id(line_dict_id)
|
||||
if isinstance(mk, dict) and 'groupby_prefix_group' in mk
|
||||
)
|
||||
depth = nesting * 2
|
||||
lines = []
|
||||
|
||||
# Opening balance
|
||||
if offset == 0:
|
||||
if unfold_all_batch_data:
|
||||
init_by_cg = unfold_all_batch_data['initial_balances'][rec_id]
|
||||
else:
|
||||
init_by_cg = self._fetch_initial_balances([rec_id], options)[rec_id]
|
||||
|
||||
opening_line = report._get_partner_and_general_ledger_initial_balance_line(
|
||||
options, line_dict_id, init_by_cg, level_shift=depth,
|
||||
)
|
||||
if opening_line:
|
||||
lines.append(opening_line)
|
||||
progress = _running_balance(opening_line)
|
||||
|
||||
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_values'][rec_id]
|
||||
else:
|
||||
aml_rows = self._fetch_aml_data(options, [rec_id], offset=offset, limit=page_size)[rec_id]
|
||||
|
||||
overflow = False
|
||||
count = 0
|
||||
running = progress
|
||||
for row in aml_rows:
|
||||
if options['export_mode'] != 'print' and report.load_more_limit and count >= report.load_more_limit:
|
||||
overflow = True
|
||||
break
|
||||
new_line = self._build_aml_line(options, row, line_dict_id, running, depth_shift=depth)
|
||||
lines.append(new_line)
|
||||
running = _running_balance(new_line)
|
||||
count += 1
|
||||
|
||||
return {
|
||||
'lines': lines,
|
||||
'offset_increment': count,
|
||||
'has_more': overflow,
|
||||
'progress': running,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Line builders
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_partner_line(self, options, partner, col_data, depth_shift=0):
|
||||
"""Produce the foldable partner-level line."""
|
||||
comp_cur = self.env.company.currency_id
|
||||
first_vals = next(iter(col_data.values()))
|
||||
can_unfold = not comp_cur.is_zero(first_vals.get('debit', 0) or first_vals.get('credit', 0))
|
||||
|
||||
cols = []
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
for col_def in options['columns']:
|
||||
expr = col_def['expression_label']
|
||||
raw = col_data[col_def['column_group_key']].get(expr)
|
||||
can_unfold = can_unfold or (
|
||||
expr in ('debit', 'credit', 'amount') and not comp_cur.is_zero(raw)
|
||||
)
|
||||
cols.append(report._build_column_dict(raw, col_def, options=options))
|
||||
|
||||
if partner:
|
||||
lid = report._get_generic_line_id('res.partner', partner.id)
|
||||
else:
|
||||
lid = report._get_generic_line_id('res.partner', None, markup='no_partner')
|
||||
|
||||
return {
|
||||
'id': lid,
|
||||
'name': (partner.name or '')[:128] if partner else self._unknown_partner_label(),
|
||||
'columns': cols,
|
||||
'level': 1 + depth_shift,
|
||||
'trust': partner.trust if partner else None,
|
||||
'unfoldable': can_unfold,
|
||||
'unfolded': lid in options['unfolded_lines'] or options['unfold_all'],
|
||||
'expand_function': '_report_expand_unfoldable_line_partner_ledger',
|
||||
}
|
||||
|
||||
def _unknown_partner_label(self):
|
||||
return _('Unknown Partner')
|
||||
|
||||
@api.model
|
||||
def _format_aml_name(self, line_name, move_ref, move_name=None):
|
||||
"""Format the display name for a move line."""
|
||||
return self.env['account.move.line']._format_aml_name(line_name, move_ref, move_name=move_name)
|
||||
|
||||
def _build_aml_line(self, options, row, parent_id, running_bal, depth_shift=0):
|
||||
"""Build a single move-line row under its partner."""
|
||||
caret = 'account.payment' if row['payment_id'] else 'account.move.line'
|
||||
|
||||
cols = []
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
for col_def in options['columns']:
|
||||
expr = col_def['expression_label']
|
||||
|
||||
if expr not in row:
|
||||
raise UserError(_("Column '%s' is unavailable for this report.", expr))
|
||||
|
||||
raw = row[expr] if col_def['column_group_key'] == row['column_group_key'] else None
|
||||
if raw is None:
|
||||
cols.append(report._build_column_dict(None, None))
|
||||
continue
|
||||
|
||||
cur = False
|
||||
if expr == 'balance':
|
||||
raw += running_bal[col_def['column_group_key']]
|
||||
if expr == 'amount_currency':
|
||||
cur = self.env['res.currency'].browse(row['currency_id'])
|
||||
if cur == self.env.company.currency_id:
|
||||
raw = ''
|
||||
cols.append(report._build_column_dict(raw, col_def, options=options, currency=cur))
|
||||
|
||||
return {
|
||||
'id': report._get_generic_line_id(
|
||||
'account.move.line', row['id'],
|
||||
parent_line_id=parent_id, markup=row['partial_id'],
|
||||
),
|
||||
'parent_id': parent_id,
|
||||
'name': self._format_aml_name(row['name'], row['ref'], row['move_name']),
|
||||
'columns': cols,
|
||||
'caret_options': caret,
|
||||
'level': 3 + depth_shift,
|
||||
}
|
||||
|
||||
def _build_total_line(self, options, col_totals):
|
||||
cols = []
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
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, col_def, options=options))
|
||||
|
||||
return {
|
||||
'id': report._get_generic_line_id(None, None, markup='total'),
|
||||
'name': _('Total'),
|
||||
'level': 1,
|
||||
'columns': cols,
|
||||
}
|
||||
|
||||
def open_journal_items(self, options, params):
|
||||
params['view_ref'] = 'account.view_move_line_tree_grouped_partner'
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
action = report.open_journal_items(options=options, params=params)
|
||||
action.get('context', {}).update({'search_default_group_by_account': 0})
|
||||
return action
|
||||
Reference in New Issue
Block a user