feat(projekte): Projekttyp "Abruf" fuer Abrechnung nach Aufwand

Abruf braucht keinen Budgettopf: es gibt kein zugesagtes Volumen und kein
Ablaufdatum, gebucht wird, was anfaellt. Der Satz kann deshalb nicht am
Topf haengen und steht am Projekt (pulse_projects.rate, Stunde oder Tag).
Der Umsatz sind die abrechenbaren Stunden mal diesem Satz; offen ist
nichts.

- Migration ergaenzt den ENUM-Wert und die Spalte rate
- Anlegen verlangt einen Satz und nimmt keine Toepfe an; ein Topf
  nachtraeglich anzulegen wird abgewiesen
- Wechsel auf Abruf nur, solange auf den Toepfen nichts gebucht ist -
  sonst wuerden Buchungen ihr Budget verlieren; leere Toepfe fallen weg
- Buchungsdialog, Projektkarte, Projektdetail und Projektreport zeigen
  Satz und Umsatz statt Budgetbalken

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-18 00:13:35 +02:00
co-authored by Claude Opus 5
parent 2af516ec20
commit 8c93df11d9
12 changed files with 182 additions and 35 deletions
+5 -3
View File
@@ -46,6 +46,7 @@ export async function loadEntries(settings, { from, to, userIds = null, customer
u.name AS user_name, u.color AS user_color, u.is_external, u.company AS user_company, u.weekly_hours,
u.manager_id,
p.name AS project_name, p.code AS project_code, p.type AS project_type, p.billing_unit,
p.rate AS project_rate,
c.id AS customer_id, c.name AS customer_name, c.short_name AS customer_short, c.color AS customer_color,
t.name AS task_name,
b.label AS pot_label, b.rate AS pot_rate, b.budget_amount AS pot_amount
@@ -72,12 +73,13 @@ export async function loadEntries(settings, { from, to, userIds = null, customer
}
return rows.map((r) => {
const project = { name: r.project_name, code: r.project_code, billing_unit: r.billing_unit };
const project = { name: r.project_name, code: r.project_code, billing_unit: r.billing_unit, type: r.project_type, rate: r.project_rate };
const hours = r.minutes / 60;
let amount = 0;
let fixedAmount = 0;
if (r.billable && r.project_type === 'billable') {
const rate = hourlyRate({ rate: r.pot_rate }, project, settings);
// Billable rechnet ueber den Satz des Topfs ab, Abruf ueber den am Projekt.
if (r.billable && (r.project_type === 'billable' || r.project_type === 'abruf')) {
const rate = hourlyRate(r.project_type === 'abruf' ? null : { rate: r.pot_rate }, project, settings);
amount = rate ? hours * rate : 0;
}
if (r.billable && r.project_type === 'fixed' && r.pot_amount && potMinutes.get(r.pot_id)) {