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

237 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```python
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) |
```python
# 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
```python
# 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:
0. `0_Opsummering` — nøgletal, antal matches, totalbeløb, difference, faktura-status
1. `1_Faktura_Status` — alle Uniconta fakturaer med BETALT/DELVIST/UBETALT
2. `2_Matches` — alle matchede par med type, score, fortegn, beløbs-diff, dato-diff
3. `3_Alle_Coop` — alle rå Coop-rækker med match-status kolonne
4. `4_Alle_Uniconta` — alle rå Uniconta-rækker med match-status kolonne
5. `5_Kun_Coop` — uafstemte Coop-poster
6. `6_Kun_Uniconta` — uafstemte Uniconta-poster
7. `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