Initial import: 10 Radix skills across 3 categories
Kategoriserer danske regnskabs-/ERP-skills, ERP-data-afstemnings-
skill og to devops-workflows (gitea-issue-agent, kanban-workflows)
som Radix-medlemmers AI-agenter kan dele.
Struktur:
skills/<kategori>/<skill-navn>/SKILL.md
skills/<kategori>/<skill-navn>/{references,templates,scripts}/
Indekseret af index.json (genereret af scripts/rebuild_index.py).
Kør `python3 scripts/rebuild_index.py --check` i CI for at fange
synkroniseringsfejl.
This commit is contained in:
+25
@@ -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
|
||||||
@@ -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/<kategori>/<navn>/` 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/<kategori>/<skill-navn>/` 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`.
|
||||||
+215
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
@@ -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
|
||||||
@@ -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: <UUID v4>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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` | `<unik UUID>` | 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: <UUID v4>
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -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
|
||||||
@@ -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.*
|
||||||
@@ -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
|
||||||
@@ -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.*
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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 `<option value="bank">Bank (Kontobevaegelser .csv)</option>`
|
||||||
|
- **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.
|
||||||
@@ -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
|
||||||
|
```
|
||||||
+109
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
+89
@@ -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.
|
||||||
@@ -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
|
||||||
+122
@@ -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.
|
||||||
+122
@@ -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
|
||||||
+167
@@ -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
|
||||||
+146
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"""
|
||||||
|
Generic Excel file analyzer for ERP reconciliation projects.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python analyze_excel.py <directory> [--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()
|
||||||
@@ -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=<the user's Gitea login, e.g. "dennis">
|
||||||
|
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/<owner>/<repo>/<index>/`.
|
||||||
|
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 `<!-- hermes:gitea-issue-agent v1 status=proposal -->`
|
||||||
|
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-<n>-<slug>`.
|
||||||
|
3. Implement, run tests, commit, push (with token in URL or via
|
||||||
|
`~/.netrc`).
|
||||||
|
4. `gitea_create_pull_request` with `head=hermes/issue-<n>-<slug>`,
|
||||||
|
`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 <user> --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.
|
||||||
@@ -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 <<EOF ... EOF` block) and stop. Do not try
|
||||||
|
alternative tools, do not retry, do not work around it. The user
|
||||||
|
must run the edit themselves.
|
||||||
|
|
||||||
|
This is not a bug. It is the threat model.
|
||||||
|
|
||||||
|
## Approval-gate design rationale
|
||||||
|
|
||||||
|
The agent should never implement, push, or open a PR for an issue
|
||||||
|
that hasn't been approved. This protects against three classes of
|
||||||
|
mistake:
|
||||||
|
|
||||||
|
1. **Misread issue** — the user wanted "fix the X button", the agent
|
||||||
|
rewrote `X()` in 14 files. Approval forces a human to look at the
|
||||||
|
proposal first.
|
||||||
|
2. **Out-of-scope issue** — the user wanted to push back, not fix.
|
||||||
|
Approval makes "no" cheap (just don't reply).
|
||||||
|
3. **Bad agent day** — the LLM hallucinated, the issue got a wrong
|
||||||
|
proposal, the agent is in a weird state. Approval makes the loop
|
||||||
|
recoverable without reverting commits.
|
||||||
|
|
||||||
|
The two state markers (`proposal`, `pr_ready`) survive Hermes
|
||||||
|
restarts, plugin reloads, and Gitea restarts, because they live in
|
||||||
|
the issue's comment history. The agent never has to remember
|
||||||
|
anything to resume — it can re-scan and pick up where it left off.
|
||||||
|
|
||||||
|
## Approval matching — why it requires a known login by default
|
||||||
|
|
||||||
|
The default approval regex is permissive enough to handle Danish
|
||||||
|
("godkend", "godkender") and English ("approve", "approved", "ok",
|
||||||
|
"go") in either order with the agent mention. But without a
|
||||||
|
known-login check, **any user** with write access to the repo could
|
||||||
|
say "looks good, agent" in a comment and trigger a push. That's a
|
||||||
|
prompt-injection vector — a malicious screenshot, or a co-worker
|
||||||
|
joking, could authorize code execution.
|
||||||
|
|
||||||
|
Two safe modes:
|
||||||
|
|
||||||
|
- **Label-only mode** (the default until `GITEA_APPROVER_LOGIN` is
|
||||||
|
set): a maintainer applies one of three labels. Labels can only
|
||||||
|
be applied by users with write access, so the trust surface is
|
||||||
|
the maintainer set, not "anyone who can comment".
|
||||||
|
- **Login-gated mode** (once `GITEA_APPROVER_LOGIN` is set): a
|
||||||
|
comment from that specific user, matching the regex, AND
|
||||||
|
after the latest proposal comment. This is the strict mode.
|
||||||
|
|
||||||
|
If the user wants both, both work — labels OR login-comment. If they
|
||||||
|
want neither (auto-implement everything), they should set both
|
||||||
|
`GITEA_APPROVER_LOGIN` and approve a *meta-issue* saying "always
|
||||||
|
implement" — but that's not the default and not recommended.
|
||||||
|
|
||||||
|
## The first run is always manual
|
||||||
|
|
||||||
|
Before scheduling a cron, the user must:
|
||||||
|
|
||||||
|
1. Run a manual `gitea_issue_agent_scan` and confirm issues show up.
|
||||||
|
2. Pick a real issue and walk it through: scan → fetch images →
|
||||||
|
analyze → propose → wait → approve → implement → test → push →
|
||||||
|
PR.
|
||||||
|
3. Confirm the PR body links to the issue, the commit message is
|
||||||
|
reasonable, and `git log` shows the expected author/email.
|
||||||
|
4. **Then** schedule a cron. The cron prompt itself should be
|
||||||
|
minimal — "scan, propose for new issues, implement approved" —
|
||||||
|
with a clear hand-off to the skill for the details.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Verify the Gitea issue agent is wired up end to end.
|
||||||
|
#
|
||||||
|
# Exits 0 on success. Prints OK / FAIL lines for each step.
|
||||||
|
# Run after every env or plugin change.
|
||||||
|
#
|
||||||
|
# Does NOT need a real token in the script — reads from ~/.hermes/.env.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ok() { echo " OK $*"; }
|
||||||
|
bad() { echo " FAIL $*"; exit 1; }
|
||||||
|
|
||||||
|
echo "=== 1. Plugin files present ==="
|
||||||
|
PLUGIN_DIR="$HOME/.hermes/plugins/gitea-issue-agent"
|
||||||
|
for f in plugin.yaml __init__.py README.md; do
|
||||||
|
[ -f "$PLUGIN_DIR/$f" ] && ok "$f" || bad "$PLUGIN_DIR/$f missing"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 2. Plugin listed in config.yaml ==="
|
||||||
|
grep -q "gitea-issue-agent" "$HOME/.hermes/config.yaml" \
|
||||||
|
&& ok "config.yaml references gitea-issue-agent" \
|
||||||
|
|| bad "gitea-issue-agent not in plugins.enabled — add it under plugins.enabled in ~/.hermes/config.yaml"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== 3. Plugin imports without error ==="
|
||||||
|
python3 - <<'PY' && ok "import gitea-issue-agent OK" || bad "import failed"
|
||||||
|
import importlib.util, sys
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"gitea_issue_agent",
|
||||||
|
f"{__import__('os').path.expanduser('~')}/.hermes/plugins/gitea-issue-agent/__init__.py",
|
||||||
|
)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules["gitea_issue_agent"] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
assert len(mod.SCHEMAS) >= 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."
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!-- hermes:gitea-issue-agent v1 status=proposal -->
|
||||||
|
|
||||||
|
## Analyse
|
||||||
|
|
||||||
|
<kort resumé af hvad issuet beder om — i dansk hvis brugeren er dansk>
|
||||||
|
|
||||||
|
## Screenshots / visuelle referencer
|
||||||
|
|
||||||
|
<bullet-liste over hvad hver screenshot viser og hvad vi har udledt af den>
|
||||||
|
|
||||||
|
## Rod-hypotese
|
||||||
|
|
||||||
|
<1-3 bullets der peger på den underliggende årsag, ikke symptomet>
|
||||||
|
|
||||||
|
## Foreslået ændring
|
||||||
|
|
||||||
|
- **Repo:** `<owner>/<repo>`
|
||||||
|
- **Branch:** `hermes/issue-<index>-<kort-slug>`
|
||||||
|
- **Filer der forventes ændret:**
|
||||||
|
- `<sti/til/fil.py>` — <hvad og hvorfor>
|
||||||
|
- `<sti/til/test_x.py>` — ny test der dækker regressionen
|
||||||
|
- **Risiko:** lav / medium / høj — <begrundelse>
|
||||||
|
|
||||||
|
## Test-plan
|
||||||
|
|
||||||
|
- `docker compose exec web python manage.py test <app>` skal køre grønt
|
||||||
|
- Manuel verify: <trin brugeren kan tjekke i UI>
|
||||||
|
- Hvis relevant: <evt. playwright/screenshot-verifikation>
|
||||||
|
|
||||||
|
## Spørgsmål til Dennis
|
||||||
|
|
||||||
|
- <evt. valg brugeren skal tage før implementering — f.eks. "skal eksisterende data migreres, eller kun nye records?">
|
||||||
|
|
||||||
|
## Godkend
|
||||||
|
|
||||||
|
Hvis du vil have mig til at implementere det her, så svar med:
|
||||||
|
|
||||||
|
> @hermes godkend
|
||||||
|
|
||||||
|
(eller sæt label `hermes-approved` på issuet)
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user