236053cd7f
Kategoriserer danske regnskabs-/ERP-skills, ERP-data-afstemnings-
skill og to devops-workflows (gitea-issue-agent, kanban-workflows)
som Radix-medlemmers AI-agenter kan dele.
Struktur:
skills/<kategori>/<skill-navn>/SKILL.md
skills/<kategori>/<skill-navn>/{references,templates,scripts}/
Indekseret af index.json (genereret af scripts/rebuild_index.py).
Kør `python3 scripts/rebuild_index.py --check` i CI for at fange
synkroniseringsfejl.
3.8 KiB
3.8 KiB
Bank Reconciliation — Django Implementation Guide
Practical implementation notes for extending a two-source Coop↔Uniconta reconciliation webapp with bank CSV support.
Model extensions
Add these fields to Transaction (one migration):
account_number = models.CharField(max_length=50, blank=True, db_index=True)
bank_text = models.TextField(blank=True)
valuta_date = models.DateField(null=True, blank=True)
Also extend SourceFile.source choices and Transaction.source/match_type choices to include ("bank", "Bank") and ("bank_total", "Bank-samlet").
Bank CSV parser
Key implementation in apps/import_/parsers.py:
- Encodings: Try
latin-1first, thenutf-8. Most Danish bank exports use latin-1. - Delimiter:
;(semicolon) - Header mapping: Fuzzy-match by lowercasing, stripping null bytes and quotes:
dato→ date columnvalør/valuta→ value date columntekst/beskrivelse→ text columnbel(prefix) → amount columnsaldo→ balance columnreference/bilagsrefer→ reference column
- Date parse:
DD.MM.YYYY→ split by dots →datetime(int(parts[2]), int(parts[1]), int(parts[0])).date() - Amount parse:
- Strip
\xa0(nbsp),\u202f(narrow nbsp)," DKK" - If
.and,both present → remove dots, replace comma with dot - If only comma → replace with dot
- Convert to
Decimal
- Strip
- Bulk create: Use
Transaction.objects.bulk_create(txns_to_create)for performance - Account extraction: Regex
r"(\d{8,})"on text field to find kundenummer/kontonummer
Bank ↔ Coop match (many-to-one)
Group bank payments by (account_number, year, month) and match against the sum of unmatched Coop transactions in the same bucket.
bank_txns = Transaction.objects.filter(source="bank", year=year).values(
"account_number", "year", "month"
).annotate(total=Sum("amount"), count=Count("id"))
for bt in bank_txns:
coop_sum = Transaction.objects.filter(
source="coop", year=bt["year"], month=bt["month"],
match_status="unmatched", account_number=bt["account_number"],
).aggregate(s=Sum("amount"))["s"]
if coop_sum is not None and abs(bt["total"] - coop_sum) <= TOLERANCE:
# Create MatchGroup linking bank bucket to Coop bucket
...
Note: Bank amounts are positive (indbetalinger), Coop RE/Bankoverførsel amounts are negative. Compare absolute values. Set match_type="bank_total".
Views/templates changes
- Dashboard: Add
bank_count,bank_sumcards; addbank_count/bank_sumcolumns to monthly overview table - Upload form: Add
<option value="bank">Bank (Kontobevaegelser .csv)</option> - Reconciliation list: Add
bankto source filter; render bank badge with.badge-warning - Run match page: Document match phases: Reference → ZV↔Faktura → RE/Betaling → General → Many-to-one → Bank-total
Discovered pitfalls
- Migrations not detected: If
makemigrationsreports "No changes detected", check whether a prior manual migration already captured the changes. Runmigrate --checkfirst. - Docker file path mismatch: When testing via
docker compose exec, container filesystem may differ from host. Use the web UI upload form for file import testing. - Bank text homogeneity: All transactions from the same counterparty often have identical text (e.g.
"Coop 0010014808 -SE MEDD."). Text similarity is useless for matching. Rely onaccount_number+ date+amount grouping. - Bank negatives are rare but critical: Negative bank amounts (~1-2%) represent bonus/modregning/retur. Do NOT match these against positive Coop invoices. Flag separately for bonus reconciliation.
- Bank total may differ slightly from Coop sum: Normal difference is ±0.1% (fees, rounding). Use tolerance matching (e.g. ±5 DKK) for bank-total matches.