Bank transaction importers
byro imports real bank transactions from files uploaded on the “Import bank transactions” page in the finance section. Which file formats are available depends on the installed plugins: every plugin can register one or more bank transaction importers, and the user selects the importer that matches the uploaded file (for example “CAMT.053” or “Fidor CSV”).
An importer only answers one question: which bank transactions does this file
contain? It parses its input format and yields neutral
ImportedBankTransaction objects. byro’s
core then validates the transactions, detects duplicates, creates the
bookkeeping entries and runs the matching pipeline. An importer never creates
Transaction or Booking objects, never selects bookkeeping accounts and
never matches members or fees.
Note
This API is for file based imports. Direct bank connections such as FinTS
need their own configuration and user interface and are therefore not
registered as importers. They may, however, use the same
BankTransactionImportService to
persist the transactions they fetch.
Minimal example
A complete importer looks like this:
1from datetime import date
2from decimal import Decimal
3
4from django.dispatch import receiver
5from django.utils.translation import gettext_lazy as _
6
7from byro.bookkeeping.bank_import import (
8 BankTransactionImporter,
9 ImportedBankTransaction,
10 InvalidImportFile,
11)
12from byro.bookkeeping.signals import bank_transaction_importers
13
14
15class ExampleBankImporter(BankTransactionImporter):
16 identifier = "byro_example.bank"
17 label = _("Example Bank")
18
19 def parse(self, source):
20 with source.source_file.open("rb") as f:
21 for row in parse_rows(f): # your format specific code
22 yield ImportedBankTransaction(
23 booking_date=date(2026, 9, 1),
24 amount=Decimal("25.00"),
25 currency="EUR",
26 memo="Membership fee",
27 counterparty_name="Max Mustermann",
28 counterparty_iban="DE12 3456 7890 1234 5678 90",
29 external_id="123456789",
30 )
31
32
33@receiver(bank_transaction_importers)
34def register_example_importer(sender, **kwargs):
35 return ExampleBankImporter()
Subclassing BankTransactionImporter is
optional; any object with the attributes identifier and label and a
parse(source) method is accepted. A receiver may also return a list of
importers.
Registration
Connect a receiver to byro.bookkeeping.signals.bank_transaction_importers.
The signal is sent whenever the importer selection is built or an importer is
resolved by its identifier, so registration must be cheap and side effect
free. Invalid importers and duplicate identifiers are logged and ignored.
Stable identifier
identifier is a stable, dotted string such as byro_camt.camt053. It is
stored on every RealTransactionSource and on
every booking created from it (Booking.importer), and it is used for
logging and debugging. It must never depend on the translated label and
should not change once released. Prefix it with your plugin’s package name to
avoid collisions with other plugins. The maximum length is 255 characters.
parse(source)
parse receives the RealTransactionSource
whose source_file holds the uploaded file. It returns an iterable (ideally
a generator) of ImportedBankTransaction
objects.
If the file cannot be understood, raise
InvalidImportFile, optionally with a
user presentable message. Any other exception is logged with its traceback
and reported to the user as a generic importer failure. Never put bank data
(IBANs, names, memos, raw lines) into exception messages; they are shown in
the browser.
ImportedBankTransaction
- class byro.bookkeeping.bank_import.ImportedBankTransaction(booking_date: ~datetime.date, amount: ~decimal.Decimal, value_date: ~datetime.date | None = None, currency: str = 'EUR', memo: str = '', counterparty_name: str | None = None, counterparty_iban: str | None = None, counterparty_bic: str | None = None, external_id: str | None = None, end_to_end_id: str | None = None, mandate_id: str | None = None, creditor_id: str | None = None, bank_reference: str | None = None, transaction_code: str | None = None, data: dict = <factory>)[source]
Neutral description of a single bank transaction.
This is the contract between a format specific parser and byro’s bookkeeping. It is intentionally not a Django model.
Amount semantics:
amount > 0is money arriving on the bank account,amount < 0is money leaving the bank account.- amount: Decimal
- bank_reference: str | None = None
- booking_date: date
- counterparty_bic: str | None = None
- counterparty_iban: str | None = None
- counterparty_name: str | None = None
- creditor_id: str | None = None
- currency: str = 'EUR'
- data: dict
Format specific metadata for this single transaction. Must be JSON serialisable. Never put the complete source file here.
- end_to_end_id: str | None = None
- external_id: str | None = None
Stable, bank assigned reference for this transaction (for example the CAMT
AcctSvcrRef). Must be unique per bank account across all importers. LeaveNoneif the format has no reliable identifier.
- mandate_id: str | None = None
- memo: str = ''
- transaction_code: str | None = None
- value_date: date | None = None
booking_dateThe date the bank booked the transaction (required).
value_datedefaults to the booking date.amountA
Decimalwith at most two decimal places. Positive amounts are money arriving on the bank account, negative amounts are money leaving it. A member paying 25 € isDecimal("25.00"); the association paying an 80 € invoice isDecimal("-80.00"). Zero amounts and floats are rejected. The core turns the sign into the debit/credit booking on the bank account, so importers do not need to know byro’s accounting model.currencyISO 4217 code,
"EUR"by default. byro’s bookkeeping has no currency concept, so onlyEURis accepted. A transaction in any other currency raisesUnsupportedCurrencyand fails the import; amounts are never silently reinterpreted as EUR.memoThe purpose / remittance information. Truncated to 1000 characters.
counterparty_name,counterparty_iban,counterparty_bicInformation about the other party. The IBAN is normalized (upper case, no whitespace) before it is stored. The name is shown below the memo in the account and transaction views of the office.
external_idA stable reference the bank assigned to this transaction, for example the
AcctSvcrRefof a CAMT entry or a provider transaction ID. If present, it is the primary key for duplicate detection, so it must be unique per bank account across all importers and must be identical every time the same transaction is exported. Do not use line numbers or other values that depend on the export. If your format has no such reference, leave itNoneand let the core fall back to a fingerprint.end_to_end_id,mandate_id,creditor_id,bank_reference,transaction_codeOptional SEPA and bank references. They are stored with the booking and contribute to the fallback fingerprint.
dataFormat specific metadata for this single transaction as a JSON serializable dict, e.g.
{"entry_reference": "..."}. It is stored inBooking.datanext to the core fields listed above (the keyscounterparty_name,counterparty_iban,counterparty_bic,external_id,end_to_end_id,mandate_id,creditor_id,bank_referenceandtransaction_codeare reserved for the core). Do not store the complete source file or the raw line for every booking: the original is kept inRealTransactionSource.source_file, and duplicating it bloats the database with personal data.
What the core does
For every source processed with an importer, the
BankTransactionImportService
validates and normalizes every yielded transaction,
computes its identity and skips transactions that are already known,
creates a
Transactionwith a single bankBookingon the special bank account (SpecialAccounts.bank) withsource,importeranddataset,stores the number of imported and duplicate transactions on the source,
runs the
process_transactionmatching pipeline for the new transactions.
The whole import is atomic: import_transactions() runs in a database
transaction, so if a single transaction is invalid or persisting fails,
nothing is written and the source ends in state FAILED. This also holds
when the service is used directly, for example by a bank connection plugin.
Duplicates are not errors; a run that imports nothing new because every
transaction was already known is a successful import.
Duplicate detection
Repeated and overlapping imports (an export for January to March followed by
one for January to April) must not create duplicate bookings, and the same
transaction may arrive through different importers over time. The core
therefore stores a SHA-256 identity on every imported booking
(Booking.import_identity, unique in the database) and skips transactions
whose identity already exists. Two imports running at the same time cannot
persist the same transaction twice either: the loser of the race sees a
unique constraint violation, which is counted as a duplicate.
With an
external_idthe identity is derived from the bank account and the external ID only. It does not include the importer, so a CAMT import and a later import of another format carrying the same bank reference recognize each other.Without an
external_ida fingerprint over the normalized booking date, value date, amount, currency, counterparty IBAN and name, memo and the SEPA references is used. Date and amount alone are never sufficient: two members paying the same fee on the same day are two transactions. Identical fingerprints within one file are all imported (the n-th repetition gets its own identity), and re-importing that file detects all of them again.
Importers do not implement any duplicate logic themselves. Everything else, including legitimate look-alike transactions, is deliberately not merged: an additional transaction is preferable to silently swallowing a payment.
Errors
Public API for bank transaction importers.
Plugins that want to import bank transactions from a file implement a
BankTransactionImporter and register it via the
byro.bookkeeping.signals.bank_transaction_importers signal. The importer
only parses its input and yields ImportedBankTransaction objects;
byro’s BankTransactionImportService
takes care of validation, duplicate detection, persistence and matching.
- exception byro.bookkeeping.bank_import.api.BankTransactionImportError(message=None, **kwargs)[source]
Base class for all errors raised by the bank transaction import.
str(error)is a user presentable message that must not contain bank data. Technical details belong into the log.
- exception byro.bookkeeping.bank_import.api.ImporterError(message=None, **kwargs)[source]
The importer failed unexpectedly.
- exception byro.bookkeeping.bank_import.api.InvalidBankTransaction(message=None, position=None)[source]
An importer yielded a transaction the core cannot persist.
- exception byro.bookkeeping.bank_import.api.InvalidImportFile(message=None, **kwargs)[source]
The importer could not interpret the uploaded file.
- exception byro.bookkeeping.bank_import.api.UnknownImporter(identifier=None, message=None)[source]
- exception byro.bookkeeping.bank_import.api.UnsupportedCurrency(currency, position=None)[source]
str(error) is always a message suitable for the user; technical details
go to the byro.bookkeeping.bank_import logger. The core logs the start,
completion (with counts) and failure (with the error class) of every import,
but never IBANs, names, memos or raw file content. Please follow the same
rule in your importer.
Import and matching are separate
Importers know nothing about members, fees or bookkeeping accounts. After the
core has created the bank booking, the existing process_transaction
pipeline runs unchanged, and matchers (for example one recognizing a member
number in the memo) augment the transaction. Because the core stores the
counterparty and reference fields in Booking.data, matchers work the same
for every importer.
Security
Uploaded bank files are untrusted input.
Handle malformed input gracefully and raise
InvalidImportFileinstead of letting arbitrary exceptions escape.Never execute or
evalfile content and never import modules based on file content.XML based formats (CAMT.053, MT94x wrappers) must disable external entities and DTD loading, must not perform network access while parsing and should limit resource usage (e.g. use
defusedxml). This is the plugin’s responsibility; byro’s core does not contain XML handling.Keep bank data out of exception messages and log lines.
byro limits the upload size (
BANK_TRANSACTION_IMPORT_MAX_FILE_SIZE, 25 MiB by default) and processes each source atomically.
Legacy API
Before this API existed, plugins implemented the whole import by receiving
process_csv_upload and creating Transaction and Booking objects
themselves. That signal is still sent for sources without an importer (it is
offered as “Legacy bank importer (plugin)” in the selection when a receiver is
connected), so existing plugins keep working. It is deprecated for new
development, and a source processed by a new importer never triggers it.