385 lines
22 KiB
Python
385 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import models, fields, api, _
|
|
from odoo.tools import float_is_zero, SQL
|
|
from odoo.exceptions import UserError
|
|
|
|
from itertools import chain
|
|
|
|
|
|
class MulticurrencyRevaluationReportCustomHandler(models.AbstractModel):
|
|
"""Manage Unrealized Gains/Losses.
|
|
|
|
In multi-currencies environments, we need a way to control the risk related
|
|
to currencies (in case some are higthly fluctuating) and, in some countries,
|
|
some laws also require to create journal entries to record the provisionning
|
|
of a probable future expense related to currencies. Hence, people need to
|
|
create a journal entry at the beginning of a period, to make visible the
|
|
probable expense in reports (and revert it at the end of the period, to
|
|
recon the real gain/loss.
|
|
"""
|
|
_name = 'account.multicurrency.revaluation.report.handler'
|
|
_inherit = 'account.report.custom.handler'
|
|
_description = 'Multicurrency Revaluation Report Custom Handler'
|
|
|
|
def _get_custom_display_config(self):
|
|
return {
|
|
'components': {
|
|
'AccountReportFilters': 'at_accounting.MulticurrencyRevaluationReportFilters',
|
|
},
|
|
'templates': {
|
|
'AccountReportLineName': 'at_accounting.MulticurrencyRevaluationReportLineName',
|
|
},
|
|
}
|
|
|
|
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(_("You need to activate more than one currency to access this report."))
|
|
rates = active_currencies._get_rates(self.env.company, options.get('date').get('date_to'))
|
|
# Normalize the rates to the company's currency
|
|
company_rate = rates[self.env.company.currency_id.id]
|
|
for key in rates.keys():
|
|
rates[key] /= company_rate
|
|
|
|
options['currency_rates'] = {
|
|
str(currency_id.id): {
|
|
'currency_id': currency_id.id,
|
|
'currency_name': currency_id.name,
|
|
'currency_main': self.env.company.currency_id.name,
|
|
'rate': (rates[currency_id.id]
|
|
if not previous_options.get('currency_rates', {}).get(str(currency_id.id), {}).get('rate') else
|
|
float(previous_options['currency_rates'][str(currency_id.id)]['rate'])),
|
|
} for currency_id in active_currencies
|
|
}
|
|
|
|
for currency_rates in options['currency_rates'].values():
|
|
if currency_rates['rate'] == 0:
|
|
raise UserError(_("The currency rate cannot be equal to 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'] - 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})
|
|
|
|
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
|
|
if len(self.env.companies) > 1:
|
|
warnings['at_accounting.multi_currency_revaluation_report_warning_multicompany'] = {'alert_type': 'warning'}
|
|
if options['custom_rate']:
|
|
warnings['at_accounting.multi_currency_revaluation_report_warning_custom_rate'] = {'alert_type': 'warning'}
|
|
|
|
def _custom_line_postprocessor(self, report, options, lines):
|
|
line_to_adjust_id = self.env.ref('at_accounting.multicurrency_revaluation_to_adjust').id
|
|
line_excluded_id = self.env.ref('at_accounting.multicurrency_revaluation_excluded').id
|
|
|
|
rslt = []
|
|
for index, line in enumerate(lines):
|
|
res_model_name, res_id = report._get_model_info_from_id(line['id'])
|
|
|
|
if res_model_name == 'account.report.line' and (
|
|
(res_id == line_to_adjust_id and report._get_model_info_from_id(lines[index + 1]['id']) == ('account.report.line', line_excluded_id)) or
|
|
(res_id == line_excluded_id and index == len(lines) - 1)
|
|
):
|
|
# 'To Adjust' and 'Excluded' lines need to be hidden if they have no child
|
|
continue
|
|
|
|
elif res_model_name == 'res.currency':
|
|
# Include the rate in the currency_id group lines
|
|
line['name'] = '{for_cur} (1 {comp_cur} = {rate:.6} {for_cur})'.format(
|
|
for_cur=line['name'],
|
|
comp_cur=self.env.company.currency_id.display_name,
|
|
rate=float(options['currency_rates'][str(res_id)]['rate']),
|
|
)
|
|
|
|
elif res_model_name == 'account.account':
|
|
# Mark the included/excluded lines, so that the custom component templates knows what label to put on them
|
|
line['is_included_line'] = report._get_res_id_from_line_id(line['id'], 'account.account') == line_to_adjust_id
|
|
|
|
# Inject the related model into the line dict in order to use it on the custom component template on js side to display buttons
|
|
line['cur_revaluation_line_model'] = res_model_name
|
|
|
|
rslt.append(line)
|
|
|
|
return rslt
|
|
|
|
def _custom_groupby_line_completer(self, report, options, line_dict):
|
|
model_info_from_id = report._get_model_info_from_id(line_dict['id'])
|
|
if model_info_from_id[0] == 'res.currency':
|
|
line_dict['unfolded'] = True
|
|
line_dict['unfoldable'] = False
|
|
|
|
def action_multi_currency_revaluation_open_revaluation_wizard(self, options):
|
|
"""Open the revaluation wizard."""
|
|
form = self.env.ref('at_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': form.id,
|
|
'views': [(form.id, 'form')],
|
|
'multi': 'True',
|
|
'target': 'new',
|
|
'context': {
|
|
**self._context,
|
|
'multicurrency_revaluation_report_options': options,
|
|
},
|
|
}
|
|
|
|
# ACTIONS
|
|
def action_multi_currency_revaluation_open_general_ledger(self, options, params):
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
account_id = report._get_res_id_from_line_id(params['line_id'], 'account.account')
|
|
account_line_id = report._get_generic_line_id('account.account', account_id)
|
|
general_ledger_options = self.env.ref('at_accounting.general_ledger_report').get_options(options)
|
|
general_ledger_options['unfolded_lines'] = [account_line_id]
|
|
|
|
general_ledger_action = self.env['ir.actions.actions']._for_xml_id('at_accounting.action_account_report_general_ledger')
|
|
general_ledger_action['params'] = {
|
|
'options': general_ledger_options,
|
|
'ignore_session': True,
|
|
}
|
|
|
|
return general_ledger_action
|
|
|
|
def action_multi_currency_revaluation_toggle_provision(self, options, params):
|
|
""" Include/exclude an account from the provision. """
|
|
res_ids_map = self.env['account.report']._get_res_ids_from_line_id(params['line_id'], ['res.currency', 'account.account'])
|
|
account = self.env['account.account'].browse(res_ids_map['account.account'])
|
|
currency = self.env['res.currency'].browse(res_ids_map['res.currency'])
|
|
if currency in account.exclude_provision_currency_ids:
|
|
account.exclude_provision_currency_ids -= currency
|
|
else:
|
|
account.exclude_provision_currency_ids += currency
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'reload',
|
|
}
|
|
|
|
def action_multi_currency_revaluation_open_currency_rates(self, options, params=None):
|
|
""" Open the currency rate list. """
|
|
currency_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(currency_id).display_name),
|
|
'views': [(False, 'list')],
|
|
'res_model': 'res.currency.rate',
|
|
'context': {**self.env.context, **{'default_currency_id': currency_id, 'active_id': currency_id}},
|
|
'domain': [('currency_id', '=', currency_id)],
|
|
}
|
|
|
|
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._multi_currency_revaluation_get_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._multi_currency_revaluation_get_custom_lines(options, 'excluded', current_groupby, next_groupby, offset=offset, limit=limit)
|
|
|
|
def _multi_currency_revaluation_get_custom_lines(self, options, line_code, current_groupby, next_groupby, offset=0, limit=None):
|
|
def build_result_dict(report, query_res):
|
|
return {
|
|
'balance_currency': query_res['balance_currency'] if len(query_res['currency_id']) == 1 else None,
|
|
'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None,
|
|
'balance_operation': query_res['balance_operation'],
|
|
'balance_current': query_res['balance_current'],
|
|
'adjustment': query_res['adjustment'],
|
|
'has_sublines': query_res['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 []))
|
|
|
|
# No need to run any SQL if we're computing the main line: it does not display any total
|
|
if not current_groupby:
|
|
return {
|
|
'balance_currency': None,
|
|
'currency_id': None,
|
|
'balance_operation': None,
|
|
'balance_current': None,
|
|
'adjustment': None,
|
|
'has_sublines': False,
|
|
}
|
|
|
|
query = "(VALUES {})".format(', '.join("(%s, %s)" for rate in options['currency_rates']))
|
|
params = list(chain.from_iterable((cur['currency_id'], cur['rate']) for cur in options['currency_rates'].values()))
|
|
custom_currency_table_query = SQL(query, *params)
|
|
date_to = options['date']['date_to']
|
|
select_part_not_an_exchange_move_id = SQL(
|
|
"""
|
|
NOT EXISTS (
|
|
SELECT 1
|
|
FROM account_partial_reconcile part_exch
|
|
WHERE part_exch.exchange_move_id = account_move_line.move_id
|
|
AND part_exch.max_date <= %s
|
|
)
|
|
""",
|
|
date_to
|
|
)
|
|
|
|
query = report._get_report_query(options, 'strict_range')
|
|
tail_query = report._get_engine_query_tail(offset, limit)
|
|
full_query = SQL(
|
|
"""
|
|
WITH custom_currency_table(currency_id, rate) AS (%(custom_currency_table_query)s)
|
|
|
|
-- Final select that gets the following lines:
|
|
-- (where there is a change in the rates of currency between the creation of the move and the full payments)
|
|
-- - Moves that don't have a payment yet at a certain date
|
|
-- - Moves that have a partial but are not fully paid at a certain date
|
|
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 (
|
|
-- Get moves that have at least one partial at a certain date and are not fully paid at that date
|
|
SELECT
|
|
""" + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f"""
|
|
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,
|
|
(
|
|
-- adjustment is computed as: balance_current - balance_operation
|
|
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 %(table_references)s,
|
|
account_account AS account,
|
|
res_currency AS aml_currency,
|
|
res_currency AS aml_comp_currency,
|
|
custom_currency_table,
|
|
|
|
-- Get for each move line the amount residual and amount_residual currency
|
|
-- both for matched "debit" and matched "credit" the same way as account.move.line
|
|
-- '_compute_amount_residual()' method does
|
|
-- (using LATERAL greatly reduce the number of lines for which we have to compute it)
|
|
LATERAL (
|
|
-- Get sum of matched "debit" amount and amount in currency for related move line at date
|
|
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 <= %(date_to)s
|
|
GROUP BY aml_id,
|
|
curr.decimal_places
|
|
UNION
|
|
-- Get sum of matched "credit" amount and amount in currency for related move line at date
|
|
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 <= %(date_to)s
|
|
GROUP BY aml_id,
|
|
curr.decimal_places
|
|
) AS ara
|
|
WHERE %(search_condition)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 {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} (
|
|
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 (%(select_part_not_an_exchange_move_id)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
|
|
-- Moves that don't have a payment yet at a certain date
|
|
SELECT
|
|
""" + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f"""
|
|
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 %(table_references)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 %(search_condition)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 {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} (
|
|
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 (%(select_part_not_an_exchange_move_id)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 <= %(date_to)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_query)s
|
|
""",
|
|
custom_currency_table_query=custom_currency_table_query,
|
|
table_references=query.from_clause,
|
|
date_to=date_to,
|
|
tail_query=tail_query,
|
|
search_condition=query.where_clause,
|
|
select_part_not_an_exchange_move_id=select_part_not_an_exchange_move_id,
|
|
)
|
|
self._cr.execute(full_query)
|
|
query_res_lines = self._cr.dictfetchall()
|
|
|
|
if not current_groupby:
|
|
return build_result_dict(report, query_res_lines and query_res_lines[0] or {})
|
|
else:
|
|
rslt = []
|
|
for query_res in query_res_lines:
|
|
grouping_key = query_res['grouping_key']
|
|
rslt.append((grouping_key, build_result_dict(report, query_res)))
|
|
return rslt
|