Kontrolle: Projektkonto und Rechnungsbau waehlen dieselbe Aufwands-AB
Gemeinsame Auswahl juengsteAufwandAb (gueltig, jueengste nach gesendetAm) im AB-Speicher. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4177b76d82
commit
c2245b6905
4 files changed
+32
-8
No files matched your search
@@ -8,7 +8,7 @@
|
||||
import { config } from '../../config.js'
|
||||
import { createDocument, deleteDocument, getDocument, listAllDocuments, listDocuments, Query, updateDocument } from '../appwriteAdmin.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
import { LIMITS, ZAHLUNGSPLAN_ARTEN } from './konstanten.js'
|
||||
import { LIMITS, VEREINBARTE_STATUS, ZAHLUNGSPLAN_ARTEN } from './konstanten.js'
|
||||
import { AUFTRAEGE_COLLECTIONS } from './schema.js'
|
||||
|
||||
const JSON_SPALTEN = { kundeJson: 'kunde', zeitquelleJson: 'zeitquelle', positionenJson: 'positionen', zahlungsplanJson: 'zahlungsplan' }
|
||||
@@ -188,3 +188,16 @@ export async function findAuftragByNummer(nummer) {
|
||||
if (typeof nummer !== 'string' || !AB_NUMMER.test(nummer)) return null
|
||||
return findeEine('nummer', nummer)
|
||||
}
|
||||
|
||||
const sendezeit = (auftrag) => new Date(auftrag.gesendetAm || auftrag.erstelltAm || 0).getTime() || 0
|
||||
|
||||
/** Jüngste zuerst nach Sendezeitpunkt (ersatzweise Anlagezeit), bei Gleichstand nach Kennung. */
|
||||
export const juengsteZuerst = (a, b) => sendezeit(b) - sendezeit(a) || String(b.id).localeCompare(String(a.id))
|
||||
|
||||
/**
|
||||
* Die Aufwands-AB, mit der ein Projekt abgerechnet und angezeigt wird: unter den gültigen (versendet, bestätigt) die jüngste
|
||||
* nach gesendetAm, sonst null. Projektkonto und Rechnungsbau benutzen beide diese eine Auswahl.
|
||||
*/
|
||||
export function juengsteAufwandAb(liste) {
|
||||
return liste.filter((ab) => ab.art === 'aufwand' && VEREINBARTE_STATUS.includes(ab.status)).sort(juengsteZuerst)[0] ?? null
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { KUNDENARTEN, STUNDENSATZ_STANDARD_CENTS, VEREINBARTE_STATUS } from './k
|
||||
import { AuftragError } from './fehler.js'
|
||||
import { auftraegeAktiv } from './schalter.js'
|
||||
import { auftragFuerTeam } from './auftragSicht.js'
|
||||
import { listAuftraege } from './auftragStore.js'
|
||||
import { listAuftraege, juengsteAufwandAb } from './auftragStore.js'
|
||||
import { listProjektRechnungen } from './projektRechnungenStore.js'
|
||||
import { abgerechneteArbeitsblaetter, abrechenbar, ladeArbeitsblaetter, zeitquelleFuerProjekt } from './arbeitsblaetter.js'
|
||||
|
||||
@@ -21,8 +21,6 @@ import { abgerechneteArbeitsblaetter, abrechenbar, ladeArbeitsblaetter, zeitquel
|
||||
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
|
||||
@@ -40,7 +38,7 @@ export function rechneKonto({ auftraege = [], rechnungen = [], blaetter = [], ab
|
||||
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 aufwandAb = juengsteAufwandAb(gueltig)
|
||||
const aufwand = aufwandAb
|
||||
? {
|
||||
stundensatzCents: aufwandAb.stundensatzCents > 0 ? aufwandAb.stundensatzCents : STUNDENSATZ_STANDARD_CENTS,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
anzahlungPosition, schlussPositionen, SCHLUSS_GRENZEN,
|
||||
} from './positionen.js'
|
||||
import { auftraegeAktiv } from './schalter.js'
|
||||
import { getAuftrag, listAuftraege, updateAuftrag } from './auftragStore.js'
|
||||
import { getAuftrag, listAuftraege, updateAuftrag, juengsteAufwandAb } from './auftragStore.js'
|
||||
import { getProjektRechnung, listProjektRechnungen, updateProjektRechnung } from './projektRechnungenStore.js'
|
||||
import {
|
||||
ladeArbeitsblaetter, ladeArbeitsblaetterNachIds, abrechenbar, abgerechneteArbeitsblaetter, offeneArbeitsblaetter, zeitquelleFuerProjekt,
|
||||
@@ -227,7 +227,7 @@ async function ladeAufwandAuftrag(kunde, projekt, auftragId) {
|
||||
}
|
||||
if (!auftraegeAktiv(kunde)) return null
|
||||
const liste = await listAuftraege({ kundenId: kunde.$id, projektId: projekt.id })
|
||||
return liste.find((ab) => ab.art === 'aufwand' && VEREINBARTE_STATUS.includes(ab.status)) ?? null
|
||||
return juengsteAufwandAb(liste)
|
||||
}
|
||||
|
||||
/** Ausdrueckliche Auswahl der Blaetter (Pruefung, Zugehoerigkeit zum Kunden, Doppelabrechnung mit Selbstheilung). */
|
||||
|
||||
@@ -16,7 +16,8 @@ const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { seedRechnungsDaten, legeAuftragAn, legeRechnungAn, leereRechnungsDaten } = await import('./auftraegeRechnungenHilfe.js')
|
||||
const { createProjectInvoice, sendInvoiceNow, voidInvoice, deleteDraft, invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import('../server/services/finance/stripeFinance.js')
|
||||
const { projektkontoDetail } = await import('../server/services/auftraege/projektkonto.js')
|
||||
const { createProjectInvoice, rechnungsVorschau, sendInvoiceNow, voidInvoice, deleteDraft, invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import('../server/services/finance/stripeFinance.js')
|
||||
const { getAuftrag, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js')
|
||||
const { umhaengen } = await import('../server/services/auftraege/projektRechnungenStore.js')
|
||||
|
||||
@@ -132,3 +133,15 @@ test('Finalisieren einer Schlussrechnung setzt die AB aus dem Projektdatensatz a
|
||||
assert.equal((await getAuftrag(neu.id)).status, 'abgerechnet')
|
||||
assert.equal((await getAuftrag(ab.id)).status, 'ersetzt')
|
||||
})
|
||||
|
||||
test('Stundensatz: bei zwei gültigen Aufwands-ABs rechnen Rechnung und Projektkonto mit derselben (jüngste nach gesendetAm)', async () => {
|
||||
const basis = { art: 'aufwand', positionen: [], summeCents: null, zahlungsplan: { art: 'monatlich', prozent: null, text: '' } }
|
||||
// A zuerst angelegt, aber später gesendet; B danach angelegt, aber früher gesendet
|
||||
await legeAuftragAn({ ...basis, stundensatzCents: 8000 }, { nummer: 'AB-2026-0010', gesendetAm: '2026-10-09T08:00:00.000Z' })
|
||||
await new Promise((resolve) => setTimeout(resolve, 15))
|
||||
await legeAuftragAn({ ...basis, stundensatzCents: 12000 }, { nummer: 'AB-2026-0011', gesendetAm: '2026-10-06T08:00:00.000Z' })
|
||||
const vorschau = await rechnungsVorschau({ customerId: 'k1', art: 'aufwand', projektId: 'p1', arbeitsblattIds: ['ws1'] })
|
||||
const konto = await projektkontoDetail('p1')
|
||||
assert.equal(konto.konto.zeit.satzCents, 8000)
|
||||
assert.match(vorschau.positionen[0].beschreibung, /à 80,00\s€/)
|
||||
})
|
||||
Reference in new issue
Block a user