Initial commit
This commit is contained in:
316
Fusion Accounting/wizard/multicurrency_revaluation.py
Normal file
316
Fusion Accounting/wizard/multicurrency_revaluation.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# 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,
|
||||
}
|
||||
Reference in New Issue
Block a user