Bemerkung und Abzuege bei Einzahlungen
Einzahlungen koennen jetzt eine Bemerkung tragen und negativ sein, damit Abzuege (Auszahlung bei Austritt, Erstattung) nachvollziehbar gebucht werden koennen. - ledger_entries.note wurde bisher nur beim Import befuellt und wird nun auch bei Einzahlungen geschrieben und angezeigt - Negative Betraege verlangen zwingend eine Bemerkung, sonst ist spaeter nicht mehr nachvollziehbar, warum jemandem Geld abgezogen wurde - Plausibilitaetsgrenze von 1000 EUR gegen Groessenordnungs-Tippfehler; bei einem Fehler in einer Zeile wird gar nichts gebucht und die Eingaben bleiben stehen - Legacy-Tabelle kl_Einzahlungen bekommt eine Bemerkung-Spalte, sonst haette ledger_mirror_legacy_payment die Notiz beim Re-Sync wieder mit NULL ueberschrieben - PayPal-Buchungen tragen jetzt Zahler, Datum und Mitteilung als Bemerkung Ausserdem behoben: letzteneintraege.php hat Einzahlungen und Striche direkt aus den Legacy-Tabellen gelesen, ohne nach Mandant zu filtern. Jeder Mandanten-Admin sah damit die Buchungen aus den Legacy-Tabellen, und das Stornieren lief ueber die nicht mandantengetrennten Legacy-IDs. Die Seite arbeitet jetzt auf dem mandantengebundenen Journal, und ledger_void_entry prueft Mandant und Buchungsart, bevor es storniert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+100
-5
@@ -41,6 +41,35 @@ function ledger_current_year(): int
|
||||
return (int)date('Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bemerkung fuer einen Journaleintrag saeubern: leere Angaben werden zu NULL,
|
||||
* die Laenge auf die Spaltenbreite (1000) begrenzt. Zeilenumbrueche werden zu
|
||||
* Leerzeichen, damit die Notiz in Tabellen und im PDF einzeilig bleibt.
|
||||
*/
|
||||
function ledger_normalize_note(?string $note): ?string
|
||||
{
|
||||
if ($note === null) {
|
||||
return null;
|
||||
}
|
||||
$note = preg_replace('/\s+/u', ' ', $note) ?? $note;
|
||||
$note = trim($note);
|
||||
if ($note === '') {
|
||||
return null;
|
||||
}
|
||||
if (strlen($note) > 1000) {
|
||||
// Byteweise kuerzen und danach eine ggf. angeschnittene Multibyte-
|
||||
// Sequenz am Ende wegnehmen (preg_match mit /u schlaegt bei
|
||||
// ungueltigem UTF-8 fehl). 1000 Bytes sind hoechstens 1000 Zeichen,
|
||||
// passen also immer in die Spalte.
|
||||
$note = substr($note, 0, 1000);
|
||||
while ($note !== '' && preg_match('//u', $note) !== 1) {
|
||||
$note = substr($note, 0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
function ledger_default_tenant_slug(): string
|
||||
{
|
||||
$tenantSlug = app_env('M4_DEFAULT_TENANT_SLUG');
|
||||
@@ -568,7 +597,7 @@ function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymen
|
||||
NULL,
|
||||
e.Datum,
|
||||
'legacy_payment',
|
||||
NULL,
|
||||
e.Bemerkung,
|
||||
'kl_Einzahlungen',
|
||||
e.EinzahlungsID
|
||||
FROM kl_Einzahlungen e
|
||||
@@ -606,6 +635,66 @@ function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymen
|
||||
* deleting it, so corrections stay auditable. Idempotent: voiding an
|
||||
* already-voided or missing entry is a no-op and returns false.
|
||||
*/
|
||||
/**
|
||||
* Storniert einen Journaleintrag anhand seiner Ledger-ID - immer auf den
|
||||
* Mandanten eingegrenzt, damit ueber eine untergeschobene ID nicht in fremden
|
||||
* Mandanten gebucht werden kann. Haengt am Eintrag noch eine Legacy-Zeile,
|
||||
* wird sie mitgeloescht, damit der Mirror den Eintrag nicht wieder auferstehen
|
||||
* laesst.
|
||||
*
|
||||
* @param string|null $expectedType Absichern, dass z. B. ueber das
|
||||
* Einzahlungs-Formular kein Strich storniert
|
||||
* werden kann.
|
||||
*/
|
||||
function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expectedType = null): bool
|
||||
{
|
||||
// Nur bekannte Legacy-Tabellen, damit der Tabellenname nie ungeprueft in
|
||||
// ein DELETE wandert.
|
||||
$legacyKeys = [
|
||||
'kl_Einzahlungen' => 'EinzahlungsID',
|
||||
'kl_Kaffeeverbrauch' => 'VerbrauchID',
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, type, legacy_table, legacy_id
|
||||
FROM ledger_entries
|
||||
WHERE id = ? AND tenant_id = ? AND voided_at IS NULL
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$entryId, $tenantId]);
|
||||
$entry = $stmt->fetch();
|
||||
|
||||
if ($entry === false) {
|
||||
return false;
|
||||
}
|
||||
if ($expectedType !== null && (string)$entry['type'] !== $expectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$pdo->prepare('UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND tenant_id = ? AND voided_at IS NULL')
|
||||
->execute([$entryId, $tenantId]);
|
||||
|
||||
$legacyTable = $entry['legacy_table'] !== null ? (string)$entry['legacy_table'] : null;
|
||||
if ($legacyTable !== null && $entry['legacy_id'] !== null && isset($legacyKeys[$legacyTable])) {
|
||||
$pdo->prepare("DELETE FROM {$legacyTable} WHERE {$legacyKeys[$legacyTable]} = ?")
|
||||
->execute([(int)$entry['legacy_id']]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function ledger_void_entry_by_legacy_id(PDO $pdo, int $tenantId, string $legacyTable, int $legacyId): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -702,16 +791,22 @@ function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId,
|
||||
* Records a payment entry straight into the ledger, without any legacy
|
||||
* kl_Einzahlungen row. Used for tenants that have no legacy shadow table.
|
||||
*/
|
||||
function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int $amountCents, string $source, ?int $createdByUserId = null): int
|
||||
/**
|
||||
* Bucht eine Einzahlung. $amountCents darf negativ sein (Abzug, Auszahlung,
|
||||
* Korrektur) - fuer Tippfehler ist aber ledger_void_entry der richtige Weg,
|
||||
* damit die Auswertung nicht durch Gegenbuchungen aufgeblaeht wird.
|
||||
*/
|
||||
function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int $amountCents, string $source, ?int $createdByUserId = null, ?string $note = null): int
|
||||
{
|
||||
$note = ledger_normalize_note($note);
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO ledger_entries
|
||||
(tenant_id, participant_id, type, amount_cents, booked_at, source, created_by_user_id)
|
||||
SELECT ?, id, 'payment', ?, NOW(), ?, ?
|
||||
(tenant_id, participant_id, type, amount_cents, booked_at, source, created_by_user_id, note)
|
||||
SELECT ?, id, 'payment', ?, NOW(), ?, ?, ?
|
||||
FROM participants
|
||||
WHERE id = ? AND tenant_id = ?"
|
||||
);
|
||||
$stmt->execute([$tenantId, $amountCents, $source, $createdByUserId, $participantId, $tenantId]);
|
||||
$stmt->execute([$tenantId, $amountCents, $source, $createdByUserId, $note, $participantId, $tenantId]);
|
||||
|
||||
if ($stmt->rowCount() !== 1) {
|
||||
throw new RuntimeException('Die Einzahlung konnte nicht gespeichert werden.');
|
||||
|
||||
+29
-7
@@ -100,15 +100,15 @@ function paypal_inbox_extract_token(string $address): ?string
|
||||
*
|
||||
* @return int Ledger-Entry-ID
|
||||
*/
|
||||
function paypal_book_payment(PDO $pdo, int $tenantId, array $participant, int $netCents, ?int $actorUserId): int
|
||||
function paypal_book_payment(PDO $pdo, int $tenantId, array $participant, int $netCents, ?int $actorUserId, ?string $note = null): int
|
||||
{
|
||||
$legacyMitarbeiterId = isset($participant['legacy_mitarbeiter_id']) && $participant['legacy_mitarbeiter_id'] !== null
|
||||
? (int) $participant['legacy_mitarbeiter_id']
|
||||
: 0;
|
||||
|
||||
if ($legacyMitarbeiterId > 0) {
|
||||
$stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)');
|
||||
$stmt->execute([$legacyMitarbeiterId, $netCents / 100, date('Y-m-d H:i:s')]);
|
||||
$stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Bemerkung, Datum) VALUES (?, ?, ?, ?)');
|
||||
$stmt->execute([$legacyMitarbeiterId, $netCents / 100, $note, date('Y-m-d H:i:s')]);
|
||||
$legacyPaymentId = (int) $pdo->lastInsertId();
|
||||
ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId);
|
||||
|
||||
@@ -120,7 +120,25 @@ function paypal_book_payment(PDO $pdo, int $tenantId, array $participant, int $n
|
||||
return (int) $idStmt->fetchColumn();
|
||||
}
|
||||
|
||||
return ledger_record_payment($pdo, $tenantId, (int) $participant['participant_id'], $netCents, 'paypal_import', $actorUserId);
|
||||
return ledger_record_payment($pdo, $tenantId, (int) $participant['participant_id'], $netCents, 'paypal_import', $actorUserId, $note);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bemerkung fuer eine aus PayPal uebernommene Buchung: Zahler und Datum, damit
|
||||
* im Journal nachvollziehbar bleibt, woher die Gutschrift stammt.
|
||||
*/
|
||||
function paypal_booking_note(array $payment): string
|
||||
{
|
||||
$note = 'PayPal: ' . trim((string) ($payment['payer_name'] ?? ''));
|
||||
if (!empty($payment['paid_at'])) {
|
||||
$note .= ' vom ' . date('d.m.Y', strtotime((string) $payment['paid_at']));
|
||||
}
|
||||
$mitteilung = trim((string) ($payment['note'] ?? ''));
|
||||
if ($mitteilung !== '') {
|
||||
$note .= ' ("' . $mitteilung . '")';
|
||||
}
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +211,11 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, $netCents, $actorUserId);
|
||||
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, $netCents, $actorUserId, paypal_booking_note([
|
||||
'payer_name' => $parsed['payer_name'] ?? '',
|
||||
'paid_at' => $parsed['date'] ?? null,
|
||||
'note' => $parsed['note'] ?? null,
|
||||
]));
|
||||
$pdo->prepare("UPDATE paypal_payments SET participant_id = ?, ledger_entry_id = ?, status = 'booked' WHERE id = ?")
|
||||
->execute([(int) $participant['participant_id'], $ledgerId, $paymentId]);
|
||||
$pdo->commit();
|
||||
@@ -273,7 +295,7 @@ function paypal_fetch_unmatched(PDO $pdo, int $tenantId): array
|
||||
*/
|
||||
function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $participantId, ?int $actorUserId): array
|
||||
{
|
||||
$stmt = $pdo->prepare("SELECT id, net_cents, status FROM paypal_payments WHERE id = ? AND tenant_id = ?");
|
||||
$stmt = $pdo->prepare("SELECT id, net_cents, status, payer_name, note, paid_at FROM paypal_payments WHERE id = ? AND tenant_id = ?");
|
||||
$stmt->execute([$paymentId, $tenantId]);
|
||||
$payment = $stmt->fetch();
|
||||
if ($payment === false || (string) $payment['status'] !== 'unmatched') {
|
||||
@@ -288,7 +310,7 @@ function paypal_assign_payment(PDO $pdo, int $tenantId, int $paymentId, int $par
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, (int) $payment['net_cents'], $actorUserId);
|
||||
$ledgerId = paypal_book_payment($pdo, $tenantId, $participant, (int) $payment['net_cents'], $actorUserId, paypal_booking_note($payment));
|
||||
$pdo->prepare("UPDATE paypal_payments SET participant_id = ?, ledger_entry_id = ?, status = 'booked' WHERE id = ?")
|
||||
->execute([$participantId, $ledgerId, $paymentId]);
|
||||
$pdo->commit();
|
||||
|
||||
Reference in New Issue
Block a user