Developer Kit · Patterns
Logbook to Billing
Logbook to Billing (Zeiterfassung → Aufwandsberechnung)
AI Defaults
- Zwei getrennte Konzepte:
- `TimeEntry` (einzelner Zeiteintrag) und `BillingPeriod` (Abrechnungszeitraum mit Soll-Stunden/Budget). Nicht vermischen. -** `billingMode` pro Zeiteintrag** entscheidet den Ausgabepfad: `contingent` (zählt auf ein Kontingent), `billable_extra` (wird separat abgerechnet), `non_billable` (zählt nirgends).
- Zwei Ausgabepfade, kein Automatismus dazwischen:
- Stunden-Abrechnung (→ Rechnungsposition) und Budget/Kontingent-Nachweis (→ Aufwandsposition) sind unabhängige Auswertungen auf denselben `TimeEntry`-Daten — nicht ein einziger kombinierter Bericht.
- Kennzahlen sind reine Aggregationen
- (`sum`, `min`, `round`) auf gefilterten `TimeEntry`-Listen — kein separates Aggregat-Objekt in der Datenbank nötig, kann pro Abfrage berechnet werden.
- Texte sind Config, kein Hardcoding:
- Rechnungs- und Aufwandszeilen entstehen aus Platzhalter-Templates (`invoiceLineTemplate`, `effortLineTemplate`), nicht aus fest verdrahteten Sätzen im Formelcode.
- Kein lauffähiges Space-Beispiel in diesem Dokument
- — nur Konzept, Datenmodell und illustrative Formeln. Konkrete Umsetzung ist projektspezifisch. ---
Problem
Viele Dienstleistungsverträge laufen nach demselben Muster: Mitarbeitende erfassen Zeiteinträge (Logbuch), und am Ende einer Periode muss daraus entweder
1. eine Rechnungsposition werden (Stunden × Satz), oder 2. ein Aufwandsnachweis ohne Rechnungsbetrag (wie viel von einem Kontingent/Budget wurde verbraucht).
Ohne ein gemeinsames Datenmodell entsteht das für jedes Projekt neu — mit leicht unterschiedlichen Feldnamen und Formeln, die schwer wiederverwendbar sind.
Datenmodell
TimeEntry (Logbuch-Zeile)
| Feld | Typ | Bedeutung | ||
|---|---|---|---|---|
start / end | Datum/Zeit | Beginn/Ende des Zeiteintrags | ||
durationHours | Zahl (berechnet) | (end - start) / 3600000 | ||
employeeRef | Referenz | Mitarbeiter:in | ||
contractRef | Referenz | Zugehöriger Vertrag/Kunde | ||
locationType | Enum | onsite \ | office \ | remote |
activityLabel | Text | Freitext oder Kategorie der Tätigkeit | ||
billingMode | Enum | contingent \ | billable_extra \ | non_billable |
billedAt | Datum (optional) | Gesetzt, sobald der Eintrag abgerechnet wurde |
BillingPeriod (Abrechnungszeitraum)
| Feld | Typ | Bedeutung |
|---|---|---|
contractRef | Referenz | Zugehöriger Vertrag/Kunde |
periodStart / periodEnd | Datum | Grenzen des Zeitraums (z. B. Kalenderjahr, Quartal, Vertragslaufzeit) |
targetHours | Zahl | Soll-Stunden für den Zeitraum |
hourlyRate | Zahl | Stundensatz |
budgetAmount | Zahl (optional) | Alternativ zu targetHours, wenn in Geld statt Stunden budgetiert wird |
Formeln (illustrativ)
let actualHours := sum(
(select TimeEntry where
contractRef = period.contractRef
and billingMode = "contingent"
and start >= period.periodStart
and start <= period.periodEnd
).durationHours
);
let remainingHours := period.targetHours - actualHours;
let progressPercent := min(100, round(actualHours / period.targetHours * 100));
let billableQueue := (select TimeEntry where contractRef = period.contractRef and billingMode = "billable_extra" and billedAt = null );</code></pre>
Zwei Ausgabepfade
1. Stunden-Abrechnung → Rechnungsposition
Für Zeiteinträge mit billingMode = "billable_extra" und billedAt = null:
1. billableQueue gruppieren (z. B. nach Periode + activityLabel) 2. Pro Gruppe: lineAmount := sum(durationHours) * hourlyRate 3. Zeilentext aus invoiceLineTemplate (siehe Textbaustein-Mechanismus) 4. Nach Übernahme in die Rechnung: billedAt auf allen enthaltenen Zeiteinträgen setzen
2. Budget/Kontingent → Aufwandsposition
Kein Rechnungsbetrag, sondern ein Statusnachweis pro BillingPeriod:
actualHours, targetHours, remainingHours, progressPercent- Zeilentext aus
effortLineTemplate - Typische Darstellung:
arcCustomProgressBar oder arcCustomKpiBar für den Fortschritt, arcCustomTable für die Liste der Perioden/Verträge
Textbaustein-Mechanismus
Statt fixer Sätze im Formelcode ein Config-Objekt mit Platzhaltern — der Wortlaut bleibt pro Projekt austauschbar, ohne die Formeln anzufassen:
let cfg := {
labels: { onsite: "Vor Ort", office: "Büro", remote: "Remote" },
invoiceLineTemplate: "{hours} Std. {activityLabel} ({period}) à {rate} €",
effortLineTemplate: "{actualHours} von {targetHours} Std. genutzt ({progressPercent} %), Rest {remainingHours} Std."
};
Platzhalter werden vor der Ausgabe per einfachem String-Replace aufgelöst (z. B. replace(cfg.invoiceLineTemplate, "{hours}", text(hours)) verkettet für jeden Platzhalter).
Beispiele
Fortschrittsanzeige für ein Kontingent
arcCustomProgressBar({
uniqueId: "contingent-progress-" + period.Nr,
value: progressPercent,
label: replace(
replace(cfg.effortLineTemplate, "{actualHours}", text(actualHours)),
"{targetHours}", text(period.targetHours)
)
})
Offene abrechenbare Stunden als Tabelle
arcCustomTable({
uniqueId: "billable-queue-" + contractRef.Nr,
table: [
{
columns: [
{ value: activityLabel },
{ value: text(durationHours) + " Std." },
{ value: text(durationHours * hourlyRate) + " €" }
]
}
// ... eine Zeile pro Eintrag aus billableQueue, gruppiert
]
})
Siehe auch
- <a href="./data-bag-actions.md"><code>data-bag-actions</code></a> — partielle Updates, falls Filter/Auswahl-Zustand pro Periode zwischengespeichert wird
docs/widgets/custom-kpi-bar.md, docs/widgets/custom-progress-bar.md — Fortschritts-/Kennzahlen-Darstellungdocs/widgets/custom-table.md — Listendarstellung für Zeiteinträge/Rechnungspositionen