"""Multi-currency conversion for financial reports. Converts move-line amounts to the report's display currency at the report end-date. Pure-Python - caller provides exchange rates as a dict {(source_code, target_code, date): rate}.""" from dataclasses import dataclass from datetime import date @dataclass class ConversionRate: source: str target: str rate: float rate_date: date def convert_amount(amount: float, *, source_currency: str, target_currency: str, rate_date: date, rates: dict) -> float: """Convert `amount` from source to target at the given date. `rates` is a dict keyed by (source, target, date) -> rate. If source == target, returns amount unchanged.""" if source_currency == target_currency: return amount key = (source_currency, target_currency, rate_date) if key in rates: return amount * rates[key] inv_key = (target_currency, source_currency, rate_date) if inv_key in rates: inv = rates[inv_key] if inv != 0: return amount / inv candidates = [ (d, r) for (s, t, d), r in rates.items() if s == source_currency and t == target_currency and d <= rate_date ] if candidates: candidates.sort(key=lambda x: x[0], reverse=True) return amount * candidates[0][1] raise ValueError( f"No exchange rate available for {source_currency}->{target_currency} on or before {rate_date}" ) def fetch_rates(env, *, target_currency_id: int, as_of: date, source_currency_ids: list[int] | None = None) -> dict: """Fetch all relevant rates from res.currency.rate as of a given date. Returns the dict-of-rates structure consumed by convert_amount. Pulls only rates where source != target and date <= as_of.""" Rate = env['res.currency.rate'].sudo() target = env['res.currency'].browse(target_currency_id) domain = [ ('name', '<=', as_of), ('currency_id', '!=', target.id), ] if source_currency_ids: domain.append(('currency_id', 'in', source_currency_ids)) rates_recs = Rate.search(domain) out = {} for r in rates_recs: out[(r.currency_id.name, target.name, r.name)] = (1.0 / r.rate) if r.rate else 0.0 return out