feat(fusion_accounting_bank_rec): exchange_diff helper for FX gain/loss pre-check
Made-with: Cursor
This commit is contained in:
@@ -1 +1,2 @@
|
||||
from . import memo_tokenizer
|
||||
from . import exchange_diff
|
||||
|
||||
46
fusion_accounting_bank_rec/services/exchange_diff.py
Normal file
46
fusion_accounting_bank_rec/services/exchange_diff.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Exchange-difference calculation helper.
|
||||
|
||||
Pure-Python FX gain/loss computation. The engine uses this for rapid
|
||||
pre-checks; Odoo's account.move._create_exchange_difference_move() is
|
||||
invoked separately for the actual GL posting.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExchangeDiffResult:
|
||||
needs_diff_move: bool
|
||||
diff_amount: float # in company currency; positive = gain, negative = loss
|
||||
line_company_amount: float
|
||||
against_company_amount: float
|
||||
|
||||
|
||||
def compute_exchange_diff(*, line_amount, line_currency_code, against_amount,
|
||||
against_currency_code, line_rate, against_rate) -> ExchangeDiffResult:
|
||||
"""Compute whether an exchange-diff move is needed and its magnitude.
|
||||
|
||||
Args:
|
||||
line_amount: Bank line amount in its currency
|
||||
line_currency_code: e.g. 'USD'
|
||||
against_amount: Matched journal item amount in its currency
|
||||
against_currency_code: e.g. 'USD' (or different)
|
||||
line_rate: FX rate (foreign per company currency) at line date
|
||||
against_rate: FX rate at journal item posting date
|
||||
|
||||
Returns:
|
||||
ExchangeDiffResult with needs_diff_move flag and computed diff
|
||||
in company currency (positive = gain, negative = loss).
|
||||
"""
|
||||
line_company = line_amount * line_rate
|
||||
against_company = against_amount * against_rate
|
||||
|
||||
diff = line_company - against_company
|
||||
needs_diff = abs(diff) > 0.005 # rounding tolerance
|
||||
|
||||
return ExchangeDiffResult(
|
||||
needs_diff_move=needs_diff,
|
||||
diff_amount=round(diff, 2),
|
||||
line_company_amount=round(line_company, 2),
|
||||
against_company_amount=round(against_company, 2),
|
||||
)
|
||||
@@ -1 +1,2 @@
|
||||
from . import test_memo_tokenizer
|
||||
from . import test_exchange_diff
|
||||
|
||||
56
fusion_accounting_bank_rec/tests/test_exchange_diff.py
Normal file
56
fusion_accounting_bank_rec/tests/test_exchange_diff.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.addons.fusion_accounting_bank_rec.services.exchange_diff import (
|
||||
compute_exchange_diff, ExchangeDiffResult,
|
||||
)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestExchangeDiff(TransactionCase):
|
||||
|
||||
def test_no_diff_when_currencies_match_and_rates_match(self):
|
||||
result = compute_exchange_diff(
|
||||
line_amount=100.00, line_currency_code='CAD',
|
||||
against_amount=100.00, against_currency_code='CAD',
|
||||
line_rate=1.0, against_rate=1.0,
|
||||
)
|
||||
self.assertFalse(result.needs_diff_move)
|
||||
self.assertEqual(result.diff_amount, 0.0)
|
||||
|
||||
def test_diff_when_rates_differ_same_currency(self):
|
||||
"""USD invoice posted at 1.35, USD bank line settled at 1.40 -> diff exists.
|
||||
100 USD at 1.40 = 140 CAD; same at 1.35 = 135 CAD; diff = 5 CAD gain."""
|
||||
result = compute_exchange_diff(
|
||||
line_amount=100.00, line_currency_code='USD',
|
||||
against_amount=100.00, against_currency_code='USD',
|
||||
line_rate=1.40, against_rate=1.35,
|
||||
)
|
||||
self.assertTrue(result.needs_diff_move)
|
||||
self.assertAlmostEqual(result.diff_amount, 5.00, places=2)
|
||||
|
||||
def test_diff_negative_when_rate_dropped(self):
|
||||
"""USD invoice at 1.40, settled at 1.35 -> loss"""
|
||||
result = compute_exchange_diff(
|
||||
line_amount=100.00, line_currency_code='USD',
|
||||
against_amount=100.00, against_currency_code='USD',
|
||||
line_rate=1.35, against_rate=1.40,
|
||||
)
|
||||
self.assertTrue(result.needs_diff_move)
|
||||
self.assertAlmostEqual(result.diff_amount, -5.00, places=2)
|
||||
|
||||
def test_company_amounts_computed_correctly(self):
|
||||
result = compute_exchange_diff(
|
||||
line_amount=100.00, line_currency_code='USD',
|
||||
against_amount=100.00, against_currency_code='USD',
|
||||
line_rate=1.40, against_rate=1.35,
|
||||
)
|
||||
self.assertAlmostEqual(result.line_company_amount, 140.00, places=2)
|
||||
self.assertAlmostEqual(result.against_company_amount, 135.00, places=2)
|
||||
|
||||
def test_tolerance_handles_rounding_noise(self):
|
||||
"""Tiny FX rounding under 0.005 should NOT trigger a diff move."""
|
||||
result = compute_exchange_diff(
|
||||
line_amount=100.00, line_currency_code='USD',
|
||||
against_amount=100.00, against_currency_code='USD',
|
||||
line_rate=1.40000, against_rate=1.40003, # 0.003 cent diff
|
||||
)
|
||||
self.assertFalse(result.needs_diff_move)
|
||||
Reference in New Issue
Block a user