From 236053cd7f596f16899dcffc1629b37fc41b66c8 Mon Sep 17 00:00:00 2001 From: Dennis Juhler Aagaard Date: Mon, 15 Jun 2026 14:41:04 +0200 Subject: [PATCH] Initial import: 10 Radix skills across 3 categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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///SKILL.md skills///{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. --- .gitignore | 25 ++ README.md | 67 ++++ index.json | 215 ++++++++++++ scripts/rebuild_index.py | 124 +++++++ skills/accounting/coop-bankimport/SKILL.md | 198 +++++++++++ skills/accounting/dansk-csv-import/SKILL.md | 178 ++++++++++ .../accounting/e-conomic-integration/SKILL.md | 290 ++++++++++++++++ skills/accounting/kontoplan-mapping/SKILL.md | 183 ++++++++++ skills/accounting/momsafregning/SKILL.md | 161 +++++++++ skills/accounting/radix-accounting/SKILL.md | 104 ++++++ skills/accounting/resultatopgoerelse/SKILL.md | 148 +++++++++ .../erp-data-reconciliation/SKILL.md | 314 ++++++++++++++++++ .../references/auditor-pack-and-dedup.md | 66 ++++ .../references/bank-implementation.md | 73 ++++ .../references/bank-reconciliation.md | 222 +++++++++++++ .../coop-invoice-payment-allocation.md | 109 ++++++ .../references/coop-payment-advice.md | 125 +++++++ .../references/coop-uniconta-analysis.md | 235 +++++++++++++ ...p-uniconta-import-ui-and-payment-advice.md | 89 +++++ .../references/coop-uniconta-match-engine.md | 236 +++++++++++++ .../django-accounting-ui-formatting.md | 122 +++++++ .../idempotent-source-file-imports.md | 122 +++++++ .../reconciliation-webapp-architecture.md | 167 ++++++++++ .../three-way-django-analysis-engine.md | 146 ++++++++ .../scripts/excel-analyzer.py | 211 ++++++++++++ skills/devops/gitea-issue-agent/SKILL.md | 248 ++++++++++++++ .../references/identity-and-safety.md | 101 ++++++ .../scripts/verify-plugin-and-gitea.sh | 85 +++++ .../templates/proposal-comment.md | 40 +++ skills/devops/kanban-workflows/SKILL.md | 56 ++++ 30 files changed, 4460 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.json create mode 100644 scripts/rebuild_index.py create mode 100644 skills/accounting/coop-bankimport/SKILL.md create mode 100644 skills/accounting/dansk-csv-import/SKILL.md create mode 100644 skills/accounting/e-conomic-integration/SKILL.md create mode 100644 skills/accounting/kontoplan-mapping/SKILL.md create mode 100644 skills/accounting/momsafregning/SKILL.md create mode 100644 skills/accounting/radix-accounting/SKILL.md create mode 100644 skills/accounting/resultatopgoerelse/SKILL.md create mode 100644 skills/data-science/erp-data-reconciliation/SKILL.md create mode 100644 skills/data-science/erp-data-reconciliation/references/auditor-pack-and-dedup.md create mode 100644 skills/data-science/erp-data-reconciliation/references/bank-implementation.md create mode 100644 skills/data-science/erp-data-reconciliation/references/bank-reconciliation.md create mode 100644 skills/data-science/erp-data-reconciliation/references/coop-invoice-payment-allocation.md create mode 100644 skills/data-science/erp-data-reconciliation/references/coop-payment-advice.md create mode 100644 skills/data-science/erp-data-reconciliation/references/coop-uniconta-analysis.md create mode 100644 skills/data-science/erp-data-reconciliation/references/coop-uniconta-import-ui-and-payment-advice.md create mode 100644 skills/data-science/erp-data-reconciliation/references/coop-uniconta-match-engine.md create mode 100644 skills/data-science/erp-data-reconciliation/references/django-accounting-ui-formatting.md create mode 100644 skills/data-science/erp-data-reconciliation/references/idempotent-source-file-imports.md create mode 100644 skills/data-science/erp-data-reconciliation/references/reconciliation-webapp-architecture.md create mode 100644 skills/data-science/erp-data-reconciliation/references/three-way-django-analysis-engine.md create mode 100644 skills/data-science/erp-data-reconciliation/scripts/excel-analyzer.py create mode 100644 skills/devops/gitea-issue-agent/SKILL.md create mode 100644 skills/devops/gitea-issue-agent/references/identity-and-safety.md create mode 100644 skills/devops/gitea-issue-agent/scripts/verify-plugin-and-gitea.sh create mode 100644 skills/devops/gitea-issue-agent/templates/proposal-comment.md create mode 100644 skills/devops/kanban-workflows/SKILL.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1996668 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.egg-info/ +.venv/ +venv/ +.pytest_cache/ + +# macOS +.DS_Store + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs and temp +*.log +*.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..6239674 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Radix-Skills + +Delt AI-skill-bibliotek for Radix. Bruges af alle medarbejderes AI-agenter +(Hermes, Codex m.fl.) til dansk regnskab/ERP, data-afstemning og devops-workflows. + +> **Hvad er en "skill"?** En self-contained YAML-frontmatter + markdown-fil +> (`SKILL.md`) der beskriver et domæne-specifikt workflow, som en AI-agent kan +> loade på forespørgsel. Tænk på det som en delt playbook for en bestemt +> type opgave. + +## Struktur + +```text +Radix-Skills/ +├── README.md # denne fil +├── index.json # maskinlæseligt index over alle skills +└── skills/ + ├── accounting/ # dansk regnskab, bogføring, ERP + │ ├── radix-accounting/ # top-level bogholderi-skill (sprog, formater, aktivering) + │ ├── coop-bankimport/ + │ ├── dansk-csv-import/ + │ ├── e-conomic-integration/ + │ ├── kontoplan-mapping/ + │ ├── momsafregning/ + │ └── resultatopgoerelse/ + ├── data-science/ + │ └── erp-data-reconciliation/ # afstemning mellem ERP-systemer + └── devops/ + ├── gitea-issue-agent/ # agent-drevet Gitea issue/PR-workflow + └── kanban-workflows/ +``` + +Hver skill ligger i sin egen mappe og har mindst `SKILL.md`. Større skills +kan have undermapper: + +- `references/` — uddybende dokumentation der kun loades ved behov +- `templates/` — skabeloner (f.eks. issue-kommentarer, rapporter) +- `scripts/` — hjælpescripts der følger med skillen + +## Hvordan en agent bruger repo'et + +1. Læs `index.json` for at finde relevante skills ud fra tags/description. +2. Gå ind i den relevante `skills///` mappe. +3. Læs `SKILL.md` (med YAML-frontmatter) for at aktivere skillen. +4. Følg de nummererede steps i skillen. +5. Hvis skillen henviser til en reference, load den først når det er relevant + (ikke alle på én gang). + +## Bidrag + +Nye skills skal: + +1. Lægges under `skills///` med en `SKILL.md`. +2. Følge SKILL.md-formatet (YAML-frontmatter med `name`, `description`, + `version`, `author`, `license` + markdown body). +3. Have en kort `description` der kan bruges til trigger-matching. +4. Have en `version` der følger semver. +5. Blive registreret i `index.json` (kør `python3 scripts/rebuild_index.py`). + +Åbn en Gitea-PR med ændringerne — `gitea-issue-agent`-skillen kan guide dig. + +## Vedligeholdelse + +- `index.json` genereres automatisk af `scripts/rebuild_index.py` ud fra + frontmatter i alle `SKILL.md` filer. +- Store ændringer (ny kategori, formatændring) kræver opdatering af denne + `README.md`. diff --git a/index.json b/index.json new file mode 100644 index 0000000..b2753c5 --- /dev/null +++ b/index.json @@ -0,0 +1,215 @@ +{ + "repository": "Radix-Skills", + "version": 1, + "description": "Delt AI-skill-bibliotek for Radix — bruges af alle medarbejderes AI-agenter (Hermes, Codex m.fl.) til dansk regnskab/ERP/data/devops.", + "categories": [ + "accounting", + "data-science", + "devops" + ], + "skill_count": 10, + "skills": [ + { + "category": "accounting", + "name": "coop-bankimport", + "path": "accounting/coop-bankimport/SKILL.md", + "description": "Reconcilier Coop Danmark-bankbetalinger mod Uniconta- eller e-conomic-fakturaer (og kreditnotaer, bonus, modregninger) — aktiveres ved \"Coop afstemning\", \"Coop bankbetaling\", \"Coop opgørelse\", \"forenel Coop betaling med faktura\", \"Coop kreditnota\".", + "version": "0.2.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "coop-danmark", + "uniconta", + "e-conomic", + "reconciliation", + "bankafstemning" + ], + "files": [ + "accounting/coop-bankimport/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "dansk-csv-import", + "path": "accounting/dansk-csv-import/SKILL.md", + "description": "Importér danske bank-CSV-eksporter (Nordea, Danske Bank, Jyske Bank, Sydbank) og e-conomic-bankimport-format — aktiveres ved \"importér CSV\", \"bankudtog\", \"kontoudtog\", \"Nordea eksport\", \"Danske Bank CSV\", \"Jyske Bank eksport\", \"e-conomic bankimport\".", + "version": "0.2.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "csv-import", + "bank", + "nordea", + "danske-bank", + "jyske-bank", + "e-conomic" + ], + "files": [ + "accounting/dansk-csv-import/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "e-conomic-integration", + "path": "accounting/e-conomic-integration/SKILL.md", + "description": "Integration med e-conomic cloud-bogføring — aktiveres ved \"bogfør i e-conomic\", \"hent fakturaer fra e-conomic\", \"kassekladde\", \"sync mod e-conomic\", \"opret kunde i e-conomic\", \"kontoudtog e-conomic\", \"REST API\", \"AppSecretToken\". Dækker auth, kontoplan, kassekladder, bogførte fakturaer, kreditorer, kunder og banktransaktioner.", + "version": "0.1.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "e-conomic", + "dansk-regnskab", + "api", + "cloud-bogfoering", + "integration" + ], + "files": [ + "accounting/e-conomic-integration/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "kontoplan-mapping", + "path": "accounting/kontoplan-mapping/SKILL.md", + "description": "Kortlæg transaktioner, fakturaer og bilag til dansk standardkontoplan (SUM, DG, SL) eller bogføringssystem-specifik kontoplan (e-conomic 4-cifret, Uniconta) — aktiveres ved \"hvilken konto?\", \"kontoplan\", \"kontorådgivning\", \"bogføring af [transaktionstype]\", \"hvor skal jeg bogføre dette\".", + "version": "0.2.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "kontoplan", + "SUM", + "DG", + "SL", + "uniconta", + "e-conomic" + ], + "files": [ + "accounting/kontoplan-mapping/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "momsafregning", + "path": "accounting/momsafregning/SKILL.md", + "description": "Beregn dansk momsangivelse (moms, momsfradrag, momsrefusion) til SKAT — aktiveres ved forespørgsler om \"moms\", \"momsangivelse\", \"momsrefusion\", \"kvartalsmoms\", \"halvårsmoms\", \"momsregnskab\", \"25% moms\", \"momsfri\".", + "version": "0.1.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "moms", + "skat", + "momsangivelse" + ], + "files": [ + "accounting/momsafregning/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "radix-accounting", + "path": "accounting/radix-accounting/SKILL.md", + "description": "Dansk bogføring, regnskab og ERP-assistance til Radix-portalen — aktiveres ved forespørgsler om resultatopgørelse, momsafregning, kontoplan, CSV-import, bankafstemning, Uniconta-integration, e-conomic-integration eller Coop Danmark-reconciliation. Svar altid på dansk, brug dansk talformatering (1.234.567,89) og dd-MM-yyyy datoformat.", + "version": "0.2.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "moms", + "radix-erp", + "uniconta", + "e-conomic", + "coop" + ], + "files": [ + "accounting/radix-accounting/SKILL.md" + ] + }, + { + "category": "accounting", + "name": "resultatopgoerelse", + "path": "accounting/resultatopgoerelse/SKILL.md", + "description": "Byg en dansk resultatopgørelse (P&L) ud fra Radix ERP-data — aktiveres ved forespørgsler om \"resultatopgørelse\", \"P&L\", \"årsregnskab\", \"indtjening pr. måned/kvartal/år\".", + "version": "0.1.0", + "author": "radix-accountant", + "tags": [ + "accounting", + "dansk-regnskab", + "resultatopgoerelse", + "radix-erp" + ], + "files": [ + "accounting/resultatopgoerelse/SKILL.md" + ] + }, + { + "category": "data-science", + "name": "erp-data-reconciliation", + "path": "data-science/erp-data-reconciliation/SKILL.md", + "description": "Analyze and reconcile financial/accounting data between two ERP systems (e.g. SAP/Coop and Uniconta) using Excel exports. Covers file inspection, column mapping, Danish-format normalization, match-engine design, and iterative build workflow.", + "version": "", + "author": "", + "tags": [], + "files": [ + "data-science/erp-data-reconciliation/SKILL.md", + "data-science/erp-data-reconciliation/references/auditor-pack-and-dedup.md", + "data-science/erp-data-reconciliation/references/bank-implementation.md", + "data-science/erp-data-reconciliation/references/bank-reconciliation.md", + "data-science/erp-data-reconciliation/references/coop-invoice-payment-allocation.md", + "data-science/erp-data-reconciliation/references/coop-payment-advice.md", + "data-science/erp-data-reconciliation/references/coop-uniconta-analysis.md", + "data-science/erp-data-reconciliation/references/coop-uniconta-import-ui-and-payment-advice.md", + "data-science/erp-data-reconciliation/references/coop-uniconta-match-engine.md", + "data-science/erp-data-reconciliation/references/django-accounting-ui-formatting.md", + "data-science/erp-data-reconciliation/references/idempotent-source-file-imports.md", + "data-science/erp-data-reconciliation/references/reconciliation-webapp-architecture.md", + "data-science/erp-data-reconciliation/references/three-way-django-analysis-engine.md", + "data-science/erp-data-reconciliation/scripts/excel-analyzer.py" + ] + }, + { + "category": "devops", + "name": "gitea-issue-agent", + "path": "devops/gitea-issue-agent/SKILL.md", + "description": "Build a Hermes-managed autonomous issue handler for a self-hosted Gitea instance — scan issues, analyze text + screenshot attachments, post a proposal for human approval, implement, test, push a branch, and open a Gitea PR. Use when the user wants Hermes to act on Gitea issues end-to-end with a human-in-the-loop approval gate, especially with image/visual context in the issue text.", + "version": "0.1.0", + "author": "Hermes Agent", + "tags": [ + "gitea", + "issues", + "automation", + "hermes-plugin", + "pr-workflow", + "agent-loop", + "approval-gate" + ], + "files": [ + "devops/gitea-issue-agent/SKILL.md", + "devops/gitea-issue-agent/references/identity-and-safety.md", + "devops/gitea-issue-agent/scripts/verify-plugin-and-gitea.sh", + "devops/gitea-issue-agent/templates/proposal-comment.md" + ] + }, + { + "category": "devops", + "name": "kanban-workflows", + "path": "devops/kanban-workflows/SKILL.md", + "description": "Use when operating Hermes Kanban workflows: decomposing work as an orchestrator, executing as a worker, managing lanes, status, blockers, and reconciliation.", + "version": "1.0.0", + "author": "Hermes Agent", + "tags": [ + "kanban", + "orchestration", + "workers", + "task-management", + "hermes" + ], + "files": [ + "devops/kanban-workflows/SKILL.md" + ] + } + ] +} diff --git a/scripts/rebuild_index.py b/scripts/rebuild_index.py new file mode 100644 index 0000000..5d2dd5e --- /dev/null +++ b/scripts/rebuild_index.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Rebuild index.json from SKILL.md frontmatter in this repository. + +Usage: + python3 scripts/rebuild_index.py + python3 scripts/rebuild_index.py --check # exit 1 if index.json is out of date + +The index is the canonical machine-readable manifest of all skills in this +repository. Regenerate it whenever a SKILL.md is added, removed, or has its +frontmatter changed. Commit index.json together with the SKILL.md change. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKILLS_ROOT = REPO_ROOT / "skills" +INDEX_PATH = REPO_ROOT / "index.json" + +FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL) +KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*(.+?)(?=\n[A-Za-z_]|\Z)", re.MULTILINE | re.DOTALL) + + +def _grab(frontmatter: str, key: str) -> str: + m = re.search(rf"^{key}:\s*(.+?)(?=\n[A-Za-z_]|\Z)", frontmatter, re.MULTILINE | re.DOTALL) + if not m: + return "" + val = m.group(1).strip() + # strip YAML block indicators and quotes + val = val.strip(">").strip("|").strip() + if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")): + val = val[1:-1] + return val.strip() + + +def _grab_tags(frontmatter: str) -> list[str]: + m = re.search(r"tags:\s*\[(.*?)\]", frontmatter, re.DOTALL) + if not m: + return [] + return [t.strip().strip('"').strip("'") for t in m.group(1).split(",") if t.strip()] + + +def build_index() -> dict: + skills: list[dict] = [] + for skill_md in sorted(SKILLS_ROOT.rglob("SKILL.md")): + rel = skill_md.relative_to(SKILLS_ROOT) + parts = rel.parts + category = parts[0] if len(parts) > 1 else "root" + name = parts[-2] if len(parts) > 1 else "root" + + text = skill_md.read_text(encoding="utf-8") + fm_match = FRONTMATTER_RE.match(text) + frontmatter = fm_match.group(1) if fm_match else "" + + desc = _grab(frontmatter, "description").replace("\n", " ").strip() + version = _grab(frontmatter, "version") + author = _grab(frontmatter, "author") + tags = _grab_tags(frontmatter) + + all_files = sorted( + str(p.relative_to(SKILLS_ROOT)) + for p in skill_md.parent.rglob("*") + if p.is_file() + ) + + skills.append( + { + "category": category, + "name": name, + "path": str(rel), + "description": desc, + "version": version, + "author": author, + "tags": tags, + "files": all_files, + } + ) + + return { + "repository": "Radix-Skills", + "version": 1, + "description": ( + "Delt AI-skill-bibliotek for Radix — bruges af alle medarbejderes " + "AI-agenter (Hermes, Codex m.fl.) til dansk regnskab/ERP/data/devops." + ), + "categories": sorted({s["category"] for s in skills}), + "skill_count": len(skills), + "skills": skills, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Exit 1 if index.json is stale") + args = parser.parse_args() + + new_index = build_index() + new_json = json.dumps(new_index, ensure_ascii=False, indent=2) + "\n" + + if args.check: + if not INDEX_PATH.exists(): + print("index.json does not exist", file=sys.stderr) + return 1 + current = INDEX_PATH.read_text(encoding="utf-8") + if current != new_json: + print("index.json is out of date — run scripts/rebuild_index.py", file=sys.stderr) + return 1 + return 0 + + INDEX_PATH.write_text(new_json, encoding="utf-8") + print( + f"Wrote {INDEX_PATH.relative_to(REPO_ROOT)} with {new_index['skill_count']} skills " + f"across {len(new_index['categories'])} categories: {', '.join(new_index['categories'])}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/accounting/coop-bankimport/SKILL.md b/skills/accounting/coop-bankimport/SKILL.md new file mode 100644 index 0000000..507176b --- /dev/null +++ b/skills/accounting/coop-bankimport/SKILL.md @@ -0,0 +1,198 @@ +--- +name: coop-bankimport +description: > + Reconcilier Coop Danmark-bankbetalinger mod Uniconta- eller e-conomic-fakturaer (og kreditnotaer, + bonus, modregninger) — aktiveres ved "Coop afstemning", "Coop bankbetaling", "Coop opgørelse", + "forenel Coop betaling med faktura", "Coop kreditnota". +version: 0.2.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, coop-danmark, uniconta, e-conomic, reconciliation, bankafstemning] + related: [dansk-csv-import, kontoplan-mapping, resultatopgoerelse, e-conomic-integration] +--- + +# Coop Danmark-bankimport & afstemning (Uniconta eller e-conomic) + +Specifikt workflow til **Coop Danmark ↔ [Uniconta|e-conomic]-afstemning**. Hver bankbetaling fra Coop følges af en specifikation (opgørelse) der viser præcis hvilke fakturaer, kreditnotaer, bonusser og modregninger beløbet dækker. Målet er at matche **bankbetalinger (Coop) → opgørelseslinjer → bogføringssystem-bilag** og producere bogføringsklare posteringer. + +## Hvilket bogføringssystem? + +| System | Fordel ved Coop-afstemning | Ulempe | +|---|---|---| +| **Uniconta** | Lokal data, ingen API-rate-limit, kender hele kontohistorik | Manuel CSV-eksport nødvendig | +| **e-conomic** | Cloud-baseret, kan bogføre direkte via API, kunde 1 (Coop) findes typisk allerede | Rate-limit, kræver AppSecretToken | + +**Spørg brugeren** hvilket system de bruger — svaret bestemmer hvordan data hentes og hvordan bogføringsforslaget præsenteres. + +## Aktiveres ved + +- "Forenel denne Coop-bankbetaling med Uniconta" +- "Importer Coop-opgørelse for uge 22" +- "Hvorfor matcher Coop-bankbetalingen ikke Uniconta-saldoen?" +- "Generer bogføringsforslag for denne Coop-indbetaling" + +## Forventede input + +Brugeren leverer typisk: +1. **Coop bankbetalings-CSV** (Sydbank, Jyske Bank eller Danske Bank — særskilt format for Coop) +2. **Coop-specifikation** (PDF eller Excel) — viser linjer for hver faktura +3. **Bogføringssystem-data** (ét af): + - **Uniconta-eksport** (fakturaer, kreditnotaer, åbne poster) — typisk Excel + - **e-conomic API-kald** (se `e-conomic-integration` skill): `/invoices/booked?filter=balance$gt:0&filter=customer.name$like:Coop` + +## Coops bankbetalingsformat (typisk) + +``` +Bogføringsdato;Posteringstekst;Beløb;Saldo +15-06-2026;"Coop Danmark A/S Bankbetaling 12345678";-23.456,78;100.000,00 +``` +- **Negativ beløb** = du har modtaget penge fra Coop (kredit på din konto) +- **Reference** = Coops bankaftale-id (8 cifre typisk) +- **Dato** = afregningsdato + +## Coops specifikation (Excel/PDF, eksempel) + +| Fakturanr | Fakturadato | Beløb | Moms | Total | Type | +|---|---|---|---|---|---| +| 2026-1234 | 01-06-2026 | 1.000,00 | 250,00 | 1.250,00 | Faktura | +| 2026-1235 | 02-06-2026 | 2.500,00 | 625,00 | 3.125,00 | Faktura | +| KN-2026-100 | 03-06-2026 | -500,00 | -125,00 | -625,00 | Kreditnota | +| Bonus-2026-06 | 30-06-2026 | -1.000,00 | 0,00 | -1.000,00 | Bonus | +| Modregning-2026-06 | 30-06-2026 | -250,00 | 0,00 | -250,00 | Modregning | +| **Total** | | **1.750,00** | **750,00** | **2.500,00** | | + +Bankbetalingen på 2.500,00 kr. dækker altså: 2 fakturaer − 1 kreditnota − bonus − modregning. + +## Workflow + +### Fase 1: Importér bankbetaling +1. **Modtag Coop-CSV** (typisk Sydbank-format) +2. **Filtrér Coop-poster** (tekst matcher `Coop Danmark`) +3. **Vis hver bankbetaling** med reference, dato, beløb + +### Fase 2: Importér opgørelse (specifikation) +1. **Modtag specifikationsfil** (PDF eller Excel) +2. **Parse linjer**: fakturaer, kreditnotaer, bonus, modregninger +3. **Beregn totaler** og verificer at de matcher bankbetalingen +4. **Hvis differens**: marker som "afventer manuel kontrol" + +### Fase 3: Match mod bogføringssystem + +**Variant A: Uniconta** +1. **Hent Uniconta-fakturaer** (via API eller eksport) for samme periode +2. **For hver faktura i opgørelsen**: søg i Uniconta efter match på: + - **Primær match**: fakturanr + beløb (høj confidence) + - **Sekundær match**: beløb + dato ±7 dage (medium confidence) + - **Ingen match**: marker som "mangler i Uniconta" + +**Variant B: e-conomic** +1. **Hent bogførte fakturaer** via API: + ``` + GET /invoices/booked?filter=balance$gt:0&filter=customer.name$like:Coop + ``` +2. **Filtrér på kunde** (Coop Danmark A/S) — de fleste Coops kunder findes som kunde 1, men tjek +3. **For hver faktura i opgørelsen**: søg i e-conomic-respons efter match på: + - **Primær match**: `bookedInvoiceNumber` + `grossAmount` (høj confidence) + - **Sekundær match**: `grossAmount` + `date` ±7 dage (medium confidence) + - **Ingen match**: marker som "mangler i e-conomic" + +**Vis match-status** for hver linje (uanset variant): + - ✅ Matchet (faktura fundet) + - ⚠️ Forskelligt beløb (faktura fundet, men beløb afviger) + - ❌ Manglende (faktura ikke fundet) + +### Fase 4: Generer bogføringsforslag +For hver matchet bankbetaling: + +``` +BOGFØRINGSFORSLAG — Coop bankbetaling [reference] +═══════════════════════════════════════════════════ +Dato: [dd-MM-yyyy] +Bankbeløb: 2.500,00 kr. (positivt = indbetaling) +─────────────────────────────────────────────────── + +DEBET (kontonr / navn) BELØB +─────────────────────────────────────────────────── +[56200] Bank, Sydbank 2.500,00 + +KREDIT (kontonr / navn) BELØB +─────────────────────────────────────────────────── +[10120] Tilgodehavender, salg 1.250,00 (faktura 2026-1234) +[10120] Tilgodehavender, salg 3.125,00 (faktura 2026-1235) +[10120] Tilgodehavender, salg −625,00 (kreditnota KN-2026-100) +[52100] Bonus og rabatter 1.000,00 (bonus) +[58000] Modregning 250,00 (modregning) + +Total debet: 2.500,00 +Total kredit: 2.500,00 +═══════════════════════════════════════════════════ +Diff: 0,00 ✅ Balanceret +═══════════════════════════════════════════════════ +``` + +### Fase 5: Bruger-godkendelse +⚠️ **Vis altid bogføringsforslaget og vent på eksplicit godkendelse** før der postes til Radix/Uniconta/e-conomic. + +**Hvis brugeren vil bogføre direkte i e-conomic** (se `e-conomic-integration` skill): +1. Vis bogføringsforslaget med konkrete kontonumre +2. Generer `entries` JSON-objekt klar til `/journals/:id/vouchers/:n/entries` POST +3. Generer et UUID v4 Idempotency-Key +4. Vent på "ja, bogfør" — udfør derefter API-kald +5. Verificer at svaret er 201 Created og at `entries` indeholder bogførings-ID + +## Almindelige problemer + +- ❌ **Bankbeløb matcher ikke opgørelse-total**: typisk fordi opgørelsen kun viser netto (efter Coops egne fradrag som emballage-pant, kampagnerabat, mv.) +- ❌ **Faktura findes ikke i Uniconta**: kan skyldes at fakturaen er bogført i forkert regnskabsår, eller at Uniconta-eksporten er ufuldstændig +- ❌ **Kreditnota findes, men med forkert fortegn**: dobbelt-negativ bliver positivt — håndtér dette eksplicit +- ❌ **Coop-bonus mangler konto**: virksomheder der ikke har en dedikeret bonuskonto bruger typisk 52100 (Bonus/rabatter) — bekræft med brugeren +- ❌ **Modregning uden specifikation**: spørg brugeren — kan være emballage-pant, reklamation, eller andet +- ✅ ALTID verificer at bankbeløb = sum(opgørelseslinjer) ±0,01 kr. (afrundingstolerance) +- ✅ ALTID vis match-status per linje (matchet / afvigende / manglende) +- ✅ ALTID vent på brugerens "ja, bogfør" før API-kald + +## Kontering (typisk dansk kontoplan) + +| Kontotype | Kontonr (SUM/DG) | Bruges til | +|---|---|---| +| Bank | 56200 | Bank, Sydbank/Jyske/Danske (pengebevægelsen) | +| Tilgodehavender | 10120 | Åbne fakturaer der betales af Coop | +| Bonus/rabatter | 52100 | Coops bonus, årsbonus, kampagnerabat | +| Modregning | 58000 | Emballage-pant, reklamation, Cops egne fradrag | +| Kreditnotaer | 10120 (negativ) | Coops kreditnotaer (vareretur mv.) | + +Tallene er eksempler — bekræft altid med virksomhedens egen kontoplan. + +## Eksempel-aktivering + +**Bruger:** "Jeg har lige fået en Coop-betaling på 12.345,67 kr. d. 15-06-2026. Her er opgørelsen — kan du finde ud af hvad der dækker hvad?" + +**Svar (forkortet):** +> ✅ Bankbetaling genkendt: 12.345,67 kr. d. 15-06-2026 +> +| Opgørelseslinje | Beløb | Uniconta-match | Status | +> |---|---|---|---| +> | Faktura 2026-1234 | 1.250,00 | UN-5678 | ✅ Matchet | +> | Faktura 2026-1235 | 3.125,00 | UN-5679 | ✅ Matchet | +> | Faktura 2026-1240 | 5.000,00 | UN-5680 | ✅ Matchet | +> | Kreditnota KN-2026-100 | -625,00 | UN-5681 | ✅ Matchet | +> | Bonus Q2 2026 | -1.404,33 | (bogføres direkte) | ⚠️ Ingen reference | +> | **Total** | **7.345,67** | | | +> | **Diff vs bank** | **5.000,00** | | ⚠️ Mangler 5.000,00 kr. | +> +> **Bogføringsforslag:** +> ``` +> DEBET 56200 Bank 12.345,67 +> KREDIT 10120 Tilgodehavender 1.250,00 +> KREDIT 10120 Tilgodehavender 3.125,00 +> KREDIT 10120 Tilgodehavender 5.000,00 +> KREDIT 10120 Tilgodehavender -625,00 +> KREDIT 52100 Bonus -1.404,33 +> ``` +> +> ⚠️ **Bemærk:** Diff på 5.000,00 kr. — enten mangler en faktura i opgørelsen, eller opgørelsen er ufuldstændig. Vil du: +> 1. Indberette bogføringsforslag som det er (med diff markeret) +> 2. Springe bankbetalingen over indtil videre +> 3. Kontakte Coop for komplet specifikation diff --git a/skills/accounting/dansk-csv-import/SKILL.md b/skills/accounting/dansk-csv-import/SKILL.md new file mode 100644 index 0000000..ef71593 --- /dev/null +++ b/skills/accounting/dansk-csv-import/SKILL.md @@ -0,0 +1,178 @@ +--- +name: dansk-csv-import +description: > + Importér danske bank-CSV-eksporter (Nordea, Danske Bank, Jyske Bank, Sydbank) og + e-conomic-bankimport-format — aktiveres ved "importér CSV", "bankudtog", "kontoudtog", + "Nordea eksport", "Danske Bank CSV", "Jyske Bank eksport", "e-conomic bankimport". +version: 0.2.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, csv-import, bank, nordea, danske-bank, jyske-bank, e-conomic] + related: [kontoplan-mapping, coop-bankimport, momsafregning, e-conomic-integration] +--- + +# Dansk CSV-import (bankudtog) + +Håndterer import af **danske bank-CSV-eksporter** til Radix ERP med korrekt detektering af: +- Bank (Nordea, Danske Bank, Jyske Bank, Sydbank, Nykredit, Arbejdernes Landsbank) +- Datoformat (dansk `dd-MM-yyyy` eller ISO `yyyy-MM-dd`) +- Decimalformatering (komma som decimal — dansk standard) +- Feltsemikolon (dansk CSV-standard, ikke komma) +- Tegnsæt (Latin1, UTF-8 med/uden BOM) + +## Aktiveres ved + +- "Importér denne Nordea-CSV til Radix" +- "Jyske Bank har sendt kontoudtog — hjælp med at indlæse det" +- "Parse denne bankeksport" +- "Vis alle transaktioner fra marts 2026 i denne CSV" + +## Genkendte bankformater + +### Nordea (Erhverv) +``` +Bogføringsdato;Tekst;Beløb;Saldo;Valuta;Reference +01-06-2026;"Nets *12345";-123,45;12.345,67;DKK; +``` +- Separator: `;` +- Encoding: Latin1 +- Beløb: dansk format (`-123,45`) +- Dato: `dd-MM-yyyy` + +### Danske Bank +``` +Dato;Tekst;Beløb;Saldo +01-06-2026;"MobilePay - Anders Hansen";-250,00;5.000,00 +``` +- Separator: `;` +- Encoding: UTF-8 med BOM +- Beløb: dansk format +- Dato: `dd-MM-yyyy` + +### Jyske Bank +``` +Dato;Beskrivelse;Beløb;Saldo +01-06-2026;"POS DSB 1234 København";-89,00;3.456,78 +``` +- Separator: `;` +- Encoding: Latin1 +- Dato: `dd-MM-yyyy` +- Negative beløb: kan være `-89,00` eller `(89,00)` — begge understøttes + +### Sydbank +``` +Bogføringsdato;Posteringstekst;Beløb;Saldo +01-06-2026;"Coop Danmark A/S Bankbetaling";-12.345,67;45.678,90 +``` +- Separator: `;` +- Encoding: Latin1 +- Dato: `dd-MM-yyyy` + +### e-onomic bankimport-format +``` +Dato;Tekst;Beløb;Saldo;Modkonto;Valuta;Bankkonto +01-06-2026;"Nets *12345";-123,45;12.345,67;5820;DKK;5508 8765432 +``` +- Separator: `;` +- Encoding: UTF-8 med BOM +- **Modkonto** (e-conomics kontonr, fx 5820 for bank) — bruges direkte ved bogføring +- **Bankkonto** i dansk 14-cifret format (reg.nr. + kontonr) — verificer mod kundens egen bankaftale +- Dato: `dd-MM-yyyy` (men konverter til ISO `yyyy-MM-dd` før API-kald til e-conomic) + +## Workflow + +1. **Modtag CSV-fil** (enten via sti eller indhold direkte i chat) +2. **Detektér bank-format** ud fra header (kolonnenavne) — spørg brugeren hvis usikker +3. **Detektér encoding** (tjek for BOM, prøv Latin1, UTF-8) +4. **Parse CSV** med korrekt separator (`;` for dansk) +5. **Normaliser data**: + - Datoer → `dd-MM-yyyy` (ensret formattering) + - Beløb → Python float (erstat `,` med `.`, fjern tusindtalsseparator) + - Negativ beløb → negativ tal (fjern evt. parentesnotation) + - Tekst → trim, fjern dobbelte mellemrum +6. **Vis preview** med de første 5-10 transaktioner (bed brugeren bekræfte format) +7. **Vis statistik**: + - Antal transaktioner + - Periode (første/sidste dato) + - Total ind, total ud, slutbalance + - Kontroller at slutbalance matcher det brugeren forventede +8. **Identificér dubletter** (samme dato + beløb + tekst) +9. **Kortlæg konti** (se kontoplan-mapping skill) — eller lad brugeren vælge +10. **Generer klar-til-import JSON** til Radix ERP API + +## Eksempel-aktivering + +**Bruger:** *indsætter en filsti eller indhold:* +``` +Bogføringsdato;Tekst;Beløb;Saldo;Valuta +01-06-2026;"Nets *12345 MobilePay";-123,45;12.345,67;DKK +02-06-2026;"Coop Danmark";-5.678,90;6.666,77;DKK +... +``` + +**Svar:** +> ✅ Genkendt format: **Nordea Erhverv** (Latin1, semikolon-separeret) +> +> | Dato | Tekst | Beløb | Saldo | +> |---|---|---|---| +> | 01-06-2026 | Nets *12345 MobilePay | −123,45 | 12.345,67 | +> | 02-06-2026 | Coop Danmark | −5.678,90 | 6.666,77 | +> | ... | +> +> **Statistik:** +> - Periode: 01-06-2026 til 30-06-2026 +> - Transaktioner: 47 +> - Total ind: 8.901,23 kr. +> - Total ud: 23.456,78 kr. +> - Slutbalance: 6.666,77 kr. +> +> ⚠️ **1 mulig dublet** fundet (samme dato, beløb og tekst på række 12 og 23). Skal jeg springe dubletten over ved import? + +## Almindelige CSV-fælder + +- ❌ Latin1-filer åbnet som UTF-8 → `æ` i stedet for `æ` +- ❌ Datoer med `.` (amerikansk) i stedet for `-` (dansk) +- ❌ Beløb med `.` som tusindtalsseparator ignoreret +- ❌ Negative beløb som `(123,45)` (parentes) ikke håndteret +- ❌ Header-række fejlfortolket som data +- ❌ Linjeskift i `Tekst`-felt (fx "Coop\nDanmark") brækker parser +- ✅ ALTID detektér encoding først (BOM, Latin1, UTF-8) +- ✅ ALTID vis preview før import +- ✅ ALTID vis statistik og balancesum til kontrol + +## Radix ERP API-endpoint (for import) + +## Bogføringssystem-integration (efter import) + +Efter import kan transaktioner bogføres direkte i e-conomic (se `e-conomic-integration` skill): + +``` +POST /journals/{journalId}/vouchers/{voucherNumber}/entries +Headers: + X-AppSecretToken: ${ECONOMIC_APP_SECRET_TOKEN} + X-AgreementGrantToken: ${ECONOMIC_AGREEMENT_GRANT_TOKEN} + Idempotency-Key: +``` + +Eksempel-payload genereret fra importeret CSV: +```json +{ + "accountingYear": {"year": 2026}, + "entries": [ + { + "date": "2026-06-01", + "text": "Nets *12345 MobilePay", + "amount": -123.45, + "amountInBaseCurrency": -123.45, + "currency": "DKK", + "account": {"accountNumber": 1690}, + "contraAccount": {"accountNumber": 5820}, + "vatAccount": {"vatCode": "I25"} + } + ] +} +``` + +⚠️ **Bekræft ALTID med brugeren før bulk-import** — vis diff (antal transaktioner, totaler) og kræv eksplicit "ja, importer" før API-kald. diff --git a/skills/accounting/e-conomic-integration/SKILL.md b/skills/accounting/e-conomic-integration/SKILL.md new file mode 100644 index 0000000..1f1b4e8 --- /dev/null +++ b/skills/accounting/e-conomic-integration/SKILL.md @@ -0,0 +1,290 @@ +--- +name: e-conomic-integration +description: > + Integration med e-conomic cloud-bogføring — aktiveres ved "bogfør i e-conomic", "hent fakturaer + fra e-conomic", "kassekladde", "sync mod e-conomic", "opret kunde i e-conomic", "kontoudtog + e-conomic", "REST API", "AppSecretToken". Dækker auth, kontoplan, kassekladder, bogførte + fakturaer, kreditorer, kunder og banktransaktioner. +version: 0.1.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, e-conomic, dansk-regnskab, api, cloud-bogfoering, integration] + related: [resultatopgoerelse, momsafregning, kontoplan-mapping, coop-bankimport, dansk-csv-import] +--- + +# e-conomic-integration + +Integration med **e-conomic** cloud-bogføringssystem via den officielle REST API. Bruges til at hente fakturaer, kunder, kreditorer, bogføre bilag, synce banktransaktioner og afstemme konti — alt på dansk. + +## Aktiveres ved + +- "Bogfør denne faktura i e-conomic" +- "Hent åbne fakturaer fra e-conomic" +- "Synk banktransaktioner til e-conomic" +- "Opret kunde X i e-conomic" +- "Hvad er saldi på konto 1010 i e-conomic?" +- "Generer kassekladdeline for denne betaling" +- "Vis alle kreditorer med udestående i e-conomic" + +## API basics + +### Base-URL +``` +https://restapi.e-conomic.com +``` + +### Auth (kræver to tokens) + +| Header | Værdi | Hvad det er | +|---|---|---| +| `X-AppSecretToken` | `${ECONOMIC_APP_SECRET_TOKEN}` | App-identifikation (din app's hemmelige token) | +| `X-AgreementGrantToken` | `${ECONOMIC_AGREEMENT_GRANT_TOKEN}` | Aftale-specifik adgang (bruger-godkendt) | +| `Content-Type` | `application/json` | Altid JSON | +| `Idempotency-Key` | `` | Forhindrer dobbeltbogføring ved netværksfejl (POST/PUT) | + +⚠️ **Tokens skal ALDRIG hardcodes** — brug miljøvariabler eller en secret manager. + +### Demo-mode (sandbox) +Til test/udvikling, tilføj `?demo=true` til URL — alle writes deaktiveres (kun GET). + +## Vigtigste endpoints + +| Formål | Endpoint | HTTP | +|---|---|---| +| List konti (kontoplan) | `/accounts?pagesize=N` | GET | +| Hent én konto | `/accounts/:accountNumber` | GET | +| List bogføringsår | `/accounting-years` | GET | +| List perioder (måneder) i et år | `/accounting-years/:year/periods` | GET | +| Hent kontosaldo for en periode | `/accounts/:accountNumber/accounting-years/:year/periods/:period/totals` | GET | +| List kassekladder | `/journals` | GET | +| Hent kassekladdeline | `/journals/:id/vouchers?voucherNumber=:n` | GET | +| Opret kassekladdeline (bilag) | `/journals/:id/vouchers/:n/entries` | POST | +| Bogfør voucher | `/journals/:id/vouchers/:n/book` | POST | +| List bogførte fakturaer | `/invoices/booked` | GET | +| Opret bogført faktura | `/invoices/booked` | POST | +| Hent kunde | `/customers/:customerNumber` | GET | +| Opret kunde | `/customers` | POST | +| List kreditorer | `/suppliers` | GET | +| Opret kreditor | `/suppliers` | POST | +| List moms-konti | `/vat-accounts` | GET | +| Hent valutakurser | `/currencies` | GET | + +⚠️ **Rate limit**: e-conomic har et fair-use loft. Hold dig under 10 requests/sekund. + +## e-conomics standardkontoplan (4-cifret) + +| Kontointerval | Type | Bruges til | +|---|---|---| +| 1000-1099 | Heading/profitAndLoss | Indtægter, overskrifter | +| 1100-1199 | ProfitAndLoss | Omsætning undergrupper (varekategorier) | +| 1200-1299 | ProfitAndLoss | Diverse indtægter | +| 1300-1399 | ProfitAndLoss | Produktionsomkostninger (COGS) | +| 1500-1599 | ProfitAndLoss | Lokaleomkostninger | +| 1600-1699 | ProfitAndLoss | Salgsomkostninger | +| 1700-1799 | ProfitAndLoss | Administrationsomkostninger | +| 1800-1899 | ProfitAndLoss | Personaleomkostninger (løn) | +| 2000-2099 | ProfitAndLoss | Andre driftsomkostninger | +| 2100-2199 | ProfitAndLoss | Afskrivninger | +| 2200-2299 | ProfitAndLoss | Finansielle indtægter | +| 2300-2399 | ProfitAndLoss | Finansielle udgifter | +| **5500-5899** | **Balance** | **Bank, kasse, tilgodehavender** | +| 5800-5899 | Balance | Bankkonti (typisk 5820 = drift) | +| 6000-6899 | Balance | Leverandørgæld, moms, A-skat | +| 6500-6599 | Balance | Moms (6500 = indgående, 6600 = udgående) | +| 6900-6999 | Balance | Diverse statuskonti | + +⚠️ **Dette er e-conomics STANDARD kontoplan** — ikke alle kunder har den. Hent altid kundens egen kontoplan via `/accounts` endpoint. + +## Momskoder (VAT-accounts) + +| Kode | Type | Sats | Bruges til | +|---|---|---|---| +| `U25` | Udgående | 25% | Salg af varer/ydelser i DK (momspligtig) | +| `I25` | Indgående | 25% | Køb af varer/ydelser i DK (momsfradrag) | +| `U0` | Udgående | 0% | Eksport, EU-salg med VIES, moms-fritagne ydelser | +| `I0` | Indgående | 0% | Køb uden moms (import under bagatelgrænse) | +| `UEU25` | Udgående | 25% | EU-varesalg (vareleverance til andet EU-land) | +| `UV25` | Udgående | 25% | Byggeydelser i andre EU-lande (omvendt betalingspligt) | + +⚠️ Korrekt momskode er afgørende for momsangivelsen — forkert kode kan give skattesmæk. + +## Kassekladder (Journals) + +| Journal-ID | Navn (typisk) | Bruges til | +|---|---|---| +| 1 | Daglig / Kassekladde | Almindelige bilag (køb, salg, bank) | +| 2 | Indbetalinger | Kundeindbetalinger (kontra-konto typisk 5820 bank) | +| 3 | Lønninger | Lønbogføring (entryType `financeVoucher`) | +| 4 | Moms | Momsafregning | +| 5 | Afskrivninger | Anlægsaktiver | + +Journal-ID'er varierer fra kunde til kunde — hent altid `/journals` først. + +## Workflow: Bogfør et bilag (kassekladdeline) + +### Eksempel: bogfør kontantkøb af kontorartikler for 1.250,00 kr. inkl. moms (15-06-2026) + +**Trin 1: Hent kunde-/kassekladde-info** +``` +GET /journals +→ Find journal-ID for "Daglig" (typisk 1) +GET /journals/1/vouchers?voucherNumber=42 +→ Find næste ledige bilagsnummer +``` + +**Trin 2: Opret kassekladdeline (visuelt, FØR API-kald)** +```json +{ + "accountingYear": { + "year": 2026 + }, + "entries": [ + { + "customerInvoiceNumber": "", + "date": "2026-06-15", + "voucherNumber": 42, + "text": "Kontorartikler — Kontorbutikken A/S", + "amount": -1250.00, + "amountInBaseCurrency": -1250.00, + "currency": "DKK", + "account": { + "accountNumber": 1690 + }, + "contraAccount": { + "accountNumber": 5820 + }, + "vatAccount": { + "vatCode": "I25" + } + } + ] +} +``` + +**Trin 3: Vis brugeren og bekræft** +``` +⚠️ Klar til at bogføre: + Kassekladde: Daglig (#1) + Bilagsnr: 42 + Dato: 15-06-2026 + Tekst: Kontorartikler — Kontorbutikken A/S + Debet: 1690 Kontorartikler 1.250,00 kr. + Kredit: 5820 Bank, drift −1.250,00 kr. + Momskode: I25 (25% indgående) + Total: 1.250,00 kr. (inkl. 250,00 kr. moms) + +Vil du bogføre? (ja/nej) +``` + +**Trin 4: POST med Idempotency-Key** +``` +POST /journals/1/vouchers/42/entries +Headers: + X-AppSecretToken: ${ECONOMIC_APP_SECRET_TOKEN} + X-AgreementGrantToken: ${ECONOMIC_AGREEMENT_GRANT_TOKEN} + Content-Type: application/json + Idempotency-Key: + +Body: [ovenstående entries-array] +``` + +⚠️ **Bogfør IKKE før brugeren har bekræftet.** + +## Workflow: Synk banktransaktioner + +1. **Hent konti** for bank (`/accounts?filter=accountType$eq:balance&pagesize=100`) — find bank-konto (typisk 5820) +2. **Hent kontosaldo** for indeværende periode: + ``` + GET /accounts/5820/accounting-years/2026/periods/6/totals + ``` +3. **Sammenlign** med bankens faktiske saldo (fra CSV-import eller bank-API) +4. **Vis diff** og identificer manglende/ukorrekte transaktioner +5. **Foreslå kassekladdeline** for hver manglende transaktion + +## Workflow: Hent åbne fakturaer (kunder) + +``` +GET /invoices/booked?filter=balance$gt:0&pagesize=50 +``` + +Returnerer alle bogførte fakturaer med ubetalt saldo > 0. + +For hver faktura: +- `bookedInvoiceNumber` — fakturanr +- `customer.name` — kundenavn +- `netAmount` / `grossAmount` — beløb ekskl./inkl. moms +- `balance` — udestående +- `dueDate` — forfaldsdato (udledes af `termsNetDays` + `date`) +- `currency` — DKK/EUR/USD + +Bruges typisk til: +- Inkasso-rapporter (forfaldne >30 dage) +- Aldersopdelt fordringsliste (0-30, 31-60, 61-90, 90+) +- Likviditetsforecast + +## Workflow: Opret kunde + +```json +POST /customers +{ + "name": "Coop Danmark A/S", + "customerGroup": { "customerGroupNumber": 1 }, + "paymentTerms": { "paymentTermsNumber": 1 }, + "vatZone": { "vatZoneNumber": 1 }, + "currency": "DKK", + "address": "Roskildevej 65", + "zip": "2620", + "city": "Albertslund", + "country": "Denmark" +} +``` + +⚠️ **Tjek altid for duplikat** (søg eksisterende kunder først): +``` +GET /customers?filter=name$like:Coop$Danmark +``` + +## Almindelige fejl + +- ❌ **Glemmer Idempotency-Key** ved POST → netværksfejl kan give dobbeltbogføring +- ❌ **Forkert momskode** (U25 vs UEU25) → SKAT fanger det i krydstjek +- ❌ **Brug danske datoer i API** — e-conomic bruger ISO `yyyy-MM-dd` i JSON, IKKE dansk `dd-MM-yyyy` +- ❌ **Beløb uden fortegn-konvention** — negativ = debet, positiv = kredit (modsat dansk regnskabstradition) +- ❌ **PageSize > 1000** uden pagination +- ❌ **POST uden først at have hentet den aktuelle voucher** → 409 Conflict +- ✅ ALTID brug Idempotency-Key ved alle writes +- ✅ ALTID konverter dansk dd-MM-yyyy → ISO yyyy-MM-dd FØR API-kald +- ✅ ALTID verificer kontoplanen (`/accounts`) før bogføring — kunden kan have egne konti +- ✅ ALTID tjek eksisterende kunder/leverandører før oprettelse (dublet-forebyggelse) +- ✅ ALTID vis bogføringsforslag til brugeren FØR API-kald + +## Demo-mode anbefaling + +Brug ALTID `?demo=true` under udvikling: +- Læs-data virker (GET) +- Skrive-data deaktiveret (POST/PUT returnerer 405) +- Forhindrer utilsigtet bogføring i produktion +- Kan derfor frit browse konti, fakturaer, kunder uden risiko + +## Eksempel på aktivering + +**Bruger:** "Bogfør et køb af kontorartikler for 1.250 kr. inkl. moms i e-conomic" + +**Svar (forkortet):** +> ✅ Forbinder e-conomic... +> +> Klar til bogføring: +> +> | Felt | Værdi | +> |---|---| +> | Kassekladde | Daglig (#1) | +> | Bilagsnr | 42 | +> | Dato | 15-06-2026 | +> | Tekst | Kontorartikler — Kontorbutikken A/S | +> | Debet (1690 Kontorartikler) | 1.250,00 kr. | +> | Kredit (5820 Bank) | −1.250,00 kr. | +> | Momskode | I25 (indgående 25%) | +> +> ⚠️ Bekræft før jeg bogfører. Svaret "ja, bogfør" udløser API-kald med Idempotency-Key. diff --git a/skills/accounting/kontoplan-mapping/SKILL.md b/skills/accounting/kontoplan-mapping/SKILL.md new file mode 100644 index 0000000..c6ec7f7 --- /dev/null +++ b/skills/accounting/kontoplan-mapping/SKILL.md @@ -0,0 +1,183 @@ +--- +name: kontoplan-mapping +description: > + Kortlæg transaktioner, fakturaer og bilag til dansk standardkontoplan (SUM, DG, SL) eller + bogføringssystem-specifik kontoplan (e-conomic 4-cifret, Uniconta) — aktiveres ved + "hvilken konto?", "kontoplan", "kontorådgivning", "bogføring af [transaktionstype]", + "hvor skal jeg bogføre dette". +version: 0.2.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, kontoplan, SUM, DG, SL, uniconta, e-conomic] + related: [resultatopgoerelse, momsafregning, dansk-csv-import, e-conomic-integration] +--- + +# Kontoplan-mapping + +Mapper transaktioner og bilag til **korrekt konto i dansk standardkontoplan** (SUM, DG, SL). Bruges som hjælpe-skill af andre skills (resultatopgoerelse, momsafregning, dansk-csv-import, coop-bankimport). + +## Aktiveres ved + +- "Hvilken konto skal jeg bogføre et kontorlejemål på?" +- "Hvor hører MobilePay-gebyr hjemme i kontoplanen?" +- "Lav en kontoplan for min [virksomhedstype]" +- "Tjek om denne transaktion er konteret korrekt" + +## Dansk standardkontoplan — overblik + +### Driftskonti (resultatopgørelse) + +| Hovedgruppe | Kontointerval | Bruges til | +|---|---|---| +| Omsætning | 1000-1999 | Salg, service, licens, abonnement, eksport | +| Produktionsomkostninger | 2000-2999 | Råvarer, direkte løn, underleverandører, fragt | +| Salgsomkostninger | 3000-3999 | Marketing, salgsfremmende, rejser, repræsentation | +| Administrationsomkostninger | 4000-4999 | Kontor, IT, telefon, kontorartikler, abonnementer | +| Lokaleomkostninger | 5000-5999 | Husleje, varme, el, vand, renovation, rengøring | +| Personaleomkostninger | 6000-6999 | Løn, pension, ATP, feriepenge, AER, sociale bidrag | +| Afskrivninger | 7000-7999 | Lineære afskrivninger, scrap, salg af aktiver | +| Finansielle poster | 8000-8999 | Renter, gebyrer, kursregulering, realiserede tab/gevinst | +| Ekstraordinære poster | 9000-9999 | Ekstraordinære indtægter/udgifter (sjældent) | + +### Statuskonti (balance) + +| Hovedgruppe | Kontointerval | Bruges til | +|---|---|---| +| Aktiver | 10000-19999 | Tilgodehavender, varelager, anlægsaktiver, bank, kasse | +| Passiver | 20000-29999 | Leverandørgæld, moms, A-skat, skyldig løn, feriepengeforpligtelse | +| Egenkapital | 30000-39999 | Ansvarlig kapital, overført resultat, årets resultat | + +## e-conomic standardkontoplan (4-cifret, mest udbredt) + +e-conomic leveres med en standard dansk kontoplan. **De fleste danske SMV'er bruger denne uden ændringer.** + +### Driftskonti (resultatopgørelse) + +| Kontointerval | Bruges til | +|---|---| +| 1000-1099 | Indtægter (heading + omsætning) | +| 1100-1299 | Diverse omsætning og indtægter | +| 1300-1499 | Produktionsomkostninger / varekøb / direkte løn | +| 1500-1599 | Lokaleomkostninger (husleje, el, varme) | +| 1600-1699 | Salgs- og rejseomkostninger (marketing, transport) | +| 1700-1799 | Administrationsomkostninger (kontor, IT, telefon) | +| 1800-1899 | Personaleomkostninger (løn, pension, AER) | +| 1900-2099 | Andre driftsomkostninger, afskrivninger | +| 2100-2299 | Finansielle poster (renter, gebyrer) | +| 2300-2999 | Ekstraordinære poster | + +### Statuskonti (balance) + +| Kontointerval | Bruges til | +|---|---| +| 5500-5899 | Aktiver — bank, kasse, tilgodehavender (5820 = typisk driftsbank) | +| 6000-6899 | Passiver — leverandørgæld, moms, A-skat, skyldig løn | +| 6500-6599 | Moms (udgående/indgående/afregning) | +| 6900-6999 | Egenkapital, diverse status | + +### e-conomic standardkonti — udvalgte + +| Kontonr | Navn | Bruges til | +|---|---|---| +| 1010 | Salg af varer/ydelser m/moms | Standard DK-salg (25% moms) | +| 1080 | Salg af varer/ydelser u/moms | Eksport, EU-salg (0% moms) | +| 1300 | Varekøb | Råvarer til videresalg | +| 1500 | Husleje | Kontorlejemål | +| 1510 | El | Elforbrug | +| 1520 | Varme | Opvarmning | +| 1530 | Vand | Vandforbrug | +| 1610 | Rejseudgifter | Transport, hotel, fly | +| 1690 | Kontorartikler | Småanskaffelser, kontorartikler | +| 1700 | Telefon & internet | Mobil, fastnet, internet | +| 1730 | Software / IT-licenser | SaaS-abonnementer, licenser | +| 1800 | Bruttoløn | Lønudbetaling | +| 1810 | Pension | Pensionsbidrag | +| 1820 | ATP | ATP-bidrag | +| 1900 | AER | AER-bidrag | +| 1910 | Feriepenge | Feriepengeforpligtelse | +| 2200 | Bankgebyrer | Gebyrer, renter | +| 2210 | Renteudgifter | Renter på lån | +| 5500 | Kasse | Kontant beholdning | +| 5508 | Bank, drift | Primær driftskonto (typisk) | +| 5820 | Bank, kassekladde | Standard bank-modkonto | +| 6500 | Udgående moms | 25% moms på salg | +| 6510 | Indgående moms | 25% moms på køb | +| 6520 | Momsafregning | Saldo (kredit = skyldig, debet = tilgodehavende) | + +⚠️ **Tjek altid kundens egen kontoplan** via `GET /accounts` (e-conomic) eller Uniconta-eksport — der kan være afvigelser. + +## Almindelige transaktioner — hurtig-mapping (alle systemer) + +| Transaktion | SUM/DG | e-conomic | Type | +|---|---|---|---| +| Salg af vare (DK) | 1010 | 1010 | Driftsindtægt | +| Salg af ydelse (DK) | 1020 | 1010 | Driftsindtægt | +| Eksport (0% moms) | 1030 | 1080 | Driftsindtægt | +| Varekøb (DK) | 2010 | 1300 | Driftsomkostning | +| Kontorartikler | 4010 | 1690 | Driftsomkostning | +| Software-abonnement | 4030 | 1730 | Driftsomkostning | +| Hosting / cloud | 4030 | 1730 | Driftsomkostning | +| Husleje | 5010 | 1500 | Driftsomkostning | +| El, vand, varme | 5020-5040 | 1510-1530 | Driftsomkostning | +| Internet, telefon | 5050 | 1700 | Driftsomkostning | +| MobilePay-gebyr | 8010 | 2200 | Driftsomkostning (finansiel) | +| Stripe-gebyr | 8010 | 2200 | Driftsomkostning (finansiel) | +| Bankgebyr | 8020 | 2200 | Driftsomkostning (finansiel) | +| Renteindtægt | 8030 | 2210 (neg.) | Driftsindtægt (finansiel) | +| Renteudgift | 8040 | 2210 | Driftsomkostning (finansiel) | +| Løn til ansat | 6010 | 1800 | Personaleomkostning | +| A-skat (trukket fra løn) | 2090 | 6810 | Forpligtelse | +| Pension (arbejdsgivers andel) | 6020 | 1810 | Personaleomkostning | +| Feriepenge (reserveret) | 6015 | 1910 | Personaleomkostning | +| Moms, udgående | 2060 | 6500 | Forpligtelse | +| Moms, indgående | 2070 | 6510 | Tilgodehavende (modregnes) | +| Moms, afregning | 2080 | 6520 | Forpligtelse/Tilgodehavende | +| Bank (drift) | 5810 | 5508 / 5820 | Aktiv | + +⚠️ **Tallene er eksempler iht. SUM-standarden** (Standard for virksomheders økonomistyring). Mange SMV'er bruger deres egen tilpassede kontoplan — **bekræft altid med virksomhedens egen kontoplan** inden bogføring. + +## Workflow + +1. **Modtag transaktionstype** (eller specifik transaktion) +2. **Find relevant kontointerval** (se tabellerne ovenfor) +3. **Anvend kontekst-regler**: + - Virksomhedstype (enkeltmands, ApS, A/S, forening) → kan have egne konti + - Branche (detail, engros, service, IT, produktion) → brancherelaterede konti + - Skatteforhold (momsregistrering, CVR, SElvangivelse) → særlige konti +4. **Vis anbefaling med begrundelse**: + - Kontonr, kontonavn, kontogruppe + - Hvorfor denne konto og ikke en alternativ +5. **Alternativer**: vis 1-2 alternative konti hvis der er rimelig tvivl +6. **Bekræft med bruger** — kontoplan-valg er en menneskelig beslutning, ikke en agent-beslutning + +## Eksempel-aktivering + +**Bruger:** "Hvor skal jeg bogføre et Stripe-gebyr på 234,56 kr.?" + +**Svar:** +> **Anbefalet konto:** 8010 — Gebyrer, betalingsformidlere +> +> **Begrundelse:** Stripe-gebyrer er **finansielle omkostninger** (betalingsformidling), ikke driftsomkostninger. De hører under hovedgruppe 8 (Finansielle poster) og bogføres typisk sammen med bankgebyrer og renter. +> +> **Bogføring:** +> ``` +> DEBET 8010 Gebyrer, betalingsformidlere 234,56 +> KREDIT 5810 Bank, driftskonto 234,56 +> ``` +> +> **Alternativer:** +> - 8030 (Renteudgifter) — hvis virksomheden samler alle gebyrer dér (mindre korrekt) +> - 4025 (Salgsomkostninger, eksterne) — hvis virksomheden klassificerer alle salgsrelaterede omkostninger under salg +> +> ⚠️ *Dette er ikke bogføringsrådgivning. Kontakt din revisor for endelig kontoplan.* + +## Særlige regler + +- ❌ ALDRIG foreslå en konto uden begrundelse +- ❌ ALDRIG antag at kontoplanen følger SUM-standarden — bekræft med brugeren +- ❌ ALDRIG modificér kontoplanen uden advarsel og dobbeltgodkendelse +- ✅ ALTID vis kontogruppe (drift/balance) og sektion (omsætning/omkostning/aktiv/passiv) +- ✅ ALTID vis alternativer når der er rimelig tvivl +- ✅ ALTID medtag "Dette er ikke bogføringsrådgivning" disclaimer diff --git a/skills/accounting/momsafregning/SKILL.md b/skills/accounting/momsafregning/SKILL.md new file mode 100644 index 0000000..44b5381 --- /dev/null +++ b/skills/accounting/momsafregning/SKILL.md @@ -0,0 +1,161 @@ +--- +name: momsafregning +description: > + Beregn dansk momsangivelse (moms, momsfradrag, momsrefusion) til SKAT — aktiveres ved + forespørgsler om "moms", "momsangivelse", "momsrefusion", "kvartalsmoms", "halvårsmoms", + "momsregnskab", "25% moms", "momsfri". +version: 0.1.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, moms, skat, momsangivelse] + related: [resultatopgoerelse, kontoplan-mapping, dansk-csv-import] +--- + +# Dansk Momsafregning + +Forbereder **momsangivelse** klar til indberetning via SKAT's TastSelv Erhverv (eller skat.dk). Håndterer både **kvartalsmoms** (små/mellemstore virksomheder) og **halvårsmoms** (helårsmomsansøgere, mikrovirksomheder under 5 mio. kr. i omsætning). + +## Aktiveres ved + +- "Beregn moms for Q2 2026" +- "Forbered momsangivelse for maj 2026" +- "Hvad er min momsrefusion for hele 2025?" +- "Hvornår skal jeg indberette moms næste gang?" + +## Danske momssatser (gældende) + +| Sats | Bruges til | +|---|---| +| **25%** | Standard (langt de fleste varer og ydelser i Danmark) | +| **0%** | Eksport ud af EU, visse avis/abonnementer, kunstnerisk virksomhed under 300.000 kr. | +| Fritaget | Sygehusbehandling, undervisning, finansielle ydelser, forsikring | + +⚠️ **Bemærk**: EU-lande har forskellige satser, men denne skill håndterer **kun dansk moms**. For EU-handel brug særskilt sub-skill (ikke del af denne version). + +## Kontogruppering (dansk standardkontoplan + e-conomic) + +| Konto (SUM) | Konto (e-conomic) | Navn | Beløb i momsangivelsen | +|---|---|---|---| +| 1100-1199 | 1010 | Salg af varer/ydelser (DK, 25%) | Rubrik A: Varesalg (momspligtig) | +| 1200-1299 | 1010 | Salg af ydelser (DK, 25%) | Rubrik A: Ydelsessalg (momspligtig) | +| 1500-1599 | 1080 | Eksport (0% moms) | Rubrik A: EU-salg/eksport | +| 1600-1699 | 1300 | Råvarer/varekøb (DK, 25%) | Rubrik B: Varekøb (momsfradrag) | +| 1700-1799 | 1690 / 1730 | Driftsomkostninger (DK, 25%) | Rubrik B: Driftsomkostninger (momsfradrag) | +| 1800-1899 | 1800 | Import (køb fra udland) | Rubrik B: Import | +| 1900-1999 | — | Køb af ydelser uden moms | (ingen moms at fradrage) | +| 2060 | 6500 | Udgående moms | (salgsmoms) | +| 2070 | 6510 | Indgående moms | (momsfradrag) | +| 2080 | 6520 | Momsafregning | (saldo) | + +⚠️ **Tjek altid kundens egen kontoplan** — de faktiske kontonumre kan variere. Ovenstående er standard-reference for både SUM (dansk standardkontoplan) og e-conomic (4-cifret). + +## Struktur (momsangivelse, kvartal) + +``` +MOMSANGIVELSE — [Kvartal] [År] +═══════════════════════════════════════════════════ +Periode: [dd-MM-yyyy] til [dd-MM-yyyy] +CVR: [hentes fra Radix] +═══════════════════════════════════════════════════ + +RUBRIK A — UDGÅENDE MOMS (moms af salg) +─────────────────────────────────────────────────── +A1 Varesalg, momspligtig (25%) 1.234.567,89 +A2 Ydelsessalg, momspligtig (25%) 89.012,34 +A3 Varesalg, EU/eksport (0%) 0,00 +A4 Ydelsessalg, EU (0%) 0,00 + ... + Udgående moms i alt (25% af A1+A2) 330.895,06 +─────────────────────────────────────────────────── + +RUBRIK B — INDGÅENDE MOMS (moms på køb, kan fradrages) +─────────────────────────────────────────────────── +B1 Varekøb, momsfradrag (25%) 450.123,45 +B2 Driftsomkostninger, momsfradrag (25%) 67.890,12 +B3 Import, betalt toldmoms 0,00 + ... + Indgående moms i alt (25% af B1+B2) 129.503,39 +─────────────────────────────────────────────────── + +RUBRIK C — SALGSMOMS (MOMS DER SKAL INDBETALES) +═══════════════════════════════════════════════════ + Udgående moms 330.895,06 + Indgående moms −129.503,39 +─────────────────────────────────────────────────── + SALGSMOMS 201.391,67 +═══════════════════════════════════════════════════ + + Hvis positiv: SKAT skylder dig penge + Hvis negativ: Du skylder SKAT penge +─────────────────────────────────────────────────── + +INDBETALINGSFRIST +─────────────────────────────────────────────────── + Kvartalsmoms: 1. i 3. måned efter kvartalets udløb + Eksempel: Q1 (jan-mar) → frist 1. juni +═══════════════════════════════════════════════════ +``` + +## Workflow + +1. **Spørg om periode** (måned, kvartal, halvår) +2. **Bekræft virksomhedstype**: lille/mellemstor (kvartal) eller helårsmomsansøger (halvår) +3. **Hent salg og køb** fra Radix ERP for perioden +4. **Adskil konti** efter momssats (25%, 0%, fritaget) +5. **Beregn udgående moms** (moms af eget salg) og **indgående moms** (moms af køb) +6. **Beregn salgsmoms** = udgående moms − indgående moms +7. **Vis beløb at indberette** (positiv = SKAT skylder dig, negativ = du skylder SKAT) +8. **Angiv indberetningsfrist** iht. SKAT's regler +9. **Tilbyd at generere SKAT XML/CSV** (hvis brugeren vil indberette direkte) + +## Reverse charge (vigtig!) + +For køb af ydelser fra udlandet (fx en tysk konsulent): + +- **Køber** skal selv beregne og indberette momsen (reverse charge) +- Konti der er reverse charge: typisk 1900-1999 (køb af ydelser uden moms i købsprisen) +- I momsangivelsen: medtag i **Rubrik A** (både salg OG køb) og **Rubrik B** som fradrag — nettoeffekten er nul, men SKAT kræver begge beløb indberettet + +## Almindelige fejl + +- ❌ Medtag IKKE lønudgifter i momsbasis (løn er momsfri i DK) +- ❌ Medtag IKKE renter i momsbasis (finansielle ydelser er fritaget) +- ❌ Glem IKKE reverse charge-beløb — SKAT krydstjekker +- ❌ Brug IKKE EUR-sats for DK-virksomheder — 25% er dansk standardsats +- ✅ ALTID angiv CVR-nummer i rapporten +- ✅ ALTID vis "Dette er ikke skatterådgivning — kontakt din revisor/SKAT" på momsangivelser + +## SKAT-frister (2026) + +| Type | Periode | Frist | +|---|---|---| +| Kvartalsmoms | Q1 (jan-mar) | 1. juni | +| Kvartalsmoms | Q2 (apr-jun) | 1. september | +| Kvartalsmoms | Q3 (jul-sep) | 1. december | +| Kvartalsmoms | Q4 (okt-dec) | 1. marts (næste år) | +| Halvårsmoms | H1 (jan-jun) | 1. september | +| Halvårsmoms | H2 (jul-dec) | 1. marts (næste år) | +| Moms, mikrovirksomhed (<5 mio. kr.) | Helår (jan-dec) | 1. marts (næste år, men kun hvis ansøgt om helårsmoms) | + +⚠️ Frister kan ændre sig — tjek altid skat.dk for aktuelle frister. + +## Eksempel på aktivering + +**Bruger:** "Forbered momsangivelse for Q2 2026" + +**Svar (forkortet):** +> Momsangivelse for perioden 01-04-2026 til 30-06-2026: +> +> | Rubrik | Beløb | Moms | +> |---|---|---| +> | A1 Varesalg | 234.567,89 | 58.641,97 | +> | A2 Ydelsessalg | 12.345,67 | 3.086,42 | +> | B1 Varekøb | 89.012,34 | 22.253,09 | +> | B2 Driftsomk. | 23.456,78 | 5.864,20 | +> | **Salgsmoms** | | **33.611,10** | +> +> **Frist:** 01-09-2026 (1. september 2026) +> +> *Dette er ikke skatterådgivning. Kontakt din revisor eller SKAT for endelig indberetning.* diff --git a/skills/accounting/radix-accounting/SKILL.md b/skills/accounting/radix-accounting/SKILL.md new file mode 100644 index 0000000..5470e0b --- /dev/null +++ b/skills/accounting/radix-accounting/SKILL.md @@ -0,0 +1,104 @@ +--- +name: radix-accountant +description: > + Dansk bogføring, regnskab og ERP-assistance til Radix-portalen — aktiveres ved forespørgsler om + resultatopgørelse, momsafregning, kontoplan, CSV-import, bankafstemning, Uniconta-integration, + e-conomic-integration eller Coop Danmark-reconciliation. Svar altid på dansk, brug dansk + talformatering (1.234.567,89) og dd-MM-yyyy datoformat. +version: 0.2.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, moms, radix-erp, uniconta, e-conomic, coop] + related: [resultatopgoerelse, momsafregning, kontoplan-mapping, dansk-csv-import, coop-bankimport, e-conomic-integration] +--- + +# radix-accountant + +Dansk bogførings- og regnskabsassistent skræddersyet til **Radix ERP** (CRM/ERP kørt på portal.radixadm.dk) med integration mod **Uniconta** (lokalt), **e-conomic** (cloud-bogføring) og **Coop Danmark**-afstemningsflow. + +## Aktiveres ved + +Når brugeren skriver på dansk om: + +- **Resultatopgørelse, P&L, årsregnskab, balance, indtægter/udgifter** → `resultatopgoerelse` +- **Moms, momsangivelse, momsfradrag, momsrefusion, kvartals-/halvårsmoms, 25% / 0%** → `momsafregning` +- **CSV-import, bank-eksport, kontoudtog, Nordea/Danske Bank/Jyske/Sydbank-format** → `dansk-csv-import` +- **Coop Danmark, bankbetaling, opgørelse, afstemning mod fakturaer** → `coop-bankimport` +- **Kontoplan, kontonummer, dansk standardkontoplan, finanskonto, postering** → `kontoplan-mapping` +- **e-conomic, bogfør i e-conomic, hent fakturaer fra e-conomic, kassekladde, sync mod e-conomic** → `e-conomic-integration` + +## Faste konventioner (BINDENDE for alle sub-skills) + +| Parameter | Værdi | +|---|---| +| Sprog | Dansk (alle svar, prompts, fejlmeddelelser) | +| Talformatering | `1.234.567,89` — punktum som tusindtalsseparator, komma som decimal | +| Negativ beløb | `−1.234,56` med unicode minus-tegn (Unicode minus U+2212), ALDRIG `-` | +| Datoformat | `dd-MM-yyyy` (fx `15-06-2026`) | +| Klokkeslæt | `HH:mm` 24-timers (fx `14:35`) | +| Valuta | `kr.` eller `DKK` — aldrig `$` medmindre brugeren eksplicit beder om anden valuta | +| Moms | `25%` (standard), `0%` (fritagne ydelser), `ekskl. moms` / `inkl. moms` | +| Kontoplan reference | **e-conomic**: 4-cifret DK-standard (1000-9999). **Uniconta**: egen struktur. **SUM/DG/SL**: generisk dansk standard | +| Decimaler i rapporter | 2 decimaler for beløb, 1 decimal for procenter, 0 decimaler for heltal | +| Afrundingsregel | Banker's rounding (half-even) for at matche dansk bogføringspraksis | +| Bank reference | BIC + 10-cifret kontonr (dansk format) eller bank-reference fra CSV | + +## Reference-miljø + +- **Radix ERP**: kører på `https://portal.radixadm.dk` (DEBUG=True bevidst, dansk datoformat bevidst) +- **Test-login**: `testadmin@example.dk` / `test1234` (KUN til test/udvikling — ALDRIG persistér i rapporter) +- **Uniconta**: lokalt bogføringssystem, fakturaer og kreditorer hentes derfra +- **e-conomic**: cloud-bogføring (https://www.e-conomic.dk), REST API på `https://restapi.e-conomic.com`, OAuth-baseret adgang via AppSecretToken + AgreementGrantToken +- **Coop Danmark**: bankbetalinger + specifikationer (opgørelser) per indbetaling + +## Bogføringssystem — hvornår bruges hvad + +| System | Bruges til | Fordel | Ulempe | +|---|---|---|---| +| **Uniconta** | Dansk lokalt bogføringssystem, mange SMV'er | Lokal kontrol, dyb funktionalitet | Kræver installation | +| **e-conomic** | Cloud-bogføring, Danmarks mest udbredte | Altid tilgængeligt, stærk API, gratis for små | Begrænset fleksibilitet i kontoplan | +| **Radix ERP (egen)** | Intern ERP, CRM-delen | Integreret med Radix-data | Bogføring er ikke primær funktion | + +**Standardantagelse**: Hvis brugeren ikke specificerer, brug **e-conomic** (mest udbredt i danske SMV'er og har den bedste API). Spørg hvis det er uklart. + +## Workflow + +1. **Identificér brugerens hensigt** ud fra nøgleord (se listen ovenfor) +2. **Aktivér den relevante sub-skill** ved at indlæse dennes `SKILL.md` først +3. **Anvend de faste konventioner** i alle output (sprog, tal, dato, valuta) +4. **Bekræft data med brugeren** før du skriver noget tilbage til Radix/Uniconta/e-conomic (aldrig stille skriv) +5. **Generer rapporten** med tydelig header, periode, totaler og fodnoter +6. **Vis altid kildereference** (transaktions-id, fakturanummer, bankreference) i rapporter + +## Vigtige regler + +- ❌ ALDRIG bogfør uden brugerens eksplicitte godkendelse — vis diff først +- ❌ ALDRIG antag valuta — bekræft `DKK`/`EUR`/`USD` hvis ikke tydeligt +- ❌ ALDRIG brug amerikansk/UK-formatering (`,` som tusindtalsseparator) — det er den forkerte vej rundt for dansk +- ❌ ALDRIG modificér kontoplanen uden advarsel — ændringer kræver dobbeltgodkendelse +- ❌ ALDRIG hardcode API-tokens i eksempler — brug altid `${ECONOMIC_APP_SECRET_TOKEN}` placeholder-syntaks +- ❌ ALDRIG bogfør bilag i e-conomic uden først at vise `entries` JSON til brugeren +- ✅ ALTID vis "Dette er ikke bogføringsrådgivning — kontakt din revisor" på alle moms- og årsregnskabsrapporter +- ✅ ALTID angiv kilde (bankfil, Uniconta-id, e-conomic-bogføringsnr, faktura-nr) for hvert tal +- ✅ ALTID spørg før du sletter — brug aldrig `DELETE` uden bekræftelse +- ✅ ALTID brug Idempotency-Key ved POST/PUT til e-conomic API (forhindrer dobbeltbogføring ved netværksfejl) + +## Sub-skills + +| Skill | Formål | +|---|---| +| [resultatopgoerelse](resultatopgoerelse/SKILL.md) | Byg dansk P&L/resultatopgørelse med korrekt kontogruppering | +| [momsafregning](momsafregning/SKILL.md) | Beregn momsangivelse (kvartal/halvår), klar til SKAT-indberetning | +| [kontoplan-mapping](kontoplan-mapping/SKILL.md) | Map transaktioner til dansk standardkontoplan (SUM/DG/SL/e-conomic) | +| [dansk-csv-import](dansk-csv-import/SKILL.md) | Importér danske bank-CSV'er (Nordea, Danske Bank, Jyske, Sydbank) | +| [coop-bankimport](coop-bankimport/SKILL.md) | Reconcilier Coop Danmark bankbetalinger mod Uniconta- eller e-conomic-fakturaer | +| [e-conomic-integration](e-conomic-integration/SKILL.md) | Integration med e-conomic cloud-bogføring (auth, konti, kassekladde, fakturaer) | + +## Reference-dokumenter + +- `REFERENCES/dansk_kontoplan.md` — Oversigt over SUM/DG/SL-konti (kort) +- `REFERENCES/momssatser.md` — Aktuelle danske momssatser og fritagelser +- `REFERENCES/coop_format.md` — Forventet format på Coop opgørelser (PDF/Excel) +- `REFERENCES/economic_api.md` — e-conomic REST API endpoints og auth-flow diff --git a/skills/accounting/resultatopgoerelse/SKILL.md b/skills/accounting/resultatopgoerelse/SKILL.md new file mode 100644 index 0000000..0679bcf --- /dev/null +++ b/skills/accounting/resultatopgoerelse/SKILL.md @@ -0,0 +1,148 @@ +--- +name: resultatopgoerelse +description: > + Byg en dansk resultatopgørelse (P&L) ud fra Radix ERP-data — aktiveres ved forespørgsler om + "resultatopgørelse", "P&L", "årsregnskab", "indtjening pr. måned/kvartal/år". +version: 0.1.0 +author: radix-accountant +license: MIT +metadata: + hermes: + tags: [accounting, dansk-regnskab, resultatopgoerelse, radix-erp] + related: [kontoplan-mapping, momsafregning] +--- + +# Resultatopgørelse (Dansk P&L) + +Genererer en **dansk resultatopgørelse** med korrekt kontogruppering iht. dansk standardkontoplan, dansk talformatering (`1.234.567,89`) og dansk terminologi (Bruttofortjeneste, Driftsresultat, Årets resultat). + +## Aktiveres ved + +- "Vis resultatopgørelsen for Q1 2026" +- "Hvad er bruttofortjenesten for maj?" +- "Generer P&L for hele 2025" +- "Sammenlign omsætning mellem januar og februar 2026" + +## Struktur (standard dansk resultatopgørelse) + +``` +RESULTATOPGØRELSE — [Periode] +═══════════════════════════════════════════════════ + +NETTOOMSÆTNING + Salg af varer/ydelser (konto 1000-1099) 1.234.567,89 + Andre driftsindtægter (konto 1100-1199) 5.678,90 +─────────────────────────────────────────────────── + Nettoomsætning i alt 1.240.246,79 + +VAREFORBRUG / COGS + Varekøb (konto 1300-1399) −450.123,45 + Direkte løn (konto 1400-1499) −89.012,34 + Fragt/emballage (konto 1500-1599) −12.345,67 +─────────────────────────────────────────────────── + Vareforbrug i alt −551.481,46 + +═══════════════════════════════════════════════════ +BRUTTOFORTJENESTE 688.765,33 + Bruttomargin 55,5% + +EKSTERNE OMKOSTNINGER + Salgsomkostninger (konto 1600-1699) −23.456,78 + Lokaleomkostninger (konto 1700-1799) −45.678,90 + Administrationsomkostninger (1800-1899) −34.567,89 +─────────────────────────────────────────────────── + Eksterne omkostninger i alt −103.703,57 + +═══════════════════════════════════════════════════ +DRIFTSRESULTAT FØR AFSKRIVNINGER (EBITDA) 585.061,76 + EBITDA-margin 47,2% + +AFSKRIVNINGER + Afskrivninger (konto 2000-2099) −50.000,00 +─────────────────────────────────────────────────── + Afskrivninger i alt −50.000,00 + +═══════════════════════════════════════════════════ +DRIFTSRESULTAT (EBIT) 535.061,76 + Driftsmargin 43,2% + +FINANSIELLE POSTER + Renteindtægter (konto 2100-2199) 1.234,56 + Renteudgifter (konto 2200-2299) −5.678,90 + Kursreguleringer (konto 2300-2399) 789,01 +─────────────────────────────────────────────────── + Finansielle poster i alt −3.655,33 + +═══════════════════════════════════════════════════ +RESULTAT FØR SKAT 531.406,43 + +Skat (konto 2500-2599) −117.149,42 +─────────────────────────────────────────────────── + +═══════════════════════════════════════════════════ +ÅRETS RESULTAT 414.257,01 + Nettoavance 33,4% +═══════════════════════════════════════════════════ +``` + +## Workflow + +1. **Spørg om periode** (måned, kvartal, år, eller brugerdefineret datointerval) +2. **Hent data fra Radix ERP** via API eller Uniconta-eksport — spørg brugeren hvilken kilde der foretrækkes +3. **Gruppér konti** efter dansk standardkontoplan-struktur (se kontoplan-mapping skill) +4. **Beregn totaler** for hver sektion (Nettoomsætning, Vareforbrug, Eksterne omkostninger, osv.) +5. **Beregn marginer**: + - Bruttomargin = Bruttofortjeneste / Nettoomsætning × 100 + - EBITDA-margin = EBITDA / Nettoomsætning × 100 + - Driftsmargin = EBIT / Nettoomsætning × 100 + - Nettoavance = Årets resultat / Nettoomsætning × 100 +6. **Formater tal** iht. faste konventioner (punktum tusindtalsseparator, komma decimal) +7. **Vis rapport** med tydelig periode, totaler og marginer +8. **Tilbyd sammenligning** med tilsvarende periode året før (hvis data findes) + +## Sammenlignings-rapport (bonus) + +Når brugeren beder om en sammenligning: + +``` +SAMMENLIGNING — [Periode A] vs [Periode B] +═══════════════════════════════════════════════════ + Periode A Periode B Ændring +──────────────────────────────────────────────────────────── +Nettoomsætning 1.234.567,89 1.100.000,00 +12,2% +Bruttofortjeneste 688.765,33 590.000,00 +16,7% + Bruttomargin 55,5% 53,6% +1,9 pp +Driftsresultat (EBIT) 535.061,76 420.000,00 +27,4% + Driftsmargin 43,2% 38,2% +5,0 pp +Årets resultat 414.257,01 310.000,00 +33,6% +═══════════════════════════════════════════════════ +``` + +`pp` = procentpoint (almindelig dansk regnskabsterm). + +## Almindelige fejl at undgå + +- ❌ Vis "−" som `-` (bindestreg) — brug Unicode minus `−` (U+2212) for korrekt typografi +- ❌ Bland ikke `1.234,56` (dansk) med `1,234.56` (US) — brug ALTID dansk +- ❌ Medtag ikke moms i omsætningstallet — nettoomsætning er ekskl. moms +- ❌ Vis ikke "EBIT" som "Driftsresultat" og "EBITDA" som "Driftsresultat før afskrivninger" som om de er det samme — de er forskellige +- ✅ ALTID vis "Dette er ikke bogføringsrådgivning" i årsregnskaber + +## Eksempel på aktivering + +**Bruger:** "Vis mig resultatopgørelsen for 2025" + +**Svar (forkortet):** +> Henter transaktioner for perioden 01-01-2025 til 31-12-2025 fra Radix ERP... +> +> ``` +> RESULTATOPGØRELSE — 01-01-2025 til 31-12-2025 +> ═══════════════════════════════════════════════ +> Nettoomsætning i alt 12.345.678,90 +> ... +> ÅRETS RESULTAT 4.142.570,10 +> Nettoavance 33,6% +> ═══════════════════════════════════════════════ +> ``` +> +> *Dette er ikke bogføringsrådgivning. Kontakt din revisor for årsregnskab.* diff --git a/skills/data-science/erp-data-reconciliation/SKILL.md b/skills/data-science/erp-data-reconciliation/SKILL.md new file mode 100644 index 0000000..f486890 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/SKILL.md @@ -0,0 +1,314 @@ +--- +name: erp-data-reconciliation +description: | + Analyze and reconcile financial/accounting data between two ERP systems + (e.g. SAP/Coop and Uniconta) using Excel exports. Covers file inspection, + column mapping, Danish-format normalization, match-engine design, and + iterative build workflow. +trigger: | + User asks to reconcile, match, compare, or afstemme data between two + accounting/ERP systems, typically via Excel files. Also relevant when + importing/exporting financial ledgers, kontoudtog, vendor/customer + statements, or bonus calculations across systems. +--- + +# ERP Data Reconciliation Skill + +## Overview + +Reconciling data between two ERP/accounting systems is a multi-phase task. +The typical deliverable is a webapp or script that can: +1. Import Excel files from both systems +2. Normalize dates, amounts, and text +3. Match postings between systems +4. Show unmatched, partial matches, and differences +5. Support manual approval/rejection +6. Generate reports + +## Phase 1: File Analysis (CRITICAL — do not skip) + +Before writing any import code, run a structured analysis of the Excel files. + +### Steps +1. List all files in both source directories +2. For each file, identify: + - Sheet names + - Actual header row location (may NOT be row 1!) + - Column names and data types + - Date columns and their formats + - Amount columns and their formats + - Text columns + - Reference/invoice/document columns + - Null percentages + - Duplicate rows + - Data range (min/max dates) + +### Key Pitfalls +- **Header row is NOT always row 1.** Uniconta exports often have account + info in rows 1-2, blank row 3, headers in row 4. +- **Amounts mixed formats.** Danish Excel files often have amounts as both + Excel floats AND as Danish text strings like `-2.543.803,93`. + Always normalize using a robust parser: + ```python + def parse_danish_number(val): + if pd.isna(val): return float('nan') + if isinstance(val, (int, float)): return float(val) + s = str(val).strip() + if not s: return float('nan') + if '.' in s and ',' in s: + s = s.replace('.', '').replace(',', '.') + elif ',' in s: s = s.replace(',', '.') + return float(s) + ``` +- **Invoice/reference numbers may NOT match between systems.** This is the + #1 cause of failed reconciliation. Always check overlap BEFORE assuming + IDs can be used as join keys. Use amount overlap as a quick viability check. + +### Analysis Script Template +Use or adapt `scripts/analyze_excel.py` from the Coop-Uniconta project: +- Reads all sheets in all files +- Prints column stats, null counts, sample values +- Detects Danish number formats +- Identifies empty/duplicate rows + +## Phase 2: Determine Match Strategy + +After analysis, document the match strategy BEFORE coding the engine. + +### Decision Tree +1. **Do invoice/reference numbers overlap?** + - If yes → use `Faktura + abs(Amount) + Date` as PRIMARY signal (two-phase). + First phase: Reference==Faktura + abs(Amount) within tolerance (±5 DKK) + Date±21d. + Second phase: abs(Amount) + Date±21d for remaining rows. + - If no → use `Amount + Date + Text similarity` +2. **Are dates exact or within tolerance?** + - Same day → exact + - ±1-3 days → high confidence partial + - ±7 days → medium confidence + - ±21 days → general match tolerance + - >21 days → fuzzy/manual only +3. **Is text available on both sides?** + - Both have text → Levenshtein/rapidfuzz similarity score + - Only one side has text → amount+date only +4. **Do signs differ?** + - Same sign → same direction transaction + - Opposite sign → same transaction viewed from creditor vs debtor side. + Match on `abs(amount)` but label "MODSAT" in output. + *Example: Coop RE (payment) is negative; Uniconta Faktura is positive.* + +### Tolerance Matching (V6 innovation) + +Instead of exact amount match (±0.01), use a tolerance (±5 DKK): +- Catches rounding differences between systems +- Catches partial payments (one system may record 440.000, the other 430.732) +- Uses bucket-based indexing for fast lookup: + ```python + AMT_TOL = 5.0 + bucket = round(abs_amount / AMT_TOL) * AMT_TOL + # Search adjacent buckets: [bucket-AMT_TOL, bucket, bucket+AMT_TOL] + ``` + +### Coop Fakturaliste + Uniconta Betalingsfordeling as the Simple Forward Workflow + +When the user wants the process to be as simple as possible, prefer a forward-looking workflow based on: +1. `Payment_advice` +2. latest Uniconta Coop debtor/payment allocation export +3. the complete Coop invoice list + +Model the invoice list and payment allocation as separate import sources. Use invoice number as the primary join between Coop invoices and Uniconta allocation lines, and calculate paid/open/partial status from payment/settlement lines rather than only from original invoice postings. Treat the payment allocation as an explanation layer between a bank deposit and many invoice/credit lines. + +See `references/coop-invoice-payment-allocation.md` for the concrete Django model/import/status pattern, UI shape, and tests. + +### Coop Payment Advice / Settlement Files as a Fourth Source + +When the user provides Coop betalingsadviseringer/opgørelser for individual bank +payments, treat them as a separate reconciliation source, not just attachments. +They are often the missing bridge: + +`Bank deposit → Coop payment advice total → advice invoice/credit lines → Coop ledger / Uniconta invoices` + +For Coop-Uniconta style systems, the import UI should expose the full operational set, not just "Coop" and "Uniconta": +1. Uniconta Coop Debitor / payment allocation +2. Uniconta/ERP invoice list: all Coop invoices/credit notes for the year +3. Coop account statement / kontoudtog +4. Bank statement containing all Coop postings +5. Coop Payment_advice documents, uploadable continuously as they are collected + +Recommended source/model names: +- `coop_settlement` or `coop_payment_advice` +- `PaymentAdvice` / `PaymentAdviceDocument` / `SettlementFile`: file name, unique id/bilag number, file type + (PDF/XLSX), document date, payment date, total amount, period, parse status, matched bank transaction, raw + text/raw JSON. +- `PaymentAdviceLine` / `SettlementLine`: invoice number, invoice date, company + code, amount, currency, line type (invoice/credit/bonus/modregning/unknown), + links to bank/Coop/Uniconta transactions, match status, explanation. + +If the user does not yet have all payment advice documents, still allow upload now. Store raw PDF text or Excel row JSON with best-effort date/amount/reference/classification so later parsers can be tightened without re-requesting files. + +Parser patterns observed: +- XLSX files may have header row 3 with columns `Fakturanummer`, `Fakturadato`, + `Firmakode`, `Beløb`, `Valuta` and supplier metadata in rows 1-2. +- Text PDFs often extract as repeated triples: + invoice number line, `dd.mm.yyyy firmakode` line, amount/`DKK` line. +- Some PDFs split sign into a separate `-` column/line; treat `123,45`, `-`, + `DKK` as `-123,45`. +- Some PDFs are bank statement screenshots, not payment advice. Classify and do + not parse them as settlement lines. +- Some files include manual middle calculations or special layouts. Keep raw text + and mark parse confidence/status instead of silently forcing a schema. + +Matching/reporting logic: +1. Match payment-advice total to bank deposit using ±5 DKK tolerance. +2. Then match advice lines to Uniconta by invoice number first, then amount/date. +3. Match advice lines to Coop ledger by reference/doc number; in the Coop-Uniconta + project this was a very strong signal. +4. Treat payment-advice matches as an evidence/explanation layer, not as automatic + ledger settlement. An invoice can remain open in Uniconta while Payment_advice + proves Coop included it in a settlement, credit, bonus, modregning, or manual + middle calculation. +5. Report per bank deposit/open item: bank date/amount, matched advice file, + difference, line count, Uniconta hit count, Coop hit count, credit/bonus/ + modregning total, advice documents/vouchers/classifications, and status OK / + partial / needs review. +6. Keep 2026 or out-of-period advice files in the database, but exclude or label + them separately when current bank/Uniconta data only covers 2024-2025. + +### Invoice Status: Net-based + Deduplication + +**Critical pitfall:** The same payment may exist in BOTH systems (Coop RE and Uniconta Betaling). If you sum both, you get double the actual paid amount. + +**Solution:** Calculate net payments per invoice: +```python +# Net Coop payments (NOT abs-sum!) +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 + +# Net Uniconta payments +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 + +# Deduplicate: if both exist, use only one +if coop_paid > 0 and uni_paid > 0: + if abs(coop_paid - uni_paid) <= AMT_TOL: + betalt = coop_paid # Same payment recorded twice + else: + betalt = max(coop_paid, uni_paid) # Partial, take largest +``` + +**Also handle:** Positive Coop RE amounts are credit memos (modregninger) that should be SUBTRACTED from the net, not added with abs(). + +### Match Scoring Formula (default) +``` +Score = 0 +# Amount match (40%) +if abs(a-b) < AMT_TOL: Score += 40 + +# Date match (35%) +days = abs(d1 - d2).days +if days == 0: Score += 35 +elif days <= 3: Score += 25 +elif days <= 7: Score += 15 +elif days <= 14: Score += 5 + +# Text match (25%) +if both_have_text: + similarity = fuzzy_ratio(text1, text2) # 0-100 + if similarity > 80: Score += 25 + elif similarity > 50: Score += 12 + +# Reference bonus (extra) +if reference_match: Score += 40 + +# Thresholds +if Score >= 85: exact_match, auto_approvable +if Score >= 60: partial_match, requires_manual +if Score >= 40: fuzzy_match, requires_manual +``` + +| Table | Key Fields | +|-------|-----------| +| `ImportBatch` | source (coop/uniconta), filename, imported_at, raw_row_count | +| `Posting` | batch_id, source, fiscal_year, posting_date, amount, original_text, normalized_text, reference, document_number, original_data (JSON) | +| `Match` | posting_a_id, posting_b_id, score, match_type (exact/partial/fuzzy/manual), status (proposed/approved/rejected), matched_by (auto/user), matched_at | +| `AuditLog` | user, action, match_id, old_status, new_status, comment, timestamp | + +## Phase 4: Build Iteratively + +Follow this order strictly: +1. Analysis scripts and documentation → `ANALYSE.md` +2. Import parser with preview → validate before commit +3. Normalization layer (dates, amounts, text) +4. Match engine (exact → partial → fuzzy) +5. Simple web UI (dashboard → import → matching table) +6. Manual approve/reject + audit log +7. Reports and exports +8. Bonus calculation (if needed) +9. Tests + README + Docker + +### Revisor/Auditor Package Pattern + +When the user asks for output that can be sent to an accountant/revisor, produce a deterministic evidence package rather than relying on dashboard screenshots. First verify and remove duplicate import runs after a DB backup; then export a Markdown summary plus an Excel workbook with source files, month totals, open invoice/credit residuals, unexplained bank items, ignored bank items, payment advice rows, and raw normalized postings. Use explicit status language such as `AFSTEMT_MED_ÅBNE_POSTER` when the bank layer reconciles but invoice/credit-note residuals remain. + +See `references/auditor-pack-and-dedup.md` for the duplicate-import cleanup sequence, auditor workbook sheet shape, status semantics, and verification checklist. + +### Idempotent SourceFile Imports + +For accounting import UIs, especially Coop `Payment_advice` bulk uploads, make uploads idempotent. Add a SHA256 `content_hash` on the source-file model, compute it before creating a new row, skip identical reuploads, and update/reparse an existing same-named source file when the content changed. Wrap child-row replacement in a database transaction and test both duplicate-skip and update-replace paths so repeated user uploads do not inflate reconciliation totals. + +See `references/idempotent-source-file-imports.md` for the Django model/view/parser pattern, UI counters, bulk upload setting, and regression checklist. + +## Danish-specific Conventions + +- Dates: use ISO format internally (`YYYY-MM-DD`), but display in Danish + (`dd-MM-yyyy`) per user preference. Parse ambiguous Danish text dates with day-first semantics (`dayfirst=True`). +- Amounts: always normalize to float/Decimal; display with 2 decimals, Danish comma. +- Fiscal year: calendar year (1/1 – 31/12) unless specified otherwise. +- For Coop workflows, model invoice year/month separately from payment year/month: Coop can have ~3 months of credit, so December invoices may be paid in March of the following fiscal year. Reports must not treat that as an error by default. +- Text: normalize by lowercasing, stripping extra spaces, removing punctuation before fuzzy comparison. +- Regulation/bonus/fee/fine classification should be explicit and reviewable. Start with keyword classification (`bonus`, `rabat`, `gebyr`, `bøde`, `regulering`, `korrektion`, `modregning`) but keep raw data and manual review because wording/layout varies. + +## Django Accounting UI Display Formatting + +When the reconciliation deliverable is a Django webapp, treat amount display as part of the reconciliation contract rather than a cosmetic detail. Use one shared template filter/localization utility for money columns in dashboards, match tables, invoice/payment status pages, and reports. + +For Danish accounting UIs, monetary amounts should normally render with `.` thousands separators, `,` decimal separators, exactly two decimals, and a leading minus sign for negative values (for example `1.234.567,89`, `-1.234.567,89`, `0,00`). Prefer `Decimal`-based formatting over Python floats. Replace monetary `|floatformat:2` usage, but do not reformat percentages, match scores, counts, or raw imported/audit JSON unless explicitly requested. + +Minimum verification for UI formatting changes: +- Add a unit/template test for the formatting helper. +- Add at least one rendered-page regression test proving formatted money appears and raw `1234567.89`-style output is absent. +- Confirm templates using the filter load its tag module, and restart the web container if needed. + +See `references/django-accounting-ui-formatting.md` for the concrete Django template-filter implementation, search targets, Docker test commands, and pitfalls. + +## References +- See `references/coop-uniconta-analysis.md` for the concrete discovery + patterns from the Coop ↔ Uniconta project (header row locations, + column mappings, SAP document type codes, etc.). +- See `references/coop-uniconta-match-engine.md` for the 5-phase match engine + implementation, tolerance matching, net-based invoice status, and score formulas. +- See `references/bank-reconciliation.md` for three-source reconciliation extending + Coop ↔ Uniconta with bank CSV — month-level validation, bonus reconciliation, + and many-to-one (bank payment = sum of Coop invoices) matching strategy. +- See `references/coop-payment-advice.md` for using Coop betalingsadviseringer + (PDF/XLSX settlement files) as the bridge between bank deposits and invoice + lines, including parser patterns, coverage metrics, and reporting shape. +- See `references/three-way-django-analysis-engine.md` for Django implementation + structure: separate analysis module, batch import command, three-way report + outputs, interpretation pitfalls, and minimum tests. +- See `references/coop-invoice-payment-allocation.md` for the simplified forward + workflow using Coop invoice lists + Uniconta payment allocation + Payment_advice, + including separate models, invoice paid/open status logic, employee-expense + exceptions, UI badges, and minimum tests. +- See `references/coop-uniconta-import-ui-and-payment-advice.md` for the five-source + import UI pattern, flexible PDF/Excel Payment_advice raw import, monthly dashboard + fields, 3-month Coop credit handling, and regression tests. +- See `references/auditor-pack-and-dedup.md` for revisor-ready exports, duplicate + import cleanup after DB backup, status semantics such as `AFSTEMT_MED_ÅBNE_POSTER`, + and Excel/Markdown verification. +- See `references/idempotent-source-file-imports.md` for SHA256-based idempotent + accounting source-file uploads, update-vs-skip semantics, Django transaction + boundaries, bulk upload limits, and regression tests. +- See `references/django-accounting-ui-formatting.md` for Django money-formatting + implementation details, template search targets, regression tests, and pitfalls. \ No newline at end of file diff --git a/skills/data-science/erp-data-reconciliation/references/auditor-pack-and-dedup.md b/skills/data-science/erp-data-reconciliation/references/auditor-pack-and-dedup.md new file mode 100644 index 0000000..95cca2d --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/auditor-pack-and-dedup.md @@ -0,0 +1,66 @@ +# Auditor-ready reconciliation packs and duplicate-import cleanup + +Use this when a reconciliation webapp must produce material that can be sent to an accountant/revisor, not just a green dashboard. + +## Revisor-grade status language + +Do not collapse every successful bank reconciliation into “fully reconciled”. Use explicit statuses: + +- `AFSTEMT`: all required sources loaded, relevant bank movements explained, and no open invoice/credit-note residuals. +- `AFSTEMT_MED_ÅBNE_POSTER`: bank layer is explained, but invoice/credit-note residuals remain and must be reviewed by accounting/revisor. +- `BANK_DIFFERENCER`: required sources loaded, but relevant bank postings are unexplained. +- `MANGLER_DATA`: one or more required sources are missing. + +This prevents the dangerous interpretation “0 unexplained bank postings = no accounting work remains”. + +## Duplicate import cleanup pattern + +Historical accounting imports are often rerun. Before trusting totals, check for duplicate `SourceFile` rows by source/file name/parsed count and repeated downstream row counts. + +Safe cleanup sequence: + +1. Create a database backup first. +2. Identify duplicate import runs, keeping the canonical/original `SourceFile` rows. +3. Delete duplicate `SourceFile` rows through the ORM so related imported rows cascade consistently. +4. Record an audit log entry with reason and deleted source file IDs. +5. Recompute counts and reports after cleanup. + +Do not modify original source files. The goal is to remove duplicate imported rows, not change evidence. + +## Auditor pack contents + +Generate a deterministic export package with at least: + +- Markdown executive summary in Danish. +- Excel workbook with sheets: + - `Resume`: data completeness, bank coverage, open balances, status. + - `Kildefiler`: imported files, parsed/error counts, timestamps. + - `Måneder`: month-level totals across Coop, bank, invoice list, and payment allocation. + - `Åbne poster`: invoice/credit-note residuals with invoice number, date, customer/account, total, paid, residual, vouchers, explanation. + - `Uforklaret bank`: relevant bank movements that could not be explained. + - `Ignoreret bank`: bank movements outside the debtor reconciliation scope, with reason. + - `Payment advice`: uploaded settlement/advice records. + - `Rå posteringer`: normalized postings for traceability. + +Use Danish number formatting in human-facing Markdown and UI (`1.234.567,89`). Excel cells may stay numeric where useful, but headings and sheet names should be accountant-readable. + +## Implementation notes + +- Keep the analysis/export layer read-only; it should not mutate match status. +- Convert timezone-aware datetimes to ISO strings before writing with pandas/openpyxl; Excel rejects timezone-aware datetimes. +- Include both gross open amount and net open balance. Accountants often need both: + - gross open review amount = sum of absolute residuals; + - positive residuals = potential receivables; + - negative residuals = open credits/modregninger; + - net residual = positive + negative. +- Add tests that read the generated workbook back and assert key sheets/rows exist. +- If visual browser automation is unavailable, verify dashboard output with Django’s test client/HTTP response text and keep a separate browser/UI check when the environment supports it. + +## Minimum verification + +- Framework check passes. +- Full reconciliation/core tests pass. +- Auditor export command runs idempotently. +- Workbook exists and contains all expected sheets. +- Summary status and open totals match the recomputed analysis function. +- Dashboard shows bank coverage and open-poster status consistently with the exported package. diff --git a/skills/data-science/erp-data-reconciliation/references/bank-implementation.md b/skills/data-science/erp-data-reconciliation/references/bank-implementation.md new file mode 100644 index 0000000..ebaddc5 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/bank-implementation.md @@ -0,0 +1,73 @@ +# 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): +```python +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-1` first, then `utf-8`. Most Danish bank exports use latin-1. +- **Delimiter**: `;` (semicolon) +- **Header mapping**: Fuzzy-match by lowercasing, stripping null bytes and quotes: + - `dato` → date column + - `valør`/`valuta` → value date column + - `tekst`/`beskrivelse` → text column + - `bel` (prefix) → amount column + - `saldo` → balance column + - `reference`/`bilagsrefer` → reference column +- **Date parse**: `DD.MM.YYYY` → split by dots → `datetime(int(parts[2]), int(parts[1]), int(parts[0])).date()` +- **Amount parse**: + 1. Strip `\xa0` (nbsp), `\u202f` (narrow nbsp), `" DKK"` + 2. If `.` and `,` both present → remove dots, replace comma with dot + 3. If only comma → replace with dot + 4. Convert to `Decimal` +- **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. + +```python +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_sum` cards; add `bank_count` / `bank_sum` columns to monthly overview table +- **Upload form**: Add `` +- **Reconciliation list**: Add `bank` to 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 + +1. **Migrations not detected**: If `makemigrations` reports "No changes detected", check whether a prior manual migration already captured the changes. Run `migrate --check` first. +2. **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. +3. **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 on `account_number` + date+amount grouping. +4. **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. +5. **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. diff --git a/skills/data-science/erp-data-reconciliation/references/bank-reconciliation.md b/skills/data-science/erp-data-reconciliation/references/bank-reconciliation.md new file mode 100644 index 0000000..9ffbef0 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/bank-reconciliation.md @@ -0,0 +1,222 @@ +# 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: + ```python + 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 +```python +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. + +```python +# 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 +``` diff --git a/skills/data-science/erp-data-reconciliation/references/coop-invoice-payment-allocation.md b/skills/data-science/erp-data-reconciliation/references/coop-invoice-payment-allocation.md new file mode 100644 index 0000000..9d257d3 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/coop-invoice-payment-allocation.md @@ -0,0 +1,109 @@ +# Coop fakturaliste + Uniconta betalingsfordeling workflow + +Session-derived pattern from the Coop-Uniconta project: simplify reconciliation by using three forward-looking sources instead of many historical exports. + +## Target workflow + +Use these files as the primary operational import set: +1. Coop invoice list (`Coop Fakturaer YYYY.xlsx`) — all invoices/credits issued to Coop. +2. Uniconta payment allocation / debtor ledger (`Coop Debitor YYYY.xlsx`) — invoice and settlement/payment lines. +3. Coop `Payment_advice` — later bridge source for explicit settlement detail. + +This supports the user question: “Mangler Coop at betale nogle fakturaer?” + +## Models / concepts + +Recommended separate models, not generic postings only: + +### `CoopInvoice` +- source file, fiscal year/accounting year +- date +- account number/name +- customer reference (`Deres ref`) +- order number +- invoice number +- requisition +- total, net, VAT amounts +- payment terms / sent dates if present +- raw row JSON + sheet/row reference + +### `PaymentAllocationLine` +- source file, fiscal/accounting year +- date +- account +- invoice number +- voucher/document number +- posting number +- text +- debit, credit, VAT +- physical voucher/origin/account type/debtor-creditor/name +- `is_payment_line` boolean +- raw row JSON + sheet/row reference + +## Payment-line detection + +In Uniconta debtor/payment allocation files, not every line is a payment. They may contain both original invoice postings and later settlement lines. + +Observed signals for payment/settlement lines: +- `Bilag != Faktura` +- text contains `Coop 0010014808` +- text contains `Omp. Coop` + +Do not infer paid status from invoice existence alone; calculate it from settlement/payment lines linked by invoice number. + +## Invoice payment status algorithm + +For each `CoopInvoice.invoice_number`: +1. Find `PaymentAllocationLine` rows with same invoice number. +2. Sum payment/settlement effect from rows marked `is_payment_line`. +3. Compare against `CoopInvoice.total_amount`. +4. Emit status: + - `paid`: residual within tolerance + - `partial`: some payment but residual remains + - `open`: no settlement/payment lines found +5. Keep explanatory fields: payment vouchers, dates, amount paid, residual, source rows. + +Use Danish accounting display, but ISO dates and Decimal internally. + +## Bank reconciliation with allocation groups + +Use the payment allocation as an explanation layer between bank deposits and invoices: + +`Bank payment → allocation group/voucher → multiple invoice lines → Coop invoices` + +This is especially useful when one bank deposit equals many invoices/credits. Build groups by voucher/payment text/date and compare the group total to the bank payment. + +## Employee expense exception + +Negative bank postings whose text indicates purchases at Coop/Kvickly may be employee expenses, not Coop customer payments. Treat them as ignored/out-of-scope only when the user confirms the business meaning. In the Coop-Uniconta project, two negative bank lines with Coop/Kvickly text were confirmed as employee outlays and excluded from relevant bank coverage. + +Implementation pattern: +- keep ignored bank payments visible in reports +- do not count them as unmatched customer-payment differences +- report both total bank count and relevant bank coverage percentage + +## UI/reporting shape + +Add a dedicated invoice-payment view or dashboard section: +- paid invoices +- open/different invoices +- total invoices +- open residual amount +- status badges: `BETALT`, `ÅBEN`, `DIFFERENCE`, `FAKTURAER` +- filters for year, customer/account, amount, residual, status +- export of open/difference invoices to Excel/CSV + +## Minimum tests + +Add tests for: +1. A Coop invoice is marked paid via matching Uniconta payment allocation lines. +2. Partial/open invoices calculate residual correctly. +3. Confirmed Coop/Kvickly employee expense bank lines are ignored but still reported. +4. Relevant bank coverage is 100% when all customer-payment bank lines are explained and only confirmed out-of-scope lines remain. + +## Pitfalls + +- Do not hardcode confidential file paths or sample data into committed code. +- Keep raw Excel/source folders gitignored. +- Do not collapse invoice list, payment allocation, and payment advice into one generic parser; their semantics differ. +- Payment advice parser can be added later, but wire the import type early so UI/workflow is ready. diff --git a/skills/data-science/erp-data-reconciliation/references/coop-payment-advice.md b/skills/data-science/erp-data-reconciliation/references/coop-payment-advice.md new file mode 100644 index 0000000..43b1db3 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/coop-payment-advice.md @@ -0,0 +1,125 @@ +# Coop betalingsadviseringer / payment advice as reconciliation bridge + +Use this when Coop provides per-payment settlement/advice documents (PDF/XLSX) +for bank deposits. These documents can turn previously unexplained bank deposits +into explainable groups of invoice, credit, bonus, and modregning lines. + +## Durable workflow + +1. Inventory files by extension and classify each file: + - `payment_advice` — Coop betalingsadvisering/payment advice with invoice lines. + - `bank_statement` — bank print/screenshot containing a Coop deposit message. + - `unknown` — manual calculation, unsupported layout, or needs OCR/manual review. +2. Extract payment advice rows: + - invoice/fakturanummer + - invoice date/fakturadato + - company code/firmakode + - amount/beløb + - currency/valuta + - raw row/text and parse confidence +3. Compute advice totals from parsed rows; do not rely solely on a trailing total + because some PDFs have manually added middle calculations or split sign columns. +4. Match advice total to bank transaction with ±5 DKK tolerance. +5. Match advice lines to Coop ledger by reference/doc number and to Uniconta by + invoice number. Fall back to amount/date only for remaining lines. +6. Produce a bank-deposit-level report for accounting review. + +## PDF/XLSX parser patterns + +XLSX pattern: +- Supplier metadata may be rows 1-2. +- Header row often contains: `Fakturanummer`, `Fakturadato`, `Firmakode`, `Beløb`, + `Valuta`. +- Dates can already be Excel datetimes. + +Text PDF pattern A: +```text +Payment Advice +Fakturanummer +Fakturadato +Firmakode Beløb +Valuta +5119751312 +17.11.2023 2245 +-1556,98 DKK +... +``` + +Text PDF pattern B with split sign: +```text +5126294822 +24.10.2024 1000 +2.119,86 +- +DKK +``` +Treat the amount as negative when a standalone `-` follows the amount. + +Bank-statement PDFs: +- May include `Kontobevægelser`, `Coop Danmark A/S -SE MEDD.`, and a bank amount. +- Do not parse these as settlement invoice lines; classify separately and optionally + link to the bank transaction/message. + +## Reporting shape + +Per bank deposit show: +- bank date +- bank amount +- matched payment advice file(s) +- total according to advice +- difference +- number of advice lines +- number of lines found in Coop ledger +- number of invoice numbers found in Uniconta +- credit/negative line total +- status: `OK`, `partial`, `needs_review`, `out_of_period` + +## Interpretation pitfalls + +- Advice files may include invoice dates from prior years but payment dates in the + current year. Derive both invoice year and payment/bank year. +- A low overlap with existing “likely Coop owes” flags does not mean the advice + files are useless; it means the analysis must model settlement files as their + own source rather than using them as loose attachments. +- 2026 advice files should be imported but labelled out-of-period when bank or + Uniconta data for 2026 is incomplete. +- Keep original PDFs/XLSX confidential and out of git; only commit parser code, + schemas, docs, and non-sensitive aggregate findings. + +## Coop-Uniconta session observations + +A mixed folder of 125 files contained 116 PDFs and 9 XLSX files. A first pass found +122 payment advice files, 2 bank-statement PDFs, and 1 unknown/special PDF. Parsed +advice lines were strong as a bridge: most lines matched Coop references/doc +numbers, many matched Uniconta invoice numbers, and many previously unexplained +bank deposits could be explained by matching advice totals to bank amounts within +±5 DKK. + +Later production import of the same class of files used PyMuPDF (`fitz`) for PDFs +and pandas/openpyxl-style Excel parsing, creating one source-file record per +Payment_advice file and line-level records with raw JSON/text. A folder with 125 +files (116 PDF, 9 Excel) produced 2,727 Payment_advice lines without mutating +original files. For a 2025 auditor pack, 1,253 lines were relevant and 78 of 296 +open invoice/credit-note residuals had a Payment_advice trail. + +Important interpretation: a Payment_advice line must not automatically close or +"pay" an invoice in the ledger analysis. Treat it as an evidence/explanation layer +for open items: the invoice can remain open in Uniconta while the advice proves +Coop included it in a settlement, modregning, credit, bonus, or manual middle +calculation. Reports should therefore add fields such as advice line count, advice +amount, document list, voucher/bilag list, classifications, and a human-readable +explanation rather than changing ledger status silently. + +Implementation pattern that worked: +- Add `PyMuPDF>=1.24` to backend dependencies before relying on PDF import in Docker. +- Extend the batch importer to include `Entydigt*.pdf`, `Entydigt*.xlsx`, `*.xlsm`, + and `*.xls` from the payment-advice folder. +- Parse file names like `Entydigt id_4765_Bilag_7283.xlsx` into unique/document id + and voucher/bilag metadata when present. +- Classify lines as payment, credit, adjustment/manual total, or unknown, while + preserving raw row/text for manual review. +- Add regression tests that cover the case "open invoice has Payment_advice trail + but no Uniconta payment line"; expected output is explanatory evidence, not an + auto-paid status. +- Regenerate Markdown + Excel auditor packs after import and include Payment_advice + coverage counts in the summary. diff --git a/skills/data-science/erp-data-reconciliation/references/coop-uniconta-analysis.md b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-analysis.md new file mode 100644 index 0000000..439e220 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-analysis.md @@ -0,0 +1,235 @@ +# Coop ↔ Uniconta Reconciliation Reference (Aktualiseret 2025-05-26) + +Konkrete felter, mappings og pitfalls fra det aktuelle Coop-Uniconta projekt. + +--- + +## Coop (SAP export) — Kontoudtog.xlsx + +| Kolonne | Navn | Type | Match-brug | +|---------|------|------|------------| +| Entry Date | Entry Date | datetime | Sekundær | +| Document Type | Document Type | **string** | **Filter + match-type** | +| Document Date | Document Date | datetime | Sekundær | +| **Posting Date** | Posting Date | **datetime** | **Primær dato** | +| **Reference** | Reference | **string (75% non-null)** | **Primær match-nøgle** | +| Net Due Date | Net Due Date | datetime | Forfaldsdato | +| **Amount** | Amount in Local Currency | **float** | **Primær beløb** | +| Document Currency | Local Currency | string ('DKK') | Filter | +| **Document Number** | Document Number | int | **Audit/trace** | +| Text | Text | string (20% non-null) | Sekundær match | +| Payment Reference | Payment Reference | string (12% non-null) | OCR/trace | +| User Name | User Name | string | Audit | + +### Coop Document Types (2024-2025) +| Type | Andel | Beskrivelse | Uniconta-modpart | +|------|-------|-------------|----------------| +| RE | ~50% | Regning/betaling | Betaling/Faktura | +| ZV | ~15% | Udbetalingsforslag/faktura | Betaling | +| RG | ~6% | Kontoafstemning | Kreditnota | +| KG | ~5% | Kontoafstemning gruppe (bonus/rabat) | Kreditnota | +| ZC | ~3% | Kreditering | Kreditnota | +| ZP | ~0.4% | Betaling | Betaling | +| XX | ~0% (filtreret fra) | Manuelt bogført/migrering | — | + +**VIGTIGT:** ZV-poster er positive fakturaer udstedt til kunder. De har typisk **ingen Reference/Faktura-nummer** og matches via `abs(beløb) + dato` mod Uniconta Betalinger. + +--- + +## Uniconta — Coop danmark.xlsx + +### Fil-struktur +- Række 1: Konto-info (Checked, Konto, Kontonavn, Adresse, Bynavn) +- Række 2-3: Tomme +- Række 4: Kolonne-headers: Dato, Faktura, Forfaldsdato, Bilag, Tekst, Beløb, Resterende, Forfalden, Konteringstype, Sum +- Række 5+: Data + +| Kolonne | Navn | Type | Match-brug | +|---------|------|------|------------| +| **Dato** | Dato | **datetime** | **Primær dato** | +| **Faktura** | Faktura | **mixed int/str, 0=ingen** | **Primær match-nøgle** | +| Forfaldsdato | Forfaldsdato | datetime | Sekundær | +| Bilag | Bilag | mixed int/str | Audit | +| **Tekst** | Tekst | **string** | **Tekst-match** | +| **Beløb** | Beløb | **float/tekst** | **Primær beløb** | +| Resterende | Resterende | float | Filter | +| Forfalden | Forfalden | float | Filter | +| **Konteringstype** | Konteringstype | **string** | **Filter + match-bonus** | +| Sum | Sum | float | IGNORE — løbende saldo | + +### Uniconta Konteringstyper +| Type | Beskrivelse | Match-modpart (Coop) | +|------|-------------|---------------------| +| Faktura | Faktura udstedt | RE (betaling) eller ZV | +| Betaling | Betaling modtaget | RE, ZP, ZV | +| Kreditnota | Kreditnota | RG, ZC, KG | +| Primo | Primo-saldo | XX (filtreres fra) | +| Afslutning | Årsafslutning | — | +| Overførsel | Overførselsbilag | — | +| Manuel | Manuel posting | — | +| Rabat | Rabat | KG | + +--- + +## Match-nøgle-observationer + +### Fakturanumre overlapper IDENTISK +- Coop `Reference` og Uniconta `Faktura` bruger **identiske 4-5 cifrede tal**. +- Eksempler: 46344, 44907, 48456, 49495, 51519 +- **2.970 unikke RE-references findes som Faktura-numre i Uniconta.** +- Dette er den stærkeste match-signal — langt stærkere end beløb+dato alene. + +### Modsat fortegn er NORMALT +| Coop type | Coop fortegn | Uniconta type | Uniconta fortegn | Match | +|-----------|-------------|---------------|-----------------|-------| +| 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) | + +**Implikation:** Match-motoren SKAL sammenligne `abs(beløb)`, ikke rå beløb. + +### Dato-tolerance +- Faktura-reference matches: ±21 dage fungerer fint +- Generelle beløb+dato matches: ±21 dage fanger de fleste +- ZV↔Betaling matches: ±30 dage anbefales (betalingsforslag vs. faktura) + +--- + +## Normalisering + +### Beløb (dansk format) +Nogle Uniconta-celler indeholder tekststrenge som `-2.543.803,93`: +```python +def parse_danish_number(val): + if pd.isna(val) or val is None: return None + if isinstance(val, (int, float)): return float(val) + s = str(val).strip().replace('\xa0','').replace('\u202f','').replace(' DKK','') + if s.lower() in ('nan',''): return None + if s.endswith('.0'): s = s[:-2] + if '.' in s and ',' in s: + s = s.replace('.','').replace(',','.') + elif ',' in s: + s = s.replace(',','.') + try: return float(s) + except: return None +``` + +### Tekst +```python +def normalize_text(t): + if not t or pd.isna(t): return '' + return re.sub(r'\s+',' ',str(t).lower().strip()).strip() +``` + +--- + +## Konto-specifikt mønster + +### "Coop 0010014808" +- Uniconta indeholder mange poster med teksten "Coop 0010014808" +- Beløb varierer fra -1.155.458,24 til -65.000 +- Nogle har fakturanummer, andre har ikke +- Dette ligner en **samlet konto-kontering** for Coop-omsætning, ikke enkeltfakturaer +- Disse poster er svære at matche automatisk og bør behandles særskilt + +### Bonus/rabat-poster +- Coop KG: "CCM Purchasing Rebate Credit" +- Uniconta Rabat: sjælden, men findes +- Disse bør matches separat via beløb + dato + bonus-tekst + +--- + +## Specifikke problemer opdaget (V6.1) + +1. **Faktura 46344** — Uniconta Faktura +441.720, Coop RE -441.720, men også Uniconta Betaling -430.732,50. Delvis kreditnota. + +2. **Faktura 51519** — Uniconta Faktura +244.926, Coop RE -244.926, Uniconta Betaling -244.926. Dobbeltregistrering (Coop+Uni betaling). + +3. **Faktura 48323** — Ingen Coop-betaling, men Uniconta Betaling -240.000 findes. Faktura er BETALT via Uniconta-betaling. + +4. **Faktura 47140/47347 (DELVIST BETALT)** — Coop har både negativ og positiv RE med samme reference. Netto-sum giver korrekt status. + +5. **Dobbelt-registrering** — Samme betaling findes som både Coop RE og Uniconta Betaling. Ved summing af begge får man 2x betalt. Løsning: brug netto-sum og dedupliker. + +6. **MR8M-modregninger** — Positive Coop RE med "MR8M Credit memo" tekst er modregninger. De skal trækkes fra i netto-beregning, ikke lægges til med `abs()`. + +--- + +## Afstemningsstrategi der virkede (V6.1 anbefalet) + +**Fase 1: Reference-match (RE↔Faktura, RG↔Kreditnota)** +- Krav: Coop.Reference == Uniconta.Faktura +- Krav: abs(Coop.Amount) ≈ abs(Uniconta.Amount) ±5 kr (tolerance) +- Krav: Dato ±21 dage +- Bonus: Korrekt konteringstype +- Score ~250 +- Resultat 2024-2025: ~2.421 matches + +**Fase 2: ZV↔Faktura** +- Coop ZV (positive fakturaer) matcher Uniconta Faktura +- abs(beløb) ±5 kr + dato ±21 dage +- Bonus: Document Number == Faktura +- Resultat 2024-2025: ~173 matches + +**Fase 3: RE↔Betaling uden reference** +- Coop RE uden fakturanummer matcher Uniconta Betaling +- abs(beløb) ±5 kr + dato ±21 dage +- Bonus: tekst-lighed +- Resultat 2024-2025: ~192 matches + +**Fase 4: Generel abs(beløb)+dato** +- abs(beløb) ±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%)** + +**Uafstemt: ~2.327 Coop (31,8%) + ~2.211 Uniconta (32,9%)** + +--- + +## Faktura-status: Netto-baseret + deduplikering + +### Dobbelt-registrering +Samme betaling findes som både Coop RE og Uniconta Betaling. + +```python +# Beregn NETTO Coop betalinger (ikke abs-sum!) +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 + +# Beregn NETTO Uniconta betalinger +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 + +# Dedupliker +if coop_paid > 0 and uni_paid > 0: + if abs(coop_paid - uni_paid) <= AMT_TOL: + betalt = coop_paid # Samme betaling + else: + betalt = max(coop_paid, uni_paid) +elif coop_paid > 0: betalt = coop_paid +elif uni_paid > 0: betalt = uni_paid +else: betalt = 0 +``` + +**Resultat:** 2.968 BETALT, 2 DELVIST, 186 UBETALT + +--- + +## 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. diff --git a/skills/data-science/erp-data-reconciliation/references/coop-uniconta-import-ui-and-payment-advice.md b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-import-ui-and-payment-advice.md new file mode 100644 index 0000000..f2b6548 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-import-ui-and-payment-advice.md @@ -0,0 +1,89 @@ +# Coop-Uniconta import UI + Payment_advice lessons + +Use this reference when building or fixing a Coop/Uniconta reconciliation webapp import flow. + +## Required import sources + +The user expects five uploadable source types, not a generic two-source UI: + +1. **Uniconta Coop Debitor / betalingsfordeling** + - Shows how Coop payments are allocated across invoices/credits. + - Primary bridge from payment to invoice lines. +2. **Uniconta/ERP fakturaliste — all Coop invoices for the year** + - Complete invoice/credit-note list for the relevant year. + - Primary source for "has Coop paid all invoices?". +3. **Coop kontoudtog** + - Account statement from Coop's system. +4. **Bankkontoudtog — all Coop postings** + - Bank statement filtered to Coop-related transactions. +5. **Coop Payment_advice documents** + - PDF and Excel files, uploaded continuously as they are collected. + - Needed to identify and explain regulations, fees, bonuses, fines, credits, and offsets. + +If a UI only shows "Coop" and "Uniconta", treat that as a workflow bug even if the backend has more enum choices. + +## Fiscal year and monthly reporting + +- Fiscal year is calendar year: 1/1–31/12. +- Store/report month for every data source. +- Keep invoice month/year separate from payment month/year. +- Coop can have around 3 months of credit: December 2025 invoices may be paid in March 2026. Cross-year payment timing is expected and should be visible, not automatically flagged as an error. + +## Payment_advice ingestion pattern + +Implement Payment_advice as a real source early, even before final matching logic is known. + +Recommended flexible model fields: + +- source file +- accounting year (nullable when no date can be inferred) +- document date +- payment date +- document/bilag number +- voucher number +- invoice number +- amount +- classification: `unknown`, `payment`, `bonus`, `fee`, `fine`, `adjustment`, `credit` +- raw text +- raw JSON / raw row data +- raw sheet name / row index + +## Parser approach + +For Excel: +- Read all sheets. +- Store raw row JSON. +- Best-effort infer date from date-like columns (`Dato`, `Date`, `Betaling`, `Forfald`). +- Best-effort infer amount from amount-like columns (`Beløb`, `Amount`, `Total`, `Netto`, `Moms`); if multiple amounts appear, pick the largest absolute amount as a temporary headline amount. +- Extract invoice/voucher/document IDs from matching column names. + +For PDF: +- Store file even if text extraction is poor. +- Try `pypdf` / `PyPDF2` text extraction if available. +- Infer date and amount from filename + extracted text. +- Mark whether text was extracted. + +Do not reject Payment_advice uploads just because final parser rules are not complete. The key is preserving raw documents/data so later parser improvements can be applied. + +## UI copy / workflow guidance + +On the import page, list the five source types in the same order as the user's workflow. Explain that data is split into accounting years by date, and that Payment_advice can be uploaded gradually. + +For monthly dashboard rows, include at least: + +- Coop postings count/sum +- Uniconta postings count/sum +- Bank postings count/sum +- Invoice list count/sum +- Payment allocation count, debit, credit +- Payment_advice count/sum +- Payment_advice classifications per month + +## Minimum regression tests + +Add tests that verify: + +1. The upload page renders all five source labels and accepts PDF for Payment_advice. +2. Payment_advice Excel import preserves raw row data and derives accounting year/month. +3. Payment_advice classification detects at least a bonus/regulation example. +4. Existing invoice/payment allocation tests still pass after adding the new source. diff --git a/skills/data-science/erp-data-reconciliation/references/coop-uniconta-match-engine.md b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-match-engine.md new file mode 100644 index 0000000..39a4d14 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/coop-uniconta-match-engine.md @@ -0,0 +1,236 @@ +# 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 diff --git a/skills/data-science/erp-data-reconciliation/references/django-accounting-ui-formatting.md b/skills/data-science/erp-data-reconciliation/references/django-accounting-ui-formatting.md new file mode 100644 index 0000000..6cbde28 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/django-accounting-ui-formatting.md @@ -0,0 +1,122 @@ +# Django Accounting UI Formatting + +Former skill: `django-accounting-ui-formatting`. + +Use this when changing how money, balances, invoices, payments, totals, or differences are displayed in a Django accounting/reconciliation webapp. + +## Core rule + +Accounting amounts must be formatted deliberately and consistently. Do not rely on ad-hoc `floatformat:2` everywhere if the app has locale-specific expectations. + +For Danish Radix accounting/reconciliation UIs, render amounts with: + +- thousands separator: `.` +- decimal separator: `,` +- exactly 2 decimals +- minus sign before the formatted number + +Example: + +```text +1234567.89 -> 1.234.567,89 +-1234567.89 -> -1.234.567,89 +0 -> 0,00 +``` + +## Recommended Django implementation + +Create a shared template filter in a common app, e.g.: + +```text +backend/apps/common/templatetags/amount_format.py +``` + +Example filter: + +```python +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP + +from django import template + +register = template.Library() + + +@register.filter +def amount_dk(value): + if value is None or value == "": + return "0,00" + + try: + amount = Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + except (InvalidOperation, ValueError, TypeError): + return value + + sign = "-" if amount < 0 else "" + amount = abs(amount) + formatted = f"{amount:,.2f}" + return sign + formatted.replace(",", "X").replace(".", ",").replace("X", ".") +``` + +Ensure `templatetags/__init__.py` exists. + +In templates: + +```django +{% load amount_format %} +{{ amount|amount_dk }} kr +``` + +## What to update + +Search all relevant templates for amount displays, especially: + +- `floatformat:2` +- table columns named Beløb, Sum, Difference, Rest, Betalt +- dashboard cards +- report tables +- invoice/payment status pages +- transaction detail pages + +Replace monetary `floatformat:2` with the shared amount filter. Leave percentages, scores, counts, and raw JSON/raw imported data alone unless the user explicitly asks otherwise. + +## Tests + +Follow TDD for behavior changes: + +1. Add a failing unit/template test for the filter: + +```python +template = Template("{% load amount_format %}{{ value|amount_dk }}") +assert template.render(Context({"value": Decimal("1234567.89")})) == "1.234.567,89" +``` + +2. Add at least one rendered-page regression test for the main UI path, asserting: + +- formatted amount is present, e.g. `1.234.567,89` +- raw unformatted amount is absent, e.g. `1234567.89` + +3. Run the relevant app tests and system check in the project runtime, usually Docker Compose for Radix internal apps: + +```bash +docker compose exec -T web python manage.py test apps.core --settings=config.settings.test -v 2 +docker compose exec -T web python manage.py check +``` + +## Verification + +Before finalizing: + +- Confirm there are no remaining monetary `|floatformat:2` usages in the changed templates. +- Confirm templates that use `amount_dk` include `{% load amount_format %}`. +- Restart the web container if the running app needs to load a new template tag module: + +```bash +docker compose restart web +``` + +## Pitfalls + +- Do not alter raw JSON/raw import displays; those are audit data and should preserve original values. +- Do not apply money formatting to percentages or match scores. +- Avoid Python float math for money formatting; use `Decimal` to avoid binary rounding surprises. +- If the app already has a localization/formatting utility, extend that instead of adding a second competing filter. diff --git a/skills/data-science/erp-data-reconciliation/references/idempotent-source-file-imports.md b/skills/data-science/erp-data-reconciliation/references/idempotent-source-file-imports.md new file mode 100644 index 0000000..704e7b0 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/idempotent-source-file-imports.md @@ -0,0 +1,122 @@ +# Idempotent SourceFile imports for accounting/reconciliation apps + +Use this when users can upload the same ERP/accounting source files repeatedly, especially bulk settlement files such as Coop `Payment_advice` PDFs/XLSX. + +## Problem + +Financial import apps often store one `SourceFile` plus many parsed rows/documents. If a user uploads the same file again, the app must not create: +- duplicate `SourceFile` records +- duplicate parsed settlement/payment/invoice lines +- duplicated totals in reconciliation reports + +If the same named file has changed, treat it as an update of that source, not as a new independent source file. + +## Durable pattern + +Add a content hash to the source-file model: + +```python +class SourceFile(models.Model): + source = models.CharField(max_length=64) + file_name = models.CharField(max_length=255) + file = models.FileField(upload_to="imports/") + content_hash = models.CharField(max_length=64, blank=True, db_index=True) +``` + +Compute SHA256 from the uploaded bytes before creating a new `SourceFile`. Always rewind the file afterwards: + +```python +import hashlib + +def uploaded_file_sha256(uploaded_file): + h = hashlib.sha256() + for chunk in uploaded_file.chunks(): + h.update(chunk) + uploaded_file.seek(0) + return h.hexdigest() +``` + +Decision table: + +| Existing row? | Same file name? | Same hash? | Action | +|---|---:|---:|---| +| no | n/a | n/a | create new `SourceFile`, parse rows | +| yes | yes | yes | skip import; report as unchanged/skipped | +| yes | yes | no | update existing `SourceFile`, delete old parsed child rows, reparse | +| yes | no | yes | usually skip as duplicate content; optionally report duplicate content under different name | +| yes | no | no | create new source file | + +For source types that represent operational settlement documents, prefer matching on `(source, file_name)` for update semantics, plus `content_hash` for duplicate detection. + +## Transaction boundary + +Wrap the parser/import in a transaction so child rows are not partially replaced: + +```python +from django.db import transaction + +@transaction.atomic +def import_payment_advice(source_file): + PaymentAdviceDocument.objects.filter(source_file=source_file).delete() + # parse and recreate child rows/documents +``` + +If the upload flow creates a new file and parsing fails, delete only that newly-created `SourceFile`. Do not delete/revert an existing source record unless you have an explicit rollback strategy. + +## UI behaviour + +Return counters that distinguish: +- imported files +- updated files +- skipped unchanged files +- failed files + +Use user-facing messages like: +- `1 uændret fil sprunget over` +- `1 fil opdateret` +- `23 filer importeret` + +This matters for accounting users: they need confidence that re-uploading a folder does not inflate totals. + +## Bulk upload settings + +Django defaults may reject large batches with `TooManyFilesSent`. For workflows where users upload many small settlement documents, make the limit explicit in settings/env, for example: + +```python +DATA_UPLOAD_MAX_NUMBER_FILES = int(os.getenv("DATA_UPLOAD_MAX_NUMBER_FILES", "2000")) +``` + +Add the setting to `.env.example` and document that it controls maximum files per HTTP upload. + +## Tests to add + +Minimum regression tests: + +1. Upload same file twice with identical bytes: + - `SourceFile.objects.count()` unchanged after second upload + - child document/line count unchanged + - response contains skipped/unchanged message + +2. Upload same file name with changed bytes: + - same `SourceFile` primary key reused + - `content_hash` changes + - old child rows are deleted/replaced, not appended + - response contains updated message + +3. Bulk upload many files if the UI supports multi-file import: + - no `TooManyFilesSent` + - all valid new files imported + - repeated files skipped + +4. Parser failure path: + - failed new file does not leave orphan `SourceFile` + - failed update does not silently erase previous valid data + +## Documentation checklist + +Update README/import docs with: +- duplicate uploads are skipped by content hash +- same file name with changed content updates the existing source +- updates replace parsed rows for that source file +- original source files are not edited in place; imported copies live under media/storage +- bulk upload file-count limit and env var diff --git a/skills/data-science/erp-data-reconciliation/references/reconciliation-webapp-architecture.md b/skills/data-science/erp-data-reconciliation/references/reconciliation-webapp-architecture.md new file mode 100644 index 0000000..4455f19 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/reconciliation-webapp-architecture.md @@ -0,0 +1,167 @@ +# Reconciliation Webapp Architecture (Django) + +Reference architecture for building a webapp on top of a reconciliation engine. +Based on Coop-Uniconta project (Radix), May 2026. + +## Stack + +- **Backend**: Django 4.2 + Django REST Framework +- **Database**: MariaDB 11.4 +- **Cache/Queue**: Redis + Celery (for background matching) +- **Frontend**: Django Templates + vanilla JS (no React needed for internal tools) +- **Deployment**: Docker Compose (web, db, redis) +- **Auth**: Django built-in + admin, later MS Entra/OIDC + +## Project Structure + +``` +backend/ + config/ + settings/base.py # Shared config + settings/local.py # Dev overrides (DEBUG=True, admin enabled) + urls.py # URL routing + wsgi.py / asgi.py # Entry points + celery.py # Celery app + apps/ + core/ # Models: Transaction, MatchResult, InvoiceStatus, AuditLog, BonusRule + models.py + admin.py + matching.py # 5-phase match engine + import_/ # Excel parsers + parsers.py # Coop + Uniconta import with preview + reconciliation/ # Views: dashboard, import, matching, reports + views.py + templates/ + templates/ + base.html # Dark-themed layout (sidebar nav + main content) + static/ + css/app.css # Dark theme: --bg-primary: #0f172a, --accent: #3b82f6 + manage.py + entrypoint.sh # wait-for-db + migrate + runserver + Dockerfile # python:3.12-slim + requirements.txt +``` + +## Data Model + +``` +AccountingYear (year, start_date, end_date) +SourceFile (source, file_name, file_path, row_count, parsed_count, error_count, accounting_year) +Transaction (source_file, source, year, month, date, amount, abs_amount, + doc_type, reference, faktura, text, text_normalized, + konteringstype, bilag, doc_number, + match_status, match_type, match_score, match_group, + original_data JSON) +MatchResult (group_id, match_type, score, status, + transaction_a, transaction_b, additional_b_ids JSON, + amount_diff, date_diff_days, explanation, comment, + approved_by, approved_at) +InvoiceStatus (faktura_number, accounting_year, invoice_total, paid_total, remaining, + status, invoice_date, first_invoice_text, + coop_payment_count, uniconta_payment_count, invoice_count) +AuditLog (action, user, match_result, details JSON) +BonusRule (name, year, period_start, period_end, conditions JSON, percentage, min_amount) +BonusCalculation (rule, transaction, calculated_amount, status) +``` + +## Key Design Decisions + +1. **Store original_data as JSON** — Always preserve raw Excel row data for traceability. +2. **Separate `amount` and `abs_amount`** — `amount` keeps original sign for net calculations; `abs_amount` for matching. +3. **MatchResult captures both sides** — Primary match (transaction_a → transaction_b) + optional additional_b_ids for many-to-one. +4. **InvoiceStatus is computed, not stored per-transaction** — Recalculated after each matching run. +5. **AuditLog for every manual action** — Match approval, rejection, bonus adjustment. + +## Match Engine Integration + +The match engine runs as a Celery task triggered from the web UI: + +```python +# apps/core/matching.py +AMT_TOL = Decimal("5.00") +DATE_TOL_REF = 21 +DATE_TOL_GEN = 21 + +def run_matching(accounting_year=None): + # Phase 1: Reference match (RE↔Faktura, RG↔Kreditnota) + # Phase 2: ZV↔Faktura + # Phase 3: RE↔Betaling without reference + # Phase 4: General abs(amount)+date + # Phase 5: Many-to-one (sum of 2-3 Uni = 1 Coop) + # Bulk create MatchResult, bulk update Transaction statuses +``` + +## UI Pages + +| Page | Purpose | +|------|---------| +| Dashboard | Year selector, stat cards, invoice status summary, monthly overview | +| Import | Upload Excel, show preview, map columns if auto-detection fails | +| Reconciliation List | Filterable table of all transactions, paginated (50/page) | +| Transaction Detail | Raw data, normalized data, potential matches, match history | +| Faktura Status | All invoices with BETALT/DELVIST/UBETALT, filterable | +| Reports | By type, by month, match summary, export to Excel | +| Run Match | Trigger background matching for selected year | + +## Dark Theme CSS Variables + +```css +:root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --accent: #3b82f6; + --success: #22c55e; + --warning: #f59e0b; + --danger: #ef4444; + --border: #334155; + --radius: 8px; +} +``` + +Inspired by Radix-ERP visual style: sidebar navigation, card-based stats, +data tables with badges, filter bars above tables. + +## Docker Compose + +```yaml +services: + web: + build: ./backend + command: ["web-dev"] + ports: ["8000:8000"] + depends_on: + db: {condition: service_healthy} + env_file: [.env] + db: + image: mariadb:11.4 + environment: + MARIADB_DATABASE: ${DB_NAME} + MARIADB_USER: ${DB_USER} + MARIADB_PASSWORD: ${DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + redis: + image: redis:7-alpine +``` + +## Deployment Notes + +- `python manage.py migrate` runs on container startup via entrypoint.sh +- `python manage.py createsuperuser` for first admin login +- Uploads stored in MEDIA_ROOT (mounted volume in production) +- Static files collected via `collectstatic` for production (nginx) + +## Next Steps for a New Reconciliation Project + +1. Copy project structure from template +2. Adapt parsers for the specific Excel formats +3. Run analysis script to determine match strategy +4. Configure AMT_TOL and DATE_TOL based on data quality +5. Build import UI with column mapping fallback +6. Implement match engine phases iteratively +7. Add manual match/approve/reject with audit log +8. Add reports and bonus calculation framework diff --git a/skills/data-science/erp-data-reconciliation/references/three-way-django-analysis-engine.md b/skills/data-science/erp-data-reconciliation/references/three-way-django-analysis-engine.md new file mode 100644 index 0000000..cb39262 --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/references/three-way-django-analysis-engine.md @@ -0,0 +1,146 @@ +# Three-Way Django Analysis Engine — Coop ↔ Uniconta ↔ Bank + +Session-specific implementation notes for building a maintainable three-source reconciliation layer in a Django-based internal finance app. + +## When to use this pattern + +Use when the task has three sources with different semantic roles: + +- ERP/accounting ledger, e.g. Uniconta = invoiced/booked postings +- Counterparty/customer statement, e.g. Coop kontoudtog = settlement/detail source +- Bank CSV = actual cash movement / reality check + +Do **not** treat the result as a legal claim automatically. The engine should produce review flags and explainable evidence. + +## Recommended architecture + +Keep the analysis engine separate from import parsing and HTTP views: + +```text +apps/core/parsers.py # source-specific import/normalization +apps/core/three_way.py # pure-ish analysis functions +apps/core/management/commands/*.py # import/analyze CLI commands +apps/reconciliation/views.py # dashboard/report presentation only +apps/reconciliation/templates/... # HTML tables/cards +``` + +Good public functions in the analysis module: + +```python +analyze_three_way_year(year) +build_monthly_summary(year) +build_invoice_statuses(year) +find_unexplained_bank_payments(year) +build_bonus_adjustment_summary(year) +``` + +Views should call these functions and render results; avoid embedding reconciliation logic in templates or views. + +## Batch import command + +For folder-based source projects, add a deterministic command that imports all configured source directories: + +```bash +python manage.py import_all_sources --base-dir /app/source_data +``` + +Expected source layout: + +```text +Fra Coop/ +Fra Uniconta/ +Fra bank/ +``` + +The command should: + +1. Walk each known directory. +2. Infer source from directory, not filename alone. +3. Preserve original file, sheet and row identifiers. +4. Store row-level import errors. +5. Print counts per file and total imported/error counts. +6. Be safe to rerun or clearly document duplicate behavior. + +## Three-way reporting outputs + +A useful first report page should show: + +- possible amounts the counterparty may owe (`Mulige beløb Coop skylder`) +- unexplained bank postings (`Uforklarede bankposter`) +- invoice statuses analyzed +- months with material differences +- month-level totals across all three sources +- bonus/credit/offset summary by period + +For each proposed issue, include evidence: + +- amount difference +- relevant date range +- invoice/reference, if present +- source counts +- which fields matched and which did not +- score/status and whether manual review is required + +## Interpretation pitfall + +Large totals such as “possible amount owed” are **analysis flags**, not final claims. + +Reasons: + +- One bank payment can cover many Coop/Uniconta postings. +- One Coop settlement can cover many invoices. +- Dates may differ between invoice date, statement date, settlement date and bank date. +- Bonus/credit notes may be non-cash accruals or period-based offsets. +- Open-post lists are status snapshots and should be treated separately from ledger/history. + +The UI and README should label these as review candidates until group matching and manual approval confirm them. + +## Matching strategy after basic import + +Start with conservative layers: + +1. Invoice/reference + absolute amount + date tolerance. +2. Remaining amount + date tolerance. +3. Month-level totals across all three sources. +4. Many-to-one and one-to-many group matching. +5. Bonus/credit period reconciliation. +6. Manual approval/rejection with audit log. + +For bank matching, prioritize group matching: + +```text +bank payment = sum of many Coop settlement lines = sum of many Uniconta invoices +``` + +Use the bank as cash-flow validation, not necessarily as a row-level join target. + +## Tests to include + +Minimum tests for this class of feature: + +- invoice is paid when Uniconta, Coop and bank agree +- invoice is flagged when Coop/bank evidence indicates a residual amount +- bank payment without Coop/Uniconta support is flagged as unexplained +- monthly summary calculates Bank-Coop and Bank-Uniconta differences correctly + +For Django, add a lightweight test settings module if the normal dev database requires services/permissions: + +```text +config/settings/test.py # SQLite, fast local tests +``` + +Then run: + +```bash +DJANGO_SETTINGS_MODULE=config.settings.test python manage.py test +``` + +## Documentation to update + +When this pattern is implemented, update: + +- README.md: installation, Docker, import flow, analysis commands, limitations +- ANALYSE.md or ANALYSE_TREVEJS.md: source findings and interpretation +- HANDOFF.md: current status and next recommended steps +- TODO.md/ROADMAP.md: group matching, manual review, bonus expansion + diff --git a/skills/data-science/erp-data-reconciliation/scripts/excel-analyzer.py b/skills/data-science/erp-data-reconciliation/scripts/excel-analyzer.py new file mode 100644 index 0000000..1fa8f8b --- /dev/null +++ b/skills/data-science/erp-data-reconciliation/scripts/excel-analyzer.py @@ -0,0 +1,211 @@ +""" +Generic Excel file analyzer for ERP reconciliation projects. + +Usage: + python analyze_excel.py [--output analysis.json] + +This script: +1. Scans a directory for .xlsx files +2. Identifies sheets, headers, data ranges +3. Detects Danish number formats +4. Prints null counts and sample values +5. Saves structured JSON for downstream processing + +To customize for a project, adjust: +- EXPECTED_COLS: columns that must exist for a row to be considered a header +- AMOUNT_COL_PATTERNS: regex patterns for amount columns +- DATE_COL_PATTERNS: regex patterns for date columns +""" +import pandas as pd +import openpyxl +import os +import json +import re +from collections import OrderedDict + +# --- CUSTOMIZE PER PROJECT --- +EXPECTED_COLS = {'Dato', 'Faktura', 'Bilag', 'Tekst', 'Beløb'} +AMOUNT_COL_PATTERNS = [ + r'(?i)bel[oø]b', + r'(?i)amount', + r'(?i)sum', + r'(?i)rest', + r'(?i)forfalden', +] +DATE_COL_PATTERNS = [ + r'(?i)dato', + r'(?i)date', + r'(?i)posting', + r'(?i)entry', +] +REFERENCE_COL_PATTERNS = [ + r'(?i)reference', + r'(?i)faktura', + r'(?i)bilag', + r'(?i)document number', + r'(?i)invoice', +] +# --- END CUSTOMIZE --- + + +def classify_column(col_name): + """Guess the semantic type of a column from its name.""" + cn = str(col_name) + for p in AMOUNT_COL_PATTERNS: + if re.search(p, cn): + return 'amount' + for p in DATE_COL_PATTERNS: + if re.search(p, cn): + return 'date' + for p in REFERENCE_COL_PATTERNS: + if re.search(p, cn): + return 'reference' + return 'other' + + +def find_header_row(filepath, sheet=0, required_cols=None): + """Find the first row that looks like a header.""" + if required_cols is None: + required_cols = EXPECTED_COLS + wb = openpyxl.load_workbook(filepath, data_only=True) + ws = wb[wb.sheetnames[sheet]] + for i, row in enumerate(ws.iter_rows(values_only=True), start=0): + vals = set(str(v) for v in row if v is not None) + if required_cols.issubset(vals): + return i + return None + + +def parse_danish_number(val): + """Normalize Danish text numbers like '-2.543.803,93' to float.""" + if pd.isna(val): + return float('nan') + if isinstance(val, (int, float)): + return float(val) + s = str(val).strip() + if not s: + return float('nan') + # Danish format: thousands sep = '.', decimal = ',' + if '.' in s and ',' in s: + s = s.replace('.', '').replace(',', '.') + elif ',' in s: + s = s.replace(',', '.') + try: + return float(s) + except ValueError: + return float('nan') + + +def analyze_file(filepath): + info = { + "filepath": filepath, + "filename": os.path.basename(filepath), + "size_kb": round(os.path.getsize(filepath) / 1024, 1), + "sheets": OrderedDict(), + } + xl = pd.ExcelFile(filepath) + for sheet in xl.sheet_names: + info["sheets"][sheet] = analyze_sheet(filepath, sheet) + return info + + +def analyze_sheet(filepath, sheet): + # Try default header=0 first; if columns look wrong, try header detection + df = pd.read_excel(filepath, sheet_name=sheet, engine='openpyxl') + header_row = None + # Simple heuristic: if ALL columns are unnamed, try detecting header + unnamed = [c for c in df.columns if 'Unnamed' in str(c)] + if len(unnamed) > len(df.columns) * 0.5: + hr = find_header_row(filepath, sheet) + if hr is not None: + df = pd.read_excel(filepath, sheet_name=sheet, engine='openpyxl', header=hr) + header_row = hr + + cols = OrderedDict() + for col in df.columns: + col_type = classify_column(str(col)) + is_date = pd.api.types.is_datetime64_any_dtype(df[col]) + is_num = pd.api.types.is_numeric_dtype(df[col]) + null_count = int(df[col].isna().sum()) + unique_count = int(df[col].nunique()) + samples = [] + for v in df[col].dropna().head(5): + if len(samples) >= 3: + break + try: + if is_date: + samples.append(str(pd.Timestamp(v).date())) + else: + samples.append(str(v)[:60]) + except Exception: + samples.append(str(v)[:60]) + + # Detect danish number format + danish_flag = False + if is_num: + for v in df[col].dropna().head(10): + if isinstance(v, str) and (',' in v or ('.' in v and ',' in v.replace('.', ''))): + danish_flag = True + break + + cols[str(col)] = { + "semantic_type": col_type, + "pandas_dtype": str(df[col].dtype), + "null_count": null_count, + "null_pct": round(null_count / len(df) * 100, 1), + "unique_count": unique_count, + "is_date": bool(is_date), + "is_numeric": bool(is_num), + "danish_number_format": danish_flag, + "sample_values": samples, + } + + empty_rows = int((df.isna().all(axis=1)).sum()) + dupes = int(df.duplicated().sum()) + + # Try to find date range + date_range = None + for col, meta in cols.items(): + if meta["is_date"]: + non_null = df[col].dropna() + if len(non_null) > 0: + date_range = { + "min": str(non_null.min().date()), + "max": str(non_null.max().date()), + "column": col, + } + break + + return { + "rows": len(df), + "columns": len(df.columns), + "detected_header_row": header_row, + "empty_rows": empty_rows, + "duplicate_rows": dupes, + "date_range": date_range, + "columns": cols, + } + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Analyze Excel files for data reconciliation") + parser.add_argument("directory", help="Directory containing .xlsx files") + parser.add_argument("--output", default="excel_analysis.json", help="Output JSON file") + args = parser.parse_args() + + results = OrderedDict() + for f in sorted(os.listdir(args.directory)): + if f.endswith('.xlsx') and not f.startswith('~'): + fp = os.path.join(args.directory, f) + key = os.path.splitext(f)[0] + results[key] = analyze_file(fp) + print(f"Analyzed: {f}") + + with open(args.output, 'w', encoding='utf-8') as fh: + json.dump(results, fh, indent=2, ensure_ascii=False) + print(f"\nSaved: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/skills/devops/gitea-issue-agent/SKILL.md b/skills/devops/gitea-issue-agent/SKILL.md new file mode 100644 index 0000000..4755fc3 --- /dev/null +++ b/skills/devops/gitea-issue-agent/SKILL.md @@ -0,0 +1,248 @@ +--- +name: gitea-issue-agent +description: Build a Hermes-managed autonomous issue handler for a self-hosted Gitea instance — scan issues, analyze text + screenshot attachments, post a proposal for human approval, implement, test, push a branch, and open a Gitea PR. Use when the user wants Hermes to act on Gitea issues end-to-end with a human-in-the-loop approval gate, especially with image/visual context in the issue text. +version: 0.1.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [gitea, issues, automation, hermes-plugin, pr-workflow, agent-loop, approval-gate] + related_skills: [gitea-tea-macos-setup, software-engineering-workflows] +--- + +# Gitea Issue Agent + +End-to-end workflow for an autonomous Gitea issue handler that lives inside +Hermes. Combines: + +- A **Hermes plugin** exposing Gitea REST API tools (status, scan, get, fetch + images, comment, create PR). +- A **skill** that orchestrates the human-in-the-loop loop: propose → wait + for approval → implement → test → push → open PR. +- An optional **cron job** that triggers the loop on a schedule. + +## When to use + +- User wants Hermes to monitor issues across one or more Gitea repos and + propose fixes autonomously. +- Issue text often includes screenshots or design mock-ups (image support + is a first-class requirement). +- A human (the user) must approve each fix before code is written. +- Push and PR creation should target a working branch the user can review + and merge manually. + +Do **not** use for: GitHub (use the `gitea`-equivalent catalog MCP for that), +issues that are just questions/discussions (no code work expected), or +workflows where the user wants fully autonomous implementation with no +human gate. + +## Architecture + +``` + ┌────────────────────────────────────────┐ + │ ~/.hermes/plugins/gitea-issue-agent/ │ + Cron tick → │ ├ plugin.yaml │ + │ ├ __init__.py (registers 7 tools) │ + │ └ README.md │ + └─────────────┬──────────────────────────┘ + │ exposes + ▼ + gitea_agent_status gitea_issue_agent_scan + gitea_list_repositories gitea_get_issue + gitea_fetch_issue_images gitea_comment_issue + gitea_create_pull_request + │ consumed by + ▼ + ┌────────────────────────────────────────┐ + │ Hermes skill gitea-issue-agent-loop │ + │ (orchestrates: scan → propose → wait │ + │ → implement → test → push → PR) │ + └─────────────┬──────────────────────────┘ + │ triggers + ▼ + ┌────────────────────────────────────────┐ + │ Cron job (optional) "every 30m" │ + │ "Scan Gitea issues, propose solutions │ + │ for new ones, implement approved." │ + └────────────────────────────────────────┘ +``` + +The plugin does **not** implement the loop — it only exposes the API +surface. The loop lives in the skill and the cron prompt. This separation +keeps the plugin small and lets the loop evolve without code changes to +the plugin. + +## Required environment variables + +Add to `~/.hermes/.env` and restart the gateway: + +```bash +GITEA_BASE_URL=https://git.example.dk +GITEA_TOKEN=*** service-bruger PAT, scope: read+write issues/PRs/attachments> +GITEA_APPROVER_LOGIN= +GITEA_DEFAULT_BASE_BRANCH=main +# Optional: comma-separated allowlist. If empty, /user/repos is scanned. +GITEA_REPOS=owner/repo1,owner/repo2 +GITEA_AGENT_REPO_LIMIT=200 +GITEA_AGENT_ISSUE_LIMIT_PER_REPO=50 +``` + +Two Gitea identities, kept separate: + +- `GITEA_TOKEN` → service-bruger account (e.g. `hermes-issues`). Posts + comments and PRs as a bot. +- `tea` CLI → the **user's personal** token, e.g. `dennis`. Used for + local git push of branches the user owns. + +Mixing them causes comments to look like they came from the human +personally. See `references/identity-and-safety.md`. + +## Tool set (the plugin registers these) + +| Tool | Purpose | +| --- | --- | +| `gitea_agent_status` | Verify config + API connectivity; reports `authenticated_user`. | +| `gitea_list_repositories` | List repos visible to the token (or `GITEA_REPOS` allowlist). | +| `gitea_issue_agent_scan` | Scan open issues, classify each as `new` / `awaiting_approval` / `approved` / `pr_ready`. Returns short summary + image count. | +| `gitea_get_issue` | Fetch one issue + comments + image links + classification status. | +| `gitea_fetch_issue_images` | Download all screenshots/attachments from the issue + comments to a local cache, so `vision_analyze` can inspect them. | +| `gitea_comment_issue` | Post a comment; optional `marker` arg (`proposal` or `pr_ready`) prepends an HTML marker the scanner uses to track state. | +| `gitea_create_pull_request` | Open a PR from an already-pushed branch. The branch must be created and pushed by the agent via `git`/`terminal` first. | + +Every tool returns JSON of the shape `{"success": true, ...}` or +`{"success": false, "error": "..."}`. JSON is the only contract; the +agent's loop should branch on `success` and surface `error` verbatim. + +## The workflow (the loop the skill/cron implements) + +1. **Scan**: `gitea_issue_agent_scan` to find `status == "new"` issues. +2. **For each new issue**: + 1. `gitea_get_issue` for full text + comments. + 2. `gitea_fetch_issue_images` to download any screenshots to + `~/.hermes/cache/gitea-issue-agent////`. + 3. `vision_analyze` on each image (loaded into context) so the + proposal is grounded in what the screenshot actually shows. + 4. Draft a proposal (in Danish if the user is Danish-speaking) covering: + - summary of the bug/feature + - root-cause hypothesis + - proposed change (files, tests, branch name) + - explicit approval request + 5. `gitea_comment_issue` with `marker="proposal"`. The plugin + prepends the `` + marker so the next scan classifies this issue as `awaiting_approval`. +3. **Stop and wait for approval.** Do not implement. The skill exits + cleanly; the cron re-scans later and the human will have either: + - Posted an approval comment (matched by `APPROVAL_RE` against + `GITEA_APPROVER_LOGIN`), or + - Applied one of the approval labels (`hermes-approved`, + `approved-by-dennis`, `dennis-approved`). +4. **On the next scan**, issues that satisfy the approval rule are + classified as `approved`. The skill then: + 1. `cd` into the local clone of the repo (clone it first via + `git clone` if not present — `tea clone` will fail on non-standard + SSH ports, see `gitea-tea-macos-setup`). + 2. Create a branch `hermes/issue--`. + 3. Implement, run tests, commit, push (with token in URL or via + `~/.netrc`). + 4. `gitea_create_pull_request` with `head=hermes/issue--`, + `base=main`, `body` linking to the issue, `draft=true`. + 5. `gitea_comment_issue` with `marker="pr_ready"` and the PR URL. +5. **Stop.** The user reviews the PR in Gitea and merges by hand. + +The two state markers (`proposal`, `pr_ready`) are the only state +machine. They survive Gitea restarts, plugin reloads, and Hermes +restarts, because they live in the issue's comment history. + +## Approval semantics (default) + +- A comment from `GITEA_APPROVER_LOGIN` matching + `(?is)(@?hermes|agent|bot).{0,80}(godkend|approved|ok|go)` counts as + approval. Danish is first-class because the user is Danish-speaking. +- Any of these labels applied by a maintainer also counts: + `hermes-approved`, `approved-by-dennis`, `dennis-approved`. +- Approval is only counted if it is **after** the latest `proposal` + comment. Re-approving an old proposal does nothing. +- If `GITEA_APPROVER_LOGIN` is not set, free-form approval comments are + ignored — only labels count. This is the safe default until the user + has decided which login should be allowed to approve. + +For a longer rationale, see `references/identity-and-safety.md`. + +## Install checklist (do these in order) + +1. Decide on a service-bruger name (e.g. `hermes-issues`) and create a + Gitea PAT for it with `repository` (read+write) + `issue` (write) scope. +2. Tell the user the env vars they need to set in `~/.hermes/.env` (see + above). The agent **cannot** edit `.env` itself — Hermes blocks it + at the tool layer. See `references/identity-and-safety.md` for why. +3. Drop the plugin files into `~/.hermes/plugins/gitea-issue-agent/` + (`plugin.yaml` + `__init__.py` + `README.md`). +4. Tell the user to add `- gitea-issue-agent` under `plugins.enabled` + in `~/.hermes/config.yaml`. The agent **cannot** edit `config.yaml` + either — same reason. +5. Restart the gateway: `hermes gateway restart`. +6. Verify with `hermes chat -q "Kald gitea_agent_status" -t gitea_issue_agent` + — `api_ok: true`, `authenticated_user: hermes-issues` (or whatever the + service-bruger is named). +7. Verify the local CLI path: `tea login add --name --url ...`, + `tea whoami`, then `git clone` any visible repo (don't rely on + `tea clone` — see `gitea-tea-macos-setup` for the SSH-port-2224 trap). +8. Only then create the cron job, and only after the user has + approved running it unattended. + +## Verification + +Use the bundled script `scripts/verify-plugin-and-gitea.sh` to confirm +the plugin is loaded, the API is reachable, and at least one repo is +visible. It should print `OK` lines and exit 0. + +## Pitfalls (learned) + +- **Hermes blocks the agent from writing to `~/.hermes/config.yaml` and + `~/.hermes/.env`.** The right move when blocked is to give the user + the exact one-line edit and stop. Do not try `execute_code`, `patch`, + or `write_file` a different way — the guard fires on all of them. + See `references/identity-and-safety.md`. +- **`tea clone` fails opaquely when SSH is on a non-standard port + (e.g. 2224).** Use `git clone` with the HTTPS `clone_url` from the + Gitea API. Strip the token from `origin` immediately. +- **Stale `osxkeychain` credentials can sabotage `git push` even when + the token is correct.** Switch to `~/.netrc` with `chmod 600`. See + the troubleshooting reference in `gitea-tea-macos-setup`. +- **Service-bruger identity matters.** The plugin's `authenticated_user` + field should NOT be your personal login. If it is, create a separate + service-bruger or you'll get bot comments attributed to you. +- **Free-form approval comments must be gated by a known login** (or + by maintainer-applied labels) — otherwise a co-worker saying "looks + good!" could trigger an autonomous push. +- **First run: verify, don't cron.** Always run a manual scan + a + manually-approved test issue before scheduling cron. A cron job + firing off PRs in the user's name is the failure mode to avoid. +- **Image analysis is mandatory for screenshot issues.** The plugin + extracts image URLs and downloads them locally; the skill MUST call + `vision_analyze` on each before writing the proposal, otherwise the + proposal will be blind to what the screenshot shows. +- **Plugin state is in issue comments, not in plugin memory.** The + proposal/PR markers make the workflow restart-safe. + +## Related skills + +- `gitea-tea-macos-setup` — the local `tea` + `git` + `osxkeychain` + setup that the agent relies on. Includes the SSH-port-2224 trap and + the `getlogin(2)` gotcha. +- `software-engineering-workflows` — the plan / spike / TDD / debugging + patterns the implement-and-test phase of the loop should follow. +- `software-development/simplify-code` — useful to invoke on the + agent's own code changes before opening the PR. + +## Files in this skill + +- `scripts/verify-plugin-and-gitea.sh` — one-shot verification of the + full setup (plugin loaded + API reachable + repos visible + clone + works). Run it after every change to the env or plugin. +- `references/identity-and-safety.md` — service-bruger vs personal + identity split, Hermes safety-guard reality, approval-gate design + rationale. +- `templates/proposal-comment.md` — starter template for the + `gitea_comment_issue` body when posting a proposal. Copy, fill in + the issue-specific bits, send. diff --git a/skills/devops/gitea-issue-agent/references/identity-and-safety.md b/skills/devops/gitea-issue-agent/references/identity-and-safety.md new file mode 100644 index 0000000..eb33872 --- /dev/null +++ b/skills/devops/gitea-issue-agent/references/identity-and-safety.md @@ -0,0 +1,101 @@ +# Identity and safety in the Gitea issue agent + +## Two identities, one Gitea server + +The setup has two Gitea users that should remain separate: + +| Identity | Token stored in | Used for | What it posts | +| --- | --- | --- | --- | +| **Service-bruger** (e.g. `hermes-issues`) | `GITEA_TOKEN` in `~/.hermes/.env` | All plugin tool calls: scan, comment, create PR | Comments and PRs attributed to the bot | +| **Personal user** (e.g. `dennis`) | `tea` login + `~/.netrc` | Local `git push` of branches the agent created | Commits and merges attributed to you | + +The plugin's `gitea_agent_status` reports `authenticated_user`. If it +reports your personal login, the bot will post comments as you — fix +the `GITEA_TOKEN` before scheduling any cron job. + +## Why the Hermes safety-guards exist (and why the agent can't bypass them) + +Hermes has three layers of "agent cannot edit" protection, all by +design: + +1. **`~/.hermes/config.yaml`** — gates plugin load, model selection, + toolsets, gateway config. An agent that could rewrite this could + give itself new tools, change providers, or open outbound network + channels. The `patch` tool explicitly refuses: + `Refusing to write to Hermes config file: ... Agent cannot modify + security-sensitive configuration.` + +2. **`~/.hermes/.env`** — contains API keys, tokens, passwords. The + `write_file`, `patch`, and `execute_code` tools all refuse to + modify it. + +3. **`execute_code` with file writes** — when a script is going to + write files under `~/.hermes/`, the safety guard blocks it + unconditionally: `BLOCKED: execute_code script timed out without + user response.` + +**The right move when blocked:** give the user the exact one-line +edit (or `cat > file <= 5, f"expected at least 5 tools, got {len(mod.SCHEMAS)}" +PY + +echo +echo "=== 4. Env vars present ===" +set +e +. "$HOME/.hermes/.env" 2>/dev/null +set -e +[ -n "${GITEA_BASE_URL:-}" ] && ok "GITEA_BASE_URL=$GITEA_BASE_URL" || bad "GITEA_BASE_URL missing in ~/.hermes/.env" +[ -n "${GITEA_TOKEN:-}" ] && ok "GITEA_TOKEN set (${#GITEA_TOKEN} chars)" || bad "GITEA_TOKEN missing in ~/.hermes/.env" + +echo +echo "=== 5. API connectivity ===" +HTTP=$(curl -4 -s -o /dev/null -m 10 -w "%{http_code}" \ + -H "Authorization: token $GITEA_TOKEN" \ + "$GITEA_BASE_URL/api/v1/user") +if [ "$HTTP" = "200" ]; then + USER=$(curl -4 -s -m 10 -H "Authorization: token $GITEA_TOKEN" \ + "$GITEA_BASE_URL/api/v1/user" | python3 -c "import sys,json; print(json.load(sys.stdin).get('login','?'))") + ok "API reachable, authenticated as: $USER" +else + bad "API $GITEA_BASE_URL/api/v1/user returned HTTP $HTTP" +fi + +echo +echo "=== 6. At least one repo visible ===" +N=$(curl -4 -s -m 10 -H "Authorization: token $GITEA_TOKEN" \ + "$GITEA_BASE_URL/api/v1/user/repos?limit=100" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))") +[ "$N" -gt 0 ] && ok "$N repos visible to token" || bad "no repos visible — token scope may be too narrow" + +echo +echo "=== 7. local tea login (optional but recommended) ===" +if command -v tea >/dev/null 2>&1; then + TUSER=$(tea whoami 2>/dev/null | grep -E "^[a-zA-Z0-9_-]+$" | head -1) + [ -n "$TUSER" ] && ok "tea whoami -> $TUSER" || echo " INFO tea installed but no login yet" +else + echo " INFO tea not on PATH — see gitea-tea-macos-setup" +fi + +echo +echo "=== 8. ~/.netrc (optional but recommended) ===" +if [ -f "$HOME/.netrc" ] && grep -q "git.radixadm.dk\|$GITEA_BASE_URL" "$HOME/.netrc"; then + ok "~/.netrc has an entry for $GITEA_BASE_URL" +else + echo " INFO ~/.netrc missing — git push will use osxkeychain (may be stale)" +fi + +echo +echo "All checks passed. The agent is ready to use." diff --git a/skills/devops/gitea-issue-agent/templates/proposal-comment.md b/skills/devops/gitea-issue-agent/templates/proposal-comment.md new file mode 100644 index 0000000..ba7d003 --- /dev/null +++ b/skills/devops/gitea-issue-agent/templates/proposal-comment.md @@ -0,0 +1,40 @@ + + +## Analyse + + + +## Screenshots / visuelle referencer + + + +## Rod-hypotese + +<1-3 bullets der peger på den underliggende årsag, ikke symptomet> + +## Foreslået ændring + +- **Repo:** `/` +- **Branch:** `hermes/issue--` +- **Filer der forventes ændret:** + - `` — + - `` — ny test der dækker regressionen +- **Risiko:** lav / medium / høj — + +## Test-plan + +- `docker compose exec web python manage.py test ` skal køre grønt +- Manuel verify: +- Hvis relevant: + +## Spørgsmål til Dennis + +- + +## Godkend + +Hvis du vil have mig til at implementere det her, så svar med: + +> @hermes godkend + +(eller sæt label `hermes-approved` på issuet) diff --git a/skills/devops/kanban-workflows/SKILL.md b/skills/devops/kanban-workflows/SKILL.md new file mode 100644 index 0000000..13ea78f --- /dev/null +++ b/skills/devops/kanban-workflows/SKILL.md @@ -0,0 +1,56 @@ +--- +name: kanban-workflows +description: "Use when operating Hermes Kanban workflows: decomposing work as an orchestrator, executing as a worker, managing lanes, status, blockers, and reconciliation." +version: 1.0.0 +author: Hermes Agent +license: MIT +metadata: + hermes: + tags: [kanban, orchestration, workers, task-management, hermes] + related_skills: [autonomous-coding-agents] +--- + +# Kanban Workflows Umbrella + +## Overview + +Kanban work has two sides: orchestrators decompose and route work without doing it themselves; workers execute a scoped card, keep status current, and return verifiable evidence. Use this umbrella for both roles. + +## When to Use + +- Acting as a Hermes Kanban orchestrator. +- Acting as a Hermes Kanban worker. +- Deciding whether to split, assign, block, or reconcile cards. +- Handling worker pitfalls, examples, and edge cases. + +## Role Playbooks + +### Orchestrator + +Decompose outcomes into cards small enough for one worker, include acceptance criteria, and avoid the temptation to implement the work directly. Route work, unblock workers, and reconcile results. + +### Worker + +Own exactly the assigned card. Update status when starting, blocked, or done. Run the requested checks and return concise evidence: files changed, commands run, output, and remaining risks. + +### Lanes and external agents + +If a worker uses an external coding agent lane, keep Kanban as the source of truth and verify the lane's output before closing the card. + +## Common Pitfalls + +1. Orchestrators doing implementation work themselves. +2. Workers broadening scope beyond the card. +3. Closing cards from self-reports without independent verification. +4. Losing blocker state instead of surfacing it promptly. + +## Package Notes + +Archived source skills retain the deeper role-specific text: `kanban-orchestrator` and `kanban-worker`. + +## Verification Checklist + +- [ ] Card scope and acceptance criteria are explicit. +- [ ] Exactly one owner is responsible for each action. +- [ ] Status reflects reality: pending, in progress, blocked, done. +- [ ] Completion includes verifiable evidence.