Multi-currency accounting in Odoo works reliably when you understand the three places a rate appears: the rate at transaction time, the rate at payment time, and the rate at period-end revaluation. Miss any one and your currency gain/loss accounts will be wrong.
This post covers the technical layer - how exchange rates are stored and applied, what happens in the ORM when you post a foreign-currency invoice, and how to implement period-end revaluation correctly.
Currency configuration#
Company currency vs. transaction currency#
Each company has a company currency (res.company.currency_id). All accounting entries are stored in two amounts: the transaction amount in the foreign currency, and the equivalent amount in the company currency computed at the rate in effect on the transaction date.
Currencies are managed in Accounting → Configuration → Currencies. Enable a currency by setting it to Active. Deactivating a currency prevents new transactions but does not erase historical entries.
Exchange rate providers#
Navigate to Accounting → Configuration → Currencies → Update or set an automatic provider. Odoo ships with providers for:
- European Central Bank (EUR base)
- Banco de Mexico
- Banco do Brasil
- Manual entry
The rate type used by Odoo is a simple rate (1 USD = X EUR) stored in res.currency.rate. Rates are date-stamped - Odoo picks the rate closest to and not after the transaction date.
# How Odoo resolves the rate for a given date
rate = env['res.currency'].search([
('name', '=', 'USD'),
]).with_context(date=invoice_date)._get_conversion_rate(
from_currency=usd,
to_currency=eur,
company=company,
date=invoice_date,
)If no rate exists for the transaction date, Odoo uses the most recent prior rate. If no rate exists at all, the currency lookup raises a UserError.
Monetary fields in the ORM#
Odoo uses fields.Monetary for all currency-aware amounts. A Monetary field requires a paired currency field:
class SaleOrder(models.Model):
_name = 'sale.order'
currency_id = fields.Many2one('res.currency', required=True)
amount_total = fields.Monetary(currency_field='currency_id', compute='_compute_amount')
amount_total_signed = fields.Monetary(
currency_field='currency_id',
compute='_compute_amount_signed',
help="Total in company currency"
)When you read a Monetary field, Odoo returns the raw stored value in the field's currency. When you display it, the UI formats it with the currency symbol and decimal places defined on res.currency.
Company currency conversion: to convert an amount to the company currency, use:
amount_company_currency = foreign_currency._convert(
from_amount=amount_in_foreign,
to_currency=company.currency_id,
company=company,
date=date,
round=True,
)Never do manual rate arithmetic - always use _convert to ensure consistency with Odoo's rate lookup logic.
Journal entries for foreign-currency invoices#
When you post a customer invoice in USD for a EUR-currency company, Odoo creates journal entry lines with both the foreign amount and the company currency equivalent:
Dr Accounts Receivable (USD 1,000 = EUR 920)
Cr Revenue (USD 1,000 = EUR 920)The amounts on each line are stored in two columns on account.move.line:
amount_currency- the foreign currency amount (1,000 USD)debit/credit- the company currency equivalent (920 EUR)
Both columns are immutable after posting. The company currency equivalent is locked at the rate in effect on the invoice date.
Payment in foreign currency#
When payment arrives (say the rate moved to 1 USD = EUR 0.90), a payment journal entry is created:
Dr Bank (USD 1,000 = EUR 900)
Cr Accounts Receivable (USD 1,000 = EUR 900)The AR line is now EUR 900, but the original invoice AR line was EUR 920. A EUR 20 discrepancy exists in the AR account for this partner.
Currency gain/loss on reconciliation#
When Odoo reconciles the invoice AR line (EUR 920) against the payment AR line (EUR 900), the EUR 20 difference is automatically posted to the currency gain/loss account:
Dr Accounts Receivable - Reconciliation EUR 20
Cr Currency Exchange Gain EUR 20The gain/loss account is configured in Accounting → Configuration → Settings → Currency Exchange Gain/Loss Accounts. This must be set before posting any foreign-currency transactions - without it, reconciliation will fail with a configuration error.
Period-end revaluation#
At month-end, any open AR/AP balances denominated in foreign currency must be revalued to the current exchange rate. The difference between the original rate and the period-end rate is an unrealized gain or loss.
Navigate to Accounting → Accounting → Unrealized Currency Gains/Losses. Odoo calculates the difference for each open foreign-currency move line and proposes journal entries:
# If USD strengthened since invoice date:
Dr Accounts Receivable EUR 50 # revalue up
Cr Unrealized Currency Gain EUR 50These entries use a reversal date of the first day of the next period. On the reversal date, the entry automatically reverses - this keeps the original AR line intact for reconciliation and avoids permanent distortion of the realized gain/loss on eventual payment.
Reversal journals: configure a dedicated reversal journal for revaluation entries (e.g., "FX Revaluation"). This makes it easy to audit which entries are period-end revaluations vs. payment settlements.
Multi-currency payments and bank feeds#
When your bank account is in a foreign currency (e.g., you maintain a USD bank account in a EUR company):
- Configure the bank account's currency as USD in Accounting → Configuration → Journals → Bank.
- When importing bank statements, Odoo expects transactions in USD.
- When reconciling a USD payment against a EUR invoice, Odoo automatically computes the gain/loss on reconciliation.
Common error: importing a bank feed in the wrong currency. If your USD bank journal receives EUR-denominated transactions, Odoo will apply the wrong rate on reconciliation. Verify the journal currency before connecting the bank feed provider.
Reporting on currency exposure#
Odoo does not have a built-in currency exposure report, but you can query the data:
# Open AR lines in foreign currencies
open_lines = env['account.move.line'].search([
('account_id.account_type', '=', 'asset_receivable'),
('reconciled', '=', False),
('currency_id', '!=', env.company.currency_id.id),
])
# Group by currency
exposure = {}
for line in open_lines:
ccy = line.currency_id.name
exposure.setdefault(ccy, 0)
exposure[ccy] += line.amount_currencyThis gives you total open receivables by foreign currency - your FX exposure. Running this before each month-end revaluation is a useful sanity check.
Common mistakes#
Not setting gain/loss accounts before go-live. The first time a foreign-currency reconciliation runs without these accounts configured, Odoo will either fail silently or error out. Set the accounts during initial configuration.
Using "force exchange rate" on every invoice. Odoo allows overriding the exchange rate on a specific invoice. This breaks the rate audit trail and makes revaluation calculations unpredictable. Only use forced rates when explicitly required by contract or regulatory rule.
Forgetting to reverse revaluation entries. Unrealized gain/loss entries that are not reversed will compound at each period-end. The standard flow (revalue, then reverse first day of next period) keeps the books clean. Verify that reversals are happening by checking entries posted on the 1st of each month.
Reporting in foreign currency without a subsidiary company. Multi-currency reporting (consolidated statements in a parent currency) requires either a multi-company setup with currency translation or an external reporting tool. Odoo's built-in reports always show amounts in the company currency, not in a reporting currency. Plan for this limitation early.
For the journal entry mechanics behind reconciliation, see the Odoo accounting for developers guide. For configuring fiscal positions that interact with multi-currency setups, see the localization guide.

