317 lines
12 KiB
Python
317 lines
12 KiB
Python
# Fusion Accounting - Multicurrency Revaluation Wizard
|
|
# Creates journal entries to adjust for exchange rate differences
|
|
# on foreign-currency denominated balances, with automatic reversal.
|
|
|
|
import json
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, _, Command
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import format_date
|
|
|
|
|
|
class MulticurrencyRevaluationWizard(models.TransientModel):
|
|
"""Generates revaluation journal entries that capture unrealized
|
|
gains or losses from exchange rate fluctuations, then creates
|
|
an auto-reversal entry for the following period."""
|
|
|
|
_name = 'account.multicurrency.revaluation.wizard'
|
|
_description = 'Multicurrency Revaluation Wizard'
|
|
|
|
# --- Organization ---
|
|
company_id = fields.Many2one(
|
|
comodel_name='res.company',
|
|
default=lambda self: self.env.company,
|
|
)
|
|
|
|
# --- Accounting Configuration ---
|
|
journal_id = fields.Many2one(
|
|
comodel_name='account.journal',
|
|
compute='_compute_accounting_values',
|
|
inverse='_inverse_revaluation_journal',
|
|
compute_sudo=True,
|
|
domain=[('type', '=', 'general')],
|
|
required=True,
|
|
readonly=False,
|
|
)
|
|
date = fields.Date(
|
|
default=lambda self: self.env.context[
|
|
'multicurrency_revaluation_report_options'
|
|
]['date']['date_to'],
|
|
required=True,
|
|
)
|
|
reversal_date = fields.Date(required=True)
|
|
expense_provision_account_id = fields.Many2one(
|
|
comodel_name='account.account',
|
|
compute='_compute_accounting_values',
|
|
inverse='_inverse_expense_provision_account',
|
|
compute_sudo=True,
|
|
string="Expense Account",
|
|
required=True,
|
|
readonly=False,
|
|
)
|
|
income_provision_account_id = fields.Many2one(
|
|
comodel_name='account.account',
|
|
compute='_compute_accounting_values',
|
|
inverse='_inverse_income_provision_account',
|
|
compute_sudo=True,
|
|
string="Income Account",
|
|
required=True,
|
|
readonly=False,
|
|
)
|
|
|
|
# --- Preview & Warnings ---
|
|
preview_data = fields.Text(
|
|
compute='_compute_preview_data',
|
|
)
|
|
show_warning_move_id = fields.Many2one(
|
|
comodel_name='account.move',
|
|
compute='_compute_show_warning',
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Defaults
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
"""Initialize reversal date and validate that adjustments are needed."""
|
|
result = super().default_get(default_fields)
|
|
if 'reversal_date' in default_fields:
|
|
report_opts = self.env.context['multicurrency_revaluation_report_options']
|
|
period_end = fields.Date.to_date(report_opts['date']['date_to'])
|
|
result['reversal_date'] = period_end + relativedelta(days=1)
|
|
|
|
# Verify there is actually something to adjust
|
|
if (
|
|
not self.env.context.get('revaluation_no_loop')
|
|
and not self.with_context(
|
|
revaluation_no_loop=True,
|
|
)._get_move_vals()['line_ids']
|
|
):
|
|
raise UserError(_("No currency adjustment is needed."))
|
|
return result
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Compute Methods
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.depends('expense_provision_account_id', 'income_provision_account_id',
|
|
'reversal_date')
|
|
def _compute_show_warning(self):
|
|
"""Warn if there's an unreversed entry on the provision accounts."""
|
|
AML = self.env['account.move.line']
|
|
for wiz in self:
|
|
provision_accts = (
|
|
wiz.expense_provision_account_id
|
|
+ wiz.income_provision_account_id
|
|
)
|
|
recent_entry = AML.search([
|
|
('account_id', 'in', provision_accts.ids),
|
|
('date', '<', wiz.reversal_date),
|
|
], order='date desc', limit=1).move_id
|
|
wiz.show_warning_move_id = (
|
|
False if recent_entry.reversed_entry_id else recent_entry
|
|
)
|
|
|
|
@api.depends('expense_provision_account_id', 'income_provision_account_id',
|
|
'date', 'journal_id')
|
|
def _compute_preview_data(self):
|
|
"""Build JSON data for the move preview widget."""
|
|
col_definitions = [
|
|
{'field': 'account_id', 'label': _("Account")},
|
|
{'field': 'name', 'label': _("Label")},
|
|
{'field': 'debit', 'label': _("Debit"), 'class': 'text-end text-nowrap'},
|
|
{'field': 'credit', 'label': _("Credit"), 'class': 'text-end text-nowrap'},
|
|
]
|
|
for wiz in self:
|
|
move_vals = self._get_move_vals()
|
|
preview_groups = [
|
|
self.env['account.move']._move_dict_to_preview_vals(
|
|
move_vals, wiz.company_id.currency_id,
|
|
),
|
|
]
|
|
wiz.preview_data = json.dumps({
|
|
'groups_vals': preview_groups,
|
|
'options': {'columns': col_definitions},
|
|
})
|
|
|
|
@api.depends('company_id')
|
|
def _compute_accounting_values(self):
|
|
"""Load revaluation settings from the company."""
|
|
for wiz in self:
|
|
wiz.journal_id = wiz.company_id.account_revaluation_journal_id
|
|
wiz.expense_provision_account_id = (
|
|
wiz.company_id.account_revaluation_expense_provision_account_id
|
|
)
|
|
wiz.income_provision_account_id = (
|
|
wiz.company_id.account_revaluation_income_provision_account_id
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Inverse Methods (persist settings back to company)
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _inverse_revaluation_journal(self):
|
|
for wiz in self:
|
|
wiz.company_id.sudo().account_revaluation_journal_id = wiz.journal_id
|
|
|
|
def _inverse_expense_provision_account(self):
|
|
for wiz in self:
|
|
wiz.company_id.sudo().account_revaluation_expense_provision_account_id = (
|
|
wiz.expense_provision_account_id
|
|
)
|
|
|
|
def _inverse_income_provision_account(self):
|
|
for wiz in self:
|
|
wiz.company_id.sudo().account_revaluation_income_provision_account_id = (
|
|
wiz.income_provision_account_id
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Move Value Construction
|
|
# -------------------------------------------------------------------------
|
|
|
|
@api.model
|
|
def _get_move_vals(self):
|
|
"""Build the journal entry values from the multicurrency
|
|
revaluation report data, creating a pair of lines per
|
|
account/currency that needs adjustment."""
|
|
|
|
def _extract_model_id(parsed_segments, target_model):
|
|
"""Extract the record ID for a given model from parsed line segments."""
|
|
for _dummy, seg_model, seg_id in parsed_segments:
|
|
if seg_model == target_model:
|
|
return seg_id
|
|
return None
|
|
|
|
def _extract_adjustment(report_line):
|
|
"""Pull the adjustment amount from the report line columns."""
|
|
for col in report_line.get('columns', []):
|
|
if col.get('expression_label') == 'adjustment':
|
|
return col.get('no_format')
|
|
return None
|
|
|
|
report = self.env.ref('fusion_accounting.multicurrency_revaluation_report')
|
|
included_section = report.line_ids.filtered(
|
|
lambda ln: ln.code == 'multicurrency_included',
|
|
)
|
|
included_line_id = report._get_generic_line_id(
|
|
'account.report.line', included_section.id,
|
|
)
|
|
|
|
report_options = {
|
|
**self.env.context['multicurrency_revaluation_report_options'],
|
|
'unfold_all': False,
|
|
}
|
|
all_report_lines = report._get_lines(report_options)
|
|
entry_lines = []
|
|
|
|
for rpt_line in report._get_unfolded_lines(
|
|
all_report_lines, included_line_id,
|
|
):
|
|
parsed = report._parse_line_id(rpt_line.get('id'))
|
|
adj_balance = _extract_adjustment(rpt_line)
|
|
|
|
# Only process account-level lines with non-zero adjustments
|
|
if parsed[-1][-2] != 'account.account':
|
|
continue
|
|
if self.env.company.currency_id.is_zero(adj_balance):
|
|
continue
|
|
|
|
target_account = _extract_model_id(parsed, 'account.account')
|
|
target_currency = _extract_model_id(parsed, 'res.currency')
|
|
currency_name = self.env['res.currency'].browse(
|
|
target_currency,
|
|
).display_name
|
|
company_cur_name = self.env.company.currency_id.display_name
|
|
current_rate = report_options['currency_rates'][
|
|
str(target_currency)
|
|
]['rate']
|
|
|
|
# Account adjustment line
|
|
entry_lines.append(Command.create({
|
|
'name': _(
|
|
"Provision for %(for_cur)s "
|
|
"(1 %(comp_cur)s = %(rate)s %(for_cur)s)",
|
|
for_cur=currency_name,
|
|
comp_cur=company_cur_name,
|
|
rate=current_rate,
|
|
),
|
|
'debit': adj_balance if adj_balance > 0 else 0,
|
|
'credit': -adj_balance if adj_balance < 0 else 0,
|
|
'amount_currency': 0,
|
|
'currency_id': target_currency,
|
|
'account_id': target_account,
|
|
}))
|
|
|
|
# Provision counterpart line
|
|
if adj_balance < 0:
|
|
provision_label = _(
|
|
"Expense Provision for %s", currency_name,
|
|
)
|
|
provision_account = self.expense_provision_account_id.id
|
|
else:
|
|
provision_label = _(
|
|
"Income Provision for %s", currency_name,
|
|
)
|
|
provision_account = self.income_provision_account_id.id
|
|
|
|
entry_lines.append(Command.create({
|
|
'name': provision_label,
|
|
'debit': -adj_balance if adj_balance < 0 else 0,
|
|
'credit': adj_balance if adj_balance > 0 else 0,
|
|
'amount_currency': 0,
|
|
'currency_id': target_currency,
|
|
'account_id': provision_account,
|
|
}))
|
|
|
|
return {
|
|
'ref': _(
|
|
"Foreign currency adjustment as of %s",
|
|
format_date(self.env, self.date),
|
|
),
|
|
'journal_id': self.journal_id.id,
|
|
'date': self.date,
|
|
'line_ids': entry_lines,
|
|
}
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Main Action
|
|
# -------------------------------------------------------------------------
|
|
|
|
def create_entries(self):
|
|
"""Create the revaluation entry and its automatic reversal."""
|
|
self.ensure_one()
|
|
move_data = self._get_move_vals()
|
|
|
|
if not move_data['line_ids']:
|
|
raise UserError(_("No provision adjustment is required."))
|
|
|
|
# Create and post the revaluation entry
|
|
reval_move = self.env['account.move'].create(move_data)
|
|
reval_move.action_post()
|
|
|
|
# Create and post the reversal
|
|
reversal = reval_move._reverse_moves(default_values_list=[{
|
|
'ref': _("Reversal of: %s", reval_move.ref),
|
|
}])
|
|
reversal.date = self.reversal_date
|
|
reversal.action_post()
|
|
|
|
# Open the revaluation entry in form view
|
|
form_view = self.env.ref('account.view_move_form', False)
|
|
clean_ctx = {
|
|
k: v for k, v in self.env.context.items()
|
|
if k != 'id'
|
|
}
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'account.move',
|
|
'res_id': reval_move.id,
|
|
'view_mode': 'form',
|
|
'view_id': form_view.id,
|
|
'views': [(form_view.id, 'form')],
|
|
'context': clean_ctx,
|
|
}
|