380 lines
19 KiB
Python
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]
|