Exchange rates (BNR)¶
BnrClient reads the official exchange rates published by Banca Națională a
României on curs.bnr.ro. It is the one client in anafpy that does not talk
to ANAF, and it exists because ANAF filings are denominated in lei: an invoice
in euro has to be converted before its value can be declared.
No credentials, no certificate, no test/prod split.
from anafpy.bnr import BnrClient
async with BnrClient() as bnr:
conversion = await bnr.convert("1234.56", "EUR")
print(conversion.amount_ron) # Decimal('6491.81')
print(conversion.rate.quoted) # Decimal('5.2584')
The rate you get is the rate BNR published¶
BNR publishes once per banking day, just after 13:00. A Saturday, a public
holiday, or a call at nine in the morning therefore has no rate of its own —
so get_rates returns the latest set published on or before the day you
asked for, and tells you which day that was:
async with BnrClient() as bnr:
rates = await bnr.get_rates("2026-08-30") # a Sunday
print(rates.date) # 2026-08-28 — the Friday
Read FxRateSet.date rather than assuming the date you passed in. Presenting
Friday's rate as Sunday's is the one mistake this client is built to prevent.
Dates reach back to 2005, the start of BNR's archive; anything earlier
raises AnafConfigError.
Converting¶
FxRateSet.convert does the arithmetic, and there are two reasons not to do it
yourself:
- The multiplier. BNR quotes small-denomination currencies per 100 units —
<Rate currency="HUF" multiplier="100">1.4430</Rate>means 100 HUF = 1.4430 RON.FxRate.quotedandFxRate.multiplierstay exactly as published, and the division happens inside the conversion, so the factor of 100 cannot be lost in a rounded intermediate.FxRate.lei_per_unitis there when you want the per-unit figure to show someone. - The rounding. Two decimals, half away from zero, applied once to the final figure — the Romanian fiscal convention, not Python's default banker's rounding.
To convert several amounts at one rate — an invoice's lines — fetch the set
once and call its convert per amount, so every line demonstrably shares a
rate:
async with BnrClient() as bnr:
rates = await bnr.get_rates()
lines = [rates.convert(amount, "EUR") for amount in ("100.00", "250.50")]
An unquoted currency code raises AnafConfigError listing what BNR does quote.
Where this is used¶
The conversion feeds values anafpy otherwise cannot compute:
- e-Transport
value_ronon a goods line (e-Transport); - an invoice's BT-111, which
compute_totalsdeliberately leaves explicit-only because it needs a rate (invoice authoring); - D301's
curs_valutar(declarations).
Which day's rate a given filing must use is not this client's call. It
returns what BNR published on a date you choose. The
etransport-declare skill states the declaration-day rule
it follows for UIT filings.
Parsing¶
Documents are parsed through models generated from BNR's own published XSD
(https://curs.bnr.ro/xsd/nbrfxrates.xsd, vendored in schemas/bnr/), so the
schema is what enforces required fields and types rather than hand-written
checks. One BNR quirk is handled for you: the year archives through 2025 are
served with an http://www.bnr.ro/xsd namespace and 2026 onwards with
https://…, and the client folds the two together — so a date either side of
that boundary parses the same way.
Caching and traffic¶
BNR asks callers to read these XML files rather than scrape its site pages, and to store what they take. The client does both.
Each query uses the smallest document that can answer it — the one-day file (~2 KB) for today, the ten-day file (~14 KB) for the recent past, the year archive (~350 KB) only for older dates. And what is fetched is cached for as long as BNR could still change it:
- a closed year's archive is final — BNR will never add a day to a year that has ended — so it is held for the life of the process;
- a document BNR is still updating (today's file, the ten-day window, the current year's archive) is re-read every 15 minutes, so a 13:00 publication is picked up promptly.
Either way, the reads of one filing — every line of an invoice — cost a single fetch, concurrent reads share it, and a failed fetch is never cached.
The cache lives and dies with the process; nothing is written to disk.