Projektkonto: reine Rechenregeln (vereinbart, berechnet, offen, Zeitwert je Tag gerundet, Ohne Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8627ed1124
commit
3305371111
2 files changed
+306
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { STUNDENSATZ_STANDARD_CENTS, VEREINBARTE_STATUS } from './konstanten.js'
|
||||
import { abrechenbar } from './arbeitsblaetter.js'
|
||||
|
||||
/**
|
||||
* Projektkonto (Spec 2026-10-06, Abschnitt 4; Uebersicht V4 "L1 - Projektkonto"): reine Rechenregeln und die Ladefunktionen
|
||||
* dazu. Alles hier liest nur, weder Appwrite noch Stripe werden geschrieben.
|
||||
*/
|
||||
|
||||
// Rechnungsstatus laut mapInvoice (stripeFinance.js), die als "berechnet" zaehlen; Entwurf und storniert nie
|
||||
const FINALISIERT = new Set(['offen', 'ueberfaellig', 'bezahlt', 'uneinbringlich'])
|
||||
|
||||
const summe = (liste, feld) => liste.reduce((s, eintrag) => s + eintrag[feld], 0)
|
||||
const zeitpunkt = (auftrag) => new Date(auftrag.gesendetAm || auftrag.erstelltAm || 0).getTime() || 0
|
||||
const juengsteZuerst = (a, b) => zeitpunkt(b) - zeitpunkt(a) || String(b.id).localeCompare(String(a.id))
|
||||
const zeitWert = (minuten, satzCents) => Math.round((minuten * satzCents) / 60)
|
||||
// Wert der Zeit = Summe der je Berliner Tag gerundeten Werte (Karte: Tag -> Minuten), genau wie die Zeilen einer Aufwandsrechnung
|
||||
// (aufwandPositionen in L2: eine Zeile je Tag mit Math.round(Minuten × Satz / 60)); so laufen Konto und Rechnung nicht um Cent auseinander
|
||||
const tageswert = (minutenJeTag, satzCents) => [...minutenJeTag.values()].reduce((s, minuten) => s + zeitWert(minuten, satzCents), 0)
|
||||
const addiere = (karte, tag, minuten) => karte.set(tag, (karte.get(tag) ?? 0) + minuten)
|
||||
|
||||
/**
|
||||
* Die Zahlen eines Projekts. auftraege: alle ABs des Projekts (jeder Status); rechnungen: [{ art, auftragId, invoice }] mit
|
||||
* der Rechnung in mapInvoice-Form (nur aktive Projektrechnungen, deren Rechnung gefunden wurde); blaetter: Arbeitsblaetter der
|
||||
* Zeitquelle (datum = Berliner Tag); abgerechnet: Map oder Set der Blatt-IDs aus aktiven Projektrechnungen (auch Entwuerfe);
|
||||
* stripeOk false = die Rechnungen sind unbekannt (berechnet und bezahlt 0, offen null).
|
||||
*/
|
||||
export function rechneKonto({ auftraege = [], rechnungen = [], blaetter = [], abgerechnet = new Map(), stripeOk = true } = {}) {
|
||||
const gueltig = auftraege.filter((auftrag) => VEREINBARTE_STATUS.includes(auftrag.status))
|
||||
const festpreise = gueltig.filter((auftrag) => auftrag.art === 'festpreis')
|
||||
const festpreisIds = new Set(festpreise.map((auftrag) => auftrag.id))
|
||||
const festpreisCents = festpreise.length ? festpreise.reduce((s, auftrag) => s + (Number(auftrag.summeCents) || 0), 0) : null
|
||||
const aufwandAb = gueltig.filter((auftrag) => auftrag.art === 'aufwand').sort(juengsteZuerst)[0] ?? null
|
||||
const aufwand = aufwandAb
|
||||
? {
|
||||
stundensatzCents: aufwandAb.stundensatzCents > 0 ? aufwandAb.stundensatzCents : STUNDENSATZ_STANDARD_CENTS,
|
||||
schaetzungMinuten: aufwandAb.schaetzungMinuten ?? null,
|
||||
}
|
||||
: null
|
||||
const satzCents = aufwand ? aufwand.stundensatzCents : STUNDENSATZ_STANDARD_CENTS
|
||||
|
||||
let berechnetCents = 0
|
||||
let bezahltCents = 0
|
||||
let aufFestpreisCents = 0
|
||||
if (stripeOk) {
|
||||
for (const { invoice, auftragId } of rechnungen) {
|
||||
if (!FINALISIERT.has(invoice.status)) continue
|
||||
berechnetCents += invoice.totalCents
|
||||
bezahltCents += invoice.paidCents
|
||||
// nur Rechnungen zu einer als vereinbart zaehlenden Festpreis-AB mindern das Offene
|
||||
if (festpreisIds.has(auftragId)) aufFestpreisCents += invoice.totalCents
|
||||
}
|
||||
}
|
||||
const offenCents = festpreisCents === null || !stripeOk ? null : Math.max(0, festpreisCents - aufFestpreisCents)
|
||||
|
||||
let minuten = 0
|
||||
let aboMinuten = 0
|
||||
let offeneMinuten = 0
|
||||
const minutenJeTag = new Map() // alle abrechenbaren Blaetter (auch schon berechnete)
|
||||
const offeneMinutenJeTag = new Map() // nur die offenen; ein Blatt ohne Datum (nur theoretisch) bildet einen eigenen Tag ''
|
||||
for (const blatt of blaetter) {
|
||||
if (abrechenbar(blatt)) {
|
||||
minuten += blatt.minuten
|
||||
addiere(minutenJeTag, blatt.datum, blatt.minuten)
|
||||
if (!abgerechnet.has(blatt.id)) {
|
||||
offeneMinuten += blatt.minuten
|
||||
addiere(offeneMinutenJeTag, blatt.datum, blatt.minuten)
|
||||
}
|
||||
} else if (blatt.aboZeit && !blatt.kommentar) {
|
||||
aboMinuten += blatt.minuten
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vereinbart: { festpreisCents, aufwand },
|
||||
berechnetCents,
|
||||
bezahltCents,
|
||||
offenCents,
|
||||
zeit: {
|
||||
minuten,
|
||||
aboMinuten,
|
||||
offeneMinuten,
|
||||
satzCents,
|
||||
wertCents: tageswert(minutenJeTag, satzCents),
|
||||
offenerWertCents: tageswert(offeneMinutenJeTag, satzCents),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Zeile "Ohne Projekt": Rechnungen in mapInvoice-Form; gezaehlt werden nur finalisierte. */
|
||||
export function rechneOhneProjekt(rechnungen) {
|
||||
const gezaehlt = rechnungen.filter((invoice) => FINALISIERT.has(invoice.status))
|
||||
return { berechnetCents: summe(gezaehlt, 'totalCents'), bezahltCents: summe(gezaehlt, 'paidCents'), anzahl: gezaehlt.length }
|
||||
}
|
||||
Reference in new issue
Block a user