Files
dennis 236053cd7f Initial import: 10 Radix skills across 3 categories
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.
2026-06-15 14:41:04 +02:00

9.7 KiB

Bank Reconciliation — Three-Source Analysis (2026-05-26)

Reference for matching bank transactions against Coop SAP exports and Uniconta ledgers. This extends the two-source reconciliation with a third (bank) dimension that serves as the "ground truth" for actual cash flow.


Bank file format (Danske Bank CSV export)

Kolonne Type Brug
Dato DD.MM.YYYY Primær dato
Valør DD.MM.YYYY Sekundær dato
Tekst string Identifikation
(tom) Padding
Beløb +/- flot/DK Primær beløb
Saldo flot/DK Sekundær — løbende saldo
Egen bilagsreference string Audit/trace

CSV separator: ; (semikolon) Encoding: latin-1 (CP1252) Decimal: Danish comma, Danish point-as-thousand-separator Negative: explicit minus in front

Typisk tekstmønster for Coop-indbetalinger:

  • "Coop 0010014808 -SE MEDD."
  • Samme kundenummer for ALLE transaktioner
  • Stort set alle transaktioner har identisk tekst

Bank transaktionskategorier

Standard indbetalinger (~98%)

  • Beløb fra ~30.000 kr til ~800.000 kr
  • Sum 2024: +9.225.626 kr (129 stk)
  • Sum 2025: +10.031.368 kr (118 stk)
  • Udgør den "faktiske" Coop-omsætning der rammer banken

Negative poster — bonus/modregning/retur (~1-2%)

Dato Beløb Tekst Type
13.06.2024 -18.750,00 COOP DANMARK AS Bonus-tilbagebetaling
25.11.2024 -31,25 Coop 365 Lokal retur/modregning
22.04.2025 -128,00 DK 57014 Coop Kvickly Slagelse Butiksretur
13.05.2025 -40,00 DK 50960 Coop Kvickly Slagelse Butiksretur

Total negative: -18.949,25 kr


THREE-SOURCE MATCHING CONCEPT

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Coop Excel     │     │  Uniconta Excel │     │  Bank CSV       │
│  (fakturaer)    │     │  (bogføring)    │     │  (indbetalinger)│
└────────┬────────┘     └────────┬────────┘     └────────┬────────┘
         │                       │                       │
         ▼                       ▼                       ▼
┌──────────────────────────────────────────────────────────────────┐
│          MATCH LAYER 1: Post-for-post (Coop ↔ Uniconta)          │
│  Reference+abs(amount)+date → 68% match rate                     │
├──────────────────────────────────────────────────────────────────┤
│          MATCH LAYER 2: Month-level (All 3 sources)              │
│  Sum(Coop) ≈ Sum(Uniconta) ≈ Sum(Bank) per month                │
├──────────────────────────────────────────────────────────────────┤
│          MATCH LAYER 3: Bonus reconciliation (separate)           │
│  Sum(KG + Kreditnota) per period ≈ Bank negative per release     │
│  (tolerate time lag — bank credit arrives later than postings)   │
└──────────────────────────────────────────────────────────────────┘

Bank-to-Coop / Bank-to-Uniconta matching

Month-level validation (PRIMARY)

Compare month totals across all three sources. If Coop and Uniconta diverge, the bank settles the dispute.

Måned Bank (kr) Coop (kr) Uniconta (kr) Status
2024-04 2.139.610 TBD TBD To verify
2024-05 1.632.814 TBD TBD To verify
2024-09 1.053.598 TBD TBD To verify

Interpretation rule:

  • Bank ≈ Coop: period is captured correctly in Coop
  • Bank ≈ Uniconta: period is correct in Uniconta
  • Bank >> Coop: missing Coop postings (likely late/omitted)
  • Bank >> Uniconta: missing Uniconta postings
  • Bank << both: bonus/modregning not yet reflected in accounting

Many-to-one: Bank single payment = sum of Coop invoices

Each bank posting (e.g. 641.168,06 kr) is a consolidated transfer that aggregates many individual Coop invoices. Algorithm:

  1. Take bank amount B on date D
  2. Find Coop transactions in [D-30 days, D+3 days] that sum to B ± tolerance
  3. This is a "subset-sum" / knapsack search:
    def find_combo(target, candidates, tol=5.0):
        # Greedy for small candidate sets
        # DP for larger sets
        candidates = sorted([c for c in candidates 
                            if abs(c.amount) <= target + tol])
        # ... subset sum with tolerance
    
  4. If exact match: mark all matched Coop invoices as "grouped under bank payment"
  5. If near-match (±500 kr): flag for manual review — likely partial payment or fees

Bonus / Rebate reconciliation

The mismatch:why Bank negatives don't match individual KG/Kreditnota

Coop KG-posteringer: 486 stk, total -1.101.849,97 kr Bank negative total: -18.949,25 kr (4 stk) Uniconta Kreditnota total: -4.593.829,17 kr Uniconta Rabat: 1 stk, -178.194,55 kr

Observation: Bank -18.750 kr does NOT match ANY individual Coop KG posting (nor any Uniconta Kreditnota). The nearest Uniconta Kreditnota is -18.961 (2025-03-21).

Interpretation: Bank negatives are consolidated credit notes or bonus payments that aggregate MANY smaller individual credit postings across a period (quarter or year). They arrive in the bank later than the individual postings.

Suggested bonus reconciliation strategy

  1. Period-based grouping (not transaction-based):
    • Group Coop KG by quarter/year
    • Group Uniconta Kreditnota by linked Faktura-number
    • Group Uniconta Rabat by year
  2. Compare aggregated totals per period with bank negatives per period:
    Coop KG per period = sum(KG amounts where Posting Date in period)
    Uniconta credit per period = sum(Kreditnota + Rabat amounts in period)
    Bank negative per period = sum(bank negative amounts with Value Date in period)
    
  3. **Differences indicate:
    • Timing: bank credit arrives 1-3 months after postings
    • Pooling: quarterly/yearly bonus vs monthly individual credits
    • Source mismatch: some credits originate from Uniconta only, some from Coop only
  4. Manual review for amounts > 50.000 kr discrepancies per period

Special: Uniconta "Rabat" -178.194,55 "Coop bonus 2023"

  • Single large posting on 2024-01-16
  • No matching bank negative from 2023 or early 2024 found
  • Likely an accrual/adjustment recognized in 2024 for 2023 bonus, but the actual cash flow may have been handled differently (e.g. applied as discount on future invoices rather than direct bank transfer)
  • Flag: accrual-type postings should NOT be matched against bank — they are non-cash adjustments. Separate them into "bonus accrued" vs "bonus paid".

Key insight: Three sources triangulate

Question How to answer with 3 sources
"Is this invoice paid?" Uniconta Faktura + Coop RE + Bank amount all agree
"Did bonus arrive in cash?" Bank negative confirms, Coop KG shows breakdown
"Is Uniconta missing a posting?" Bank has it but Uniconta doesn't → missing entry
"Is Coop missing a posting?" Bank has it but Coop doesn't → missing entry
"Is this payment split?" Bank single amount = sum of multiple Coop invoices
"Is there a bonus adjustment?" Coop KG present, Uniconta Kreditnota present, but bank negative missing → accrual

Practical notes for the webapp

Bank import

  • Import as a third source alongside Coop and Uniconta
  • Store as Transaction(source="bank", ...) with minimal fields:
    • date, amount, text, original_value_date, original_balance
  • Apply the same year filter (2024-2025) to exclude stray transactions

Bank-specific views

  1. Month-level dashboard: Three-bar chart per month (Coop, Uniconta, Bank)
  2. Divergence report: Months where |Coop - Bank| > tolerance
  3. Bonus reconciliation: Separate tab for aggregated KG/Kreditnota/Rabat vs bank negatives
  4. Many-to-one explorer: Click a bank transaction to see suggested Coop invoice combinations that sum to the bank amount

Data model extension for bank

class BankTransaction(models.Model):
    date = models.DateField()
    value_date = models.DateField(null=True)
    amount = models.DecimalField(max_digits=18, decimal_places=2)
    text = models.TextField()
    balance = models.DecimalField(max_digits=18, decimal_places=2)
    source_file = models.ForeignKey(SourceFile, ...)

Or reuse existing Transaction model with source="bank".


Performance / verification script

Use docker exec with the container's Python + pandas to parse bank CSVs and cross-check against already-loaded Coop/Uniconta data.

# Quick verification of month-level totals
import pandas as pd

def parse_bank_csv(path):
    df = pd.read_csv(path, sep=';', encoding='latin-1',
                     header=0, names=['Dato','Valør','Tekst','_','Beløb','Saldo','Ref'])
    df['Dato'] = pd.to_datetime(df['Dato'], format='%d.%m.%Y', errors='coerce')
    df['Beløb'] = df['Beløb'].str.replace('.', '').str.replace(',', '.').astype(float)
    return df

# Summarize by month and compare with Coop/month, Uniconta/month
# Run this as a Django management command or script inside the container