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

7.5 KiB
Raw Permalink Blame History

Coop ↔ Uniconta Match Engine Reference (Aktualiseret 2025-05-26)

Konkrete match-engine-mønstre fra iteration V1 → V6.1.


Match-filosofi: Faser i prioriteret rækkefølge

Fakturanummer-match (Reference ↔ Faktura) er det STÆRKESTE signal. Beløb+dato er sekundært. Sekvens:

V6.1 (anbefalet)

Fase 1: Reference + abs(beløb) + dato

  • Coop Reference == Uniconta Faktura (identiske 4-5 cifrede tal)
  • abs(Coop.Amount) == abs(Uniconta.Amount) ±5 kr (tolerance for afrunding)
  • Dato ±21 dage
  • Konteringstype-bonus (RE↔Faktura, RG↔Kreditnota, KG↔Kreditnota)
  • Score ~250
  • Resultat 2024-2025: ~2.421 matches

Fase 2: ZV↔Faktura (Coop positive fakturaer)

  • Coop Document Type == ZV (positive fakturaer udstedt til kunder)
  • Match mod Uniconta Konteringstype == Faktura
  • abs(beløb) ±5 kr + dato ±21 dage
  • Bonus: Document Number == Faktura nummer
  • Resultat 2024-2025: ~173 matches

Fase 3: RE↔Betaling uden reference

  • Coop Document Type == RE (betalinger) uden fakturanummer
  • Match mod Uniconta Konteringstype == Betaling
  • abs(beløb) ±5 kr + dato ±21 dage
  • Bonus: tekst-lighed ("coop", "0010014808")
  • Resultat 2024-2025: ~192 matches

Fase 4: Generel abs(beløb) + dato (fallback)

  • abs(beløb) matcher ±5 kr
  • Dato ±21 dage
  • Konteringstype-bonus, tekst-lighed, reference-bonus
  • Score ≥ 40
  • Resultat 2024-2025: ~1.939 matches

Fase 5: Mange-til-én

  • Sum af 2-3 Uniconta-poster == Coop-postering ±5 kr
  • Dato ±14 dage, max 25 kandidater
  • Resultat 2024-2025: ~276 matches

Total matches 2024-2025: 5.001 ud af 7.328 Coop / 6.717 Uniconta (~68%)


Tolerance matching (V6 innovation)

I stedet for eksakt beløbs-match med ±0,01 kr, brug ±5 kr tolerance:

AMT_TOL = 5.0  # DKK tolerance

# Index-building: bucket per tolerance interval
def build_index(transactions, max_days=21):
    idx = defaultdict(lambda: defaultdict(list))
    for t in transactions:
        for delta in range(-max_days, max_days + 1):
            d = t['date'] + timedelta(days=delta)
            bucket = round(t['abs_amount'] / AMT_TOL) * AMT_TOL
            idx[d][bucket].append(t)
    return idx

# Lookup: søg naboliggende buckets
bucket = round(c['abs_amount'] / AMT_TOL) * AMT_TOL
for b in [bucket - AMT_TOL, bucket, bucket + AMT_TOL]:
    candidates.extend(idx.get(c['date'], {}).get(b, []))

Fordel: Fanger afrundingsforskelle og delvise betalinger som ellers ville være uafstemte.

Bivirkning: Kan skabe flere kandidater → brug score-threshold og konteringstype-filter til at vælge den bedste.


Fortegn: MODSAT er normalt og korrekt

Coop type Fortegn Uniconta type Fortegn Match-mode
RE (betaling) Negativ Faktura Positiv abs(beløb)
ZV (faktura) Positiv Betaling Negativ abs(beløb)
RG (kreditnota) Positiv Kreditnota Negativ abs(beløb)
KG (rabat) Positiv Kreditnota Negativ abs(beløb)
# Match via abs(), men marker med fortegn-kolonne
amt_match = abs(coop_amount - uniconta_amount) <= AMT_TOL
fortegn = "SAMME" if (coop_amount > 0) == (uniconta_amount > 0) else "MODSAT"

Filtrering før match

Afgræns til regnskabsår i stedet for at filtrere på Doc_Type:

  • 2024+2025 fjerner 8.861 Coop og 5.684 Uniconta poster
  • Primært primo/XX/saldo-poster fra 2023 og 2026
  • Langt mere effektiv end Doc_Type-filtrering
Filter Kilde Hvad Konsekvens
År 2024-2025 Begge Regnskabsår-afgrænsning 56% færre poster
Primo Uniconta Startsaldo Fjernes ved års-filter
Afslutning Uniconta Årsafslutning Fjernes ved års-filter

Mange-til-én: Praktisk approach

Én Coop-postering kan svare til SUM af flere Uniconta-poster.

  • AVOID: itertools.combinations over 7.328 × 6.717 rækker = O(n²) timeout
  • USE: Index Uniconta per (dato, abs_beløb) og søg kombinationer indenfor ±14 dage
  • LIMIT: Maks 3 Uniconta-poster per Coop-postering, max 25 kandidater
  • TOLERANCE: ±5 kr (ikke ±0,01 kr)
  • Resultat: ~276 matches i 2024-2025 — større effekt end V5.1

Faktura-status: Netto-baseret + deduplikering

Problemet: Dobbelt-registrering

Samme betaling kan findes som både:

  • Coop RE (betaling) med fakturanummer i Reference
  • Uniconta Betaling med fakturanummer i Faktura-kolonnen

Hvis man summerer begge får man dobbelt så meget betalt som faktisk.

Løsning: Netto-sum per faktura

# 1. Beregn NETTO Coop betalinger per faktura
#    (positive RE trækker fra — det er modregninger)
coop_net = sum(c['amount'] for c in coop 
               if c['reference'] == fnr 
               and c['doc_type'] in ('RE', 'ZP'))
coop_paid = abs(coop_net) if coop_net < 0 else 0

# 2. Beregn NETTO Uniconta betalinger per faktora
uni_net = sum(u['amount'] for u in uni 
            if u['faktura'] == fnr 
            and u['konteringstype'] == 'Betaling')
uni_paid = abs(uni_net) if uni_net < 0 else 0

# 3. Dedupliker: hvis begge findes, brug kun én
if coop_paid > 0 and uni_paid > 0:
    if abs(coop_paid - uni_paid) <= AMT_TOL:
        betalt = coop_paid  # Samme betaling, tag én
    else:
        betalt = max(coop_paid, uni_paid)  # Delvise, tag største

MR8M-modregninger

Nogle fakturaer har både negativ og positiv RE med samme reference:

  • Faktura 47140: RE -8.984 + RE +8.984 (MR8M modregning) = netto 0
  • Med abs() ville dette tælle som 17.968 betalt (FORKERT)
  • Med netto-sum: abs(-8.984 + 8.984) = 0 (KORREKT)

Score-formel (V6.1 konkret)

# Fase 1 (Reference-match)
Score = 250 - dato_diff * 4 - abs_amount_diff * 2

# Fase 2 (ZV↔Faktura)
Score = 150 - dato_diff * 3 - abs_amount_diff * 2
if doc_number == faktura: +50
if tekst_identisk: +25
if tekst_delvis: +12
Threshold: ≥ 60

# Fase 3 (RE↔Betaling)
Score = 120 - dato_diff * 3 - abs_amount_diff * 2
if reference == faktura: +40
if "coop" in tekst: +15
Threshold: ≥ 60

# Fase 4 (Generel)
Score = 100 - dato_diff * 3 - abs_amount_diff * 0.5
if konteringstype_match: +25
if tekst_identisk: +25
if tekst_delvis: +12
if reference_match: +40
Threshold: ≥ 40

# Fase 5 (Mange-til-én)
Score = 150 (fast, kræver manuel godkendelse)

Rapport-struktur (Excel-ark)

Rækkefølgen er vigtig — brugeren vil se opsummering først:

  1. 0_Opsummering — nøgletal, antal matches, totalbeløb, difference, faktura-status
  2. 1_Faktura_Status — alle Uniconta fakturaer med BETALT/DELVIST/UBETALT
  3. 2_Matches — alle matchede par med type, score, fortegn, beløbs-diff, dato-diff
  4. 3_Alle_Coop — alle rå Coop-rækker med match-status kolonne
  5. 4_Alle_Uniconta — alle rå Uniconta-rækker med match-status kolonne
  6. 5_Kun_Coop — uafstemte Coop-poster
  7. 6_Kun_Uniconta — uafstemte Uniconta-poster
  8. 7_Maaneds_Oversigt — månedlig opsummering pr. år

Nøgle: Ark 3 og 4 bevarer overblik — brugeren vil se ALLE poster. Ark 5+6 viser kun dem der stadig mangler.


Bevar original rådata

Exportér ALTID:

  • Original række-nummer (Excel-rækkeindeks)
  • Filnavn og ark-navn
  • Rå JSON med alle kolonner Så brugeren kan spore enhver match tilbage til kildefilen.

Performance-tips

  • V6.1 kører på ~10 sekunder for 7.328 Coop + 6.717 Uniconta poster
  • Brug defaultdict(lambda: defaultdict(list)) til dato→beløb→post indeks
  • Uniconta parsing: pd.read_excel(file, header=3) for at læse fra række 4
  • Python 3.9 med pandas 2.3.3 — set PYTHONPATH hvis user-site-packages