# 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]