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:
from datetime import date
from decimal import Decimal
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from byro.bookkeeping.bank_import import (
BankTransactionImporter,
ImportedBankTransaction,
InvalidImportFile,
)
from byro.bookkeeping.signals import bank_transaction_importers
class ExampleBankImporter(BankTransactionImporter):
identifier = "byro_example.bank"
label = _("Example Bank")
def parse(self, source):
with source.source_file.open("rb") as f:
for row in parse_rows(f): # your format specific code
yield ImportedBankTransaction(
booking_date=date(2026, 9, 1),
amount=Decimal("25.00"),
currency="EUR",
memo="Membership fee",
counterparty_name="Max Mustermann",
counterparty_iban="DE12 3456 7890 1234 5678 90",
external_id="123456789",
)
@receiver(bank_transaction_importers)
def register_example_importer(sender, **kwargs):
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_finance_import_bank_files.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¶
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 > 0 is money arriving on the bank account,
amount < 0 is money leaving the bank account.
Note
The per-field descriptions below are maintained by hand: they are
documentation comments in the source (#: above each field), a
convention Griffe does not extract for dataclass fields, so mkdocstrings
only renders the class itself above. Check
src/byro/bookkeeping/bank_import/api.py when changing a field.
booking_date- The date the bank booked the transaction (required).
value_datedefaults to the booking date. amount- A
decimal.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. currency- ISO 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. memo- The purpose / remittance information. Truncated to 1000 characters.
counterparty_name,counterparty_iban,counterparty_bic- Information 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_id- A 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_code- Optional SEPA and bank references. They are stored with the booking and contribute to the fallback fingerprint.
data- Format 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
:class:BankTransactionImporter and register it via the
byro.bookkeeping.signals.bank_transaction_importers signal. The importer
only parses its input and yields :class:ImportedBankTransaction objects;
byro's :class:~byro.bookkeeping.bank_import.service.BankTransactionImportService
takes care of validation, duplicate detection, persistence and matching.
BankTransactionImportError ¶
Bases: Exception
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.
UnknownImporter ¶
Bases: BankTransactionImportError
InvalidImportFile ¶
ImporterError ¶
InvalidBankTransaction ¶
UnsupportedCurrency ¶
Bases: InvalidBankTransaction
UnknownImporter and UnsupportedCurrency have no class docstring in the
source; their default_message is the user-facing text.
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.