Files
Odoo-Modules/Fusion Accounting/models/account_multicurrency_revaluation_report.py
2026-02-22 01:22:18 -05:00

380 lines
19 KiB
Python

# Fusion Accounting - Multicurrency Revaluation Report Handler
# Computes unrealised FX gains/losses and provides an adjustment wizard
from itertools import chain
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, SQL
class FusionMulticurrencyRevaluationHandler(models.AbstractModel):
"""Manages unrealised gains and losses arising from fluctuating
exchange rates. Presents balances at both historical and current
rates and offers an adjustment-entry wizard."""
_name = 'account.multicurrency.revaluation.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Multicurrency Revaluation Report Custom Handler'
# ---- Display Configuration ----
def _get_custom_display_config(self):
return {
'components': {
'AccountReportFilters': 'fusion_accounting.MulticurrencyRevaluationReportFilters',
},
'templates': {
'AccountReportLineName': 'fusion_accounting.MulticurrencyRevaluationReportLineName',
},
}
# ---- Options ----
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
active_currencies = self.env['res.currency'].search([('active', '=', True)])
if len(active_currencies) < 2:
raise UserError(_("At least two active currencies are required for this report."))
fx_rates = active_currencies._get_rates(
self.env.company, options.get('date', {}).get('date_to'),
)
base_rate = fx_rates[self.env.company.currency_id.id]
for cid in fx_rates:
fx_rates[cid] /= base_rate
options['currency_rates'] = {
str(cur.id): {
'currency_id': cur.id,
'currency_name': cur.name,
'currency_main': self.env.company.currency_id.name,
'rate': (
fx_rates[cur.id]
if not previous_options.get('currency_rates', {}).get(str(cur.id), {}).get('rate')
else float(previous_options['currency_rates'][str(cur.id)]['rate'])
),
}
for cur in active_currencies
}
for cr in options['currency_rates'].values():
if cr['rate'] == 0:
raise UserError(_("Currency rate cannot be zero."))
options['company_currency'] = options['currency_rates'].pop(
str(self.env.company.currency_id.id),
)
options['custom_rate'] = any(
not float_is_zero(cr['rate'] - fx_rates[cr['currency_id']], 20)
for cr in options['currency_rates'].values()
)
options['multi_currency'] = True
options['buttons'].append({
'name': _('Adjustment Entry'),
'sequence': 30,
'action': 'action_multi_currency_revaluation_open_revaluation_wizard',
'always_show': True,
})
# ---- Warnings ----
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
if len(self.env.companies) > 1:
warnings['fusion_accounting.multi_currency_revaluation_report_warning_multicompany'] = {
'alert_type': 'warning',
}
if options['custom_rate']:
warnings['fusion_accounting.multi_currency_revaluation_report_warning_custom_rate'] = {
'alert_type': 'warning',
}
# ---- Post-Processing ----
def _custom_line_postprocessor(self, report, options, lines):
adj_line_id = self.env.ref('fusion_accounting.multicurrency_revaluation_to_adjust').id
excl_line_id = self.env.ref('fusion_accounting.multicurrency_revaluation_excluded').id
processed = []
for idx, ln in enumerate(lines):
model_name, model_id = report._get_model_info_from_id(ln['id'])
if model_name == 'account.report.line' and (
(model_id == adj_line_id
and report._get_model_info_from_id(lines[idx + 1]['id']) == ('account.report.line', excl_line_id))
or (model_id == excl_line_id and idx == len(lines) - 1)
):
continue
elif model_name == 'res.currency':
rate_val = float(options['currency_rates'][str(model_id)]['rate'])
ln['name'] = '{fc} (1 {mc} = {r:.6} {fc})'.format(
fc=ln['name'],
mc=self.env.company.currency_id.display_name,
r=rate_val,
)
elif model_name == 'account.account':
ln['is_included_line'] = (
report._get_res_id_from_line_id(ln['id'], 'account.account') == adj_line_id
)
ln['cur_revaluation_line_model'] = model_name
processed.append(ln)
return processed
def _custom_groupby_line_completer(self, report, options, line_dict):
info = report._get_model_info_from_id(line_dict['id'])
if info[0] == 'res.currency':
line_dict['unfolded'] = True
line_dict['unfoldable'] = False
# ---- Actions ----
def action_multi_currency_revaluation_open_revaluation_wizard(self, options):
wiz_view = self.env.ref(
'fusion_accounting.view_account_multicurrency_revaluation_wizard', False,
)
return {
'name': _("Make Adjustment Entry"),
'type': 'ir.actions.act_window',
'res_model': 'account.multicurrency.revaluation.wizard',
'view_mode': 'form',
'view_id': wiz_view.id,
'views': [(wiz_view.id, 'form')],
'multi': 'True',
'target': 'new',
'context': {
**self.env.context,
'multicurrency_revaluation_report_options': options,
},
}
def action_multi_currency_revaluation_open_general_ledger(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
acct_id = report._get_res_id_from_line_id(params['line_id'], 'account.account')
acct_line_id = report._get_generic_line_id('account.account', acct_id)
gl_options = self.env.ref('fusion_accounting.general_ledger_report').get_options(options)
gl_options['unfolded_lines'] = [acct_line_id]
gl_action = self.env['ir.actions.actions']._for_xml_id(
'fusion_accounting.action_account_report_general_ledger',
)
gl_action['params'] = {
'options': gl_options,
'ignore_session': True,
}
return gl_action
def action_multi_currency_revaluation_toggle_provision(self, options, params):
"""Toggle inclusion/exclusion of an account from the provision."""
id_map = self.env['account.report']._get_res_ids_from_line_id(
params['line_id'], ['res.currency', 'account.account'],
)
acct = self.env['account.account'].browse(id_map['account.account'])
cur = self.env['res.currency'].browse(id_map['res.currency'])
if cur in acct.exclude_provision_currency_ids:
acct.exclude_provision_currency_ids -= cur
else:
acct.exclude_provision_currency_ids += cur
return {'type': 'ir.actions.client', 'tag': 'reload'}
def action_multi_currency_revaluation_open_currency_rates(self, options, params=None):
cur_id = self.env['account.report']._get_res_id_from_line_id(
params['line_id'], 'res.currency',
)
return {
'type': 'ir.actions.act_window',
'name': _('Currency Rates (%s)', self.env['res.currency'].browse(cur_id).display_name),
'views': [(False, 'list')],
'res_model': 'res.currency.rate',
'context': {**self.env.context, 'default_currency_id': cur_id, 'active_id': cur_id},
'domain': [('currency_id', '=', cur_id)],
}
# ---- Custom Engines ----
def _report_custom_engine_multi_currency_revaluation_to_adjust(
self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None,
):
return self._revaluation_custom_lines(
options, 'to_adjust', current_groupby, next_groupby, offset=offset, limit=limit,
)
def _report_custom_engine_multi_currency_revaluation_excluded(
self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None,
):
return self._revaluation_custom_lines(
options, 'excluded', current_groupby, next_groupby, offset=offset, limit=limit,
)
def _revaluation_custom_lines(self, options, line_code, current_groupby, next_groupby, offset=0, limit=None):
def _build_result(report_obj, qr):
return {
'balance_currency': qr['balance_currency'] if len(qr['currency_id']) == 1 else None,
'currency_id': qr['currency_id'][0] if len(qr['currency_id']) == 1 else None,
'balance_operation': qr['balance_operation'],
'balance_current': qr['balance_current'],
'adjustment': qr['adjustment'],
'has_sublines': qr['aml_count'] > 0,
}
report = self.env['account.report'].browse(options['report_id'])
report._check_groupby_fields(
(next_groupby.split(',') if next_groupby else [])
+ ([current_groupby] if current_groupby else []),
)
if not current_groupby:
return {
'balance_currency': None, 'currency_id': None,
'balance_operation': None, 'balance_current': None,
'adjustment': None, 'has_sublines': False,
}
rate_values_sql = "(VALUES {})".format(
', '.join("(%s, %s)" for _ in options['currency_rates']),
)
rate_params = list(chain.from_iterable(
(cr['currency_id'], cr['rate']) for cr in options['currency_rates'].values()
))
custom_rate_table = SQL(rate_values_sql, *rate_params)
report_date = options['date']['date_to']
no_exchange_clause = SQL(
"""
NOT EXISTS (
SELECT 1
FROM account_partial_reconcile pr
WHERE pr.exchange_move_id = account_move_line.move_id
AND pr.max_date <= %s
)
""",
report_date,
)
qry = report._get_report_query(options, 'strict_range')
tail = report._get_engine_query_tail(offset, limit)
provision_test = 'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'
groupby_col = f"account_move_line.{current_groupby}" if current_groupby else ''
groupby_select = f"{groupby_col} AS grouping_key," if current_groupby else ''
full_sql = SQL(
"""
WITH custom_currency_table(currency_id, rate) AS (%(rate_table)s)
SELECT
subquery.grouping_key,
ARRAY_AGG(DISTINCT(subquery.currency_id)) AS currency_id,
SUM(subquery.balance_currency) AS balance_currency,
SUM(subquery.balance_operation) AS balance_operation,
SUM(subquery.balance_current) AS balance_current,
SUM(subquery.adjustment) AS adjustment,
COUNT(subquery.aml_id) AS aml_count
FROM (
SELECT
""" + groupby_select + """
ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) AS balance_operation,
ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) AS balance_currency,
ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate AS balance_current,
(
ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate
- ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places)
) AS adjustment,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM %(from_refs)s,
account_account AS account,
res_currency AS aml_currency,
res_currency AS aml_comp_currency,
custom_currency_table,
LATERAL (
SELECT COALESCE(SUM(part.amount), 0.0) AS amount_debit,
ROUND(SUM(part.debit_amount_currency), curr.decimal_places) AS amount_debit_currency,
0.0 AS amount_credit, 0.0 AS amount_credit_currency,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM account_partial_reconcile part
JOIN res_currency curr ON curr.id = part.debit_currency_id
WHERE account_move_line.id = part.debit_move_id AND part.max_date <= %(dt)s
GROUP BY aml_id, curr.decimal_places
UNION
SELECT 0.0 AS amount_debit, 0.0 AS amount_debit_currency,
COALESCE(SUM(part.amount), 0.0) AS amount_credit,
ROUND(SUM(part.credit_amount_currency), curr.decimal_places) AS amount_credit_currency,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM account_partial_reconcile part
JOIN res_currency curr ON curr.id = part.credit_currency_id
WHERE account_move_line.id = part.credit_move_id AND part.max_date <= %(dt)s
GROUP BY aml_id, curr.decimal_places
) AS ara
WHERE %(where)s
AND account_move_line.account_id = account.id
AND account_move_line.currency_id = aml_currency.id
AND account_move_line.company_currency_id = aml_comp_currency.id
AND account_move_line.currency_id = custom_currency_table.currency_id
AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
AND (
account.currency_id != account_move_line.company_currency_id
OR (account.account_type IN ('asset_receivable', 'liability_payable')
AND account_move_line.currency_id != account_move_line.company_currency_id)
)
AND """ + provision_test + """ (
SELECT 1 FROM account_account_exclude_res_currency_provision
WHERE account_account_id = account_move_line.account_id
AND res_currency_id = account_move_line.currency_id
)
AND (%(no_exch)s)
GROUP BY account_move_line.id, aml_comp_currency.decimal_places, aml_currency.decimal_places, custom_currency_table.rate
HAVING ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) != 0
OR ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) != 0.0
UNION
SELECT
""" + groupby_select + """
account_move_line.balance AS balance_operation,
account_move_line.amount_currency AS balance_currency,
account_move_line.amount_currency / custom_currency_table.rate AS balance_current,
account_move_line.amount_currency / custom_currency_table.rate - account_move_line.balance AS adjustment,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM %(from_refs)s
JOIN account_account account ON account_move_line.account_id = account.id
JOIN custom_currency_table ON custom_currency_table.currency_id = account_move_line.currency_id
WHERE %(where)s
AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
AND (
account.currency_id != account_move_line.company_currency_id
OR (account.account_type IN ('asset_receivable', 'liability_payable')
AND account_move_line.currency_id != account_move_line.company_currency_id)
)
AND """ + provision_test + """ (
SELECT 1 FROM account_account_exclude_res_currency_provision
WHERE account_account_id = account_id
AND res_currency_id = account_move_line.currency_id
)
AND (%(no_exch)s)
AND NOT EXISTS (
SELECT 1 FROM account_partial_reconcile part
WHERE (part.debit_move_id = account_move_line.id OR part.credit_move_id = account_move_line.id)
AND part.max_date <= %(dt)s
)
AND (account_move_line.balance != 0.0 OR account_move_line.amount_currency != 0.0)
) subquery
GROUP BY grouping_key
ORDER BY grouping_key
%(tail)s
""",
rate_table=custom_rate_table,
from_refs=qry.from_clause,
dt=report_date,
where=qry.where_clause,
no_exch=no_exchange_clause,
tail=tail,
)
self.env.cr.execute(full_sql)
rows = self.env.cr.dictfetchall()
if not current_groupby:
return _build_result(report, rows[0] if rows else {})
return [(r['grouping_key'], _build_result(report, r)) for r in rows]