Gutschrift: Beleg-Upload auch fuer Sachwerte + kein Unterschriftsblock bei Geld

- Beleg-Upload jetzt fuer beide Arten: bei Geld die Ueberweisungs-
  bestaetigung, bei Sachwert das unterschriebene Dokument. ReceiptControls
  in der Liste fuer Geld UND Sachwert (Label je nach Typ). Endpoint war
  schon typ-agnostisch.
- PDF-Unterschriftsblock nur noch bei Sachwert - eine Ueberweisung wird
  nicht unterschrieben (Beleg = hochgeladene Ueberweisungsbestaetigung).
  Bei Geld entfaellt der Unterschrift/Ort-Block; im Formular sind Ort +
  'Unterschrift am' bei Geld ausgeblendet.

Verifiziert: beide PDFs erzeugen sauber (je 1 Seite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 09:26:35 +02:00
co-authored by Claude Opus 4.8
parent 5e606df49e
commit 0078654465
3 changed files with 60 additions and 38 deletions
+15 -10
View File
@@ -158,16 +158,21 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
} }
// ---- Unterschriftsblock ---- // ---- Unterschriftsblock ----
doc.moveDown(3); // Nur bei Sachwert: das Dokument wird vom Kunden unterschrieben. Bei einer
const y = doc.y; // Geld-Überweisung ist keine Unterschrift nötig als Beleg dient die
const place = cn.place || company.city || ''; // hochgeladene Überweisungsbestätigung.
doc.fontSize(10).fillColor('#000'); if (cn.type === 'SACHWERT') {
doc.text('_______________________________', 50, y); doc.moveDown(3);
doc.text('_______________________________', 320, y); const y = doc.y;
doc.fontSize(8).fillColor('#666'); const place = cn.place || company.city || '';
doc.text(`Ort/Datum${place ? ` (${place})` : ''}`, 50, y + 14); doc.fontSize(10).fillColor('#000');
doc.text('Unterschrift', 320, y + 14); doc.text('_______________________________', 50, y);
doc.fillColor('#000'); doc.text('_______________________________', 320, y);
doc.fontSize(8).fillColor('#666');
doc.text(`Ort/Datum${place ? ` (${place})` : ''}`, 50, y + 14);
doc.text('Unterschrift', 320, y + 14);
doc.fillColor('#000');
}
// ---- Fußzeile (Firmendaten) ---- // ---- Fußzeile (Firmendaten) ----
// Position aus der Seitengeometrie berechnen, damit die Fußzeile am // Position aus der Seitengeometrie berechnen, damit die Fußzeile am
+9
View File
@@ -97,6 +97,15 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ Erledigt
- [x] **🧾 Gutschrift: Beleg-Upload auch für Sachwerte + kein Unterschriftsblock bei Geld** (2026-08-12)
- **Beleg-Upload jetzt für beide Arten:** bei Geld die Überweisungsbestätigung,
bei Sachwert das **unterschriebene Dokument**. `ReceiptControls` in der Liste
für Geld UND Sachwert (Label „Überweisungsbeleg" bzw. „Unterschr. Dokument").
Endpoint `/upload/credit-notes/:id/receipt` war schon typ-agnostisch.
- **PDF-Unterschriftsblock nur noch bei Sachwert:** eine Geld-Überweisung wird
nicht unterschrieben (Beleg = hochgeladene Überweisungsbestätigung). Bei Geld
entfällt der Unterschrift/Ort-Block im PDF; im Formular sind „Ort" +
„Unterschrift am" bei Geld ausgeblendet. Verifiziert (beide PDFs 1 Seite).
- [x] **📦 Lieferschein-Nummernkreis für betragslose Sachwerte (GoBD)** (2026-08-12) - [x] **📦 Lieferschein-Nummernkreis für betragslose Sachwerte (GoBD)** (2026-08-12)
- Pentester-Hinweis R142: Beim Übergang Geld→betragsloser Sachwert wurde die - Pentester-Hinweis R142: Beim Übergang Geld→betragsloser Sachwert wurde die
schon vergebene Gutschriftsnummer auf null gesetzt → **Lücke** in der GS-Serie. schon vergebene Gutschriftsnummer auf null gesetzt → **Lücke** in der GS-Serie.
@@ -280,22 +280,25 @@ function CreditNoteFormModal({
value={form.creditDate} value={form.creditDate}
onChange={(e) => set('creditDate', e.target.value)} onChange={(e) => set('creditDate', e.target.value)}
/> />
<Input {/* Ort/Unterschrift nur bei Sachwert Überweisung wird nicht unterschrieben. */}
label="Ort (Unterschrift)" {isSachwert && (
value={form.place} <Input
onChange={(e) => set('place', e.target.value)} label="Ort (Unterschrift)"
placeholder="z.B. Oldenburg" value={form.place}
/> onChange={(e) => set('place', e.target.value)}
placeholder="z.B. Oldenburg"
/>
)}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {isSachwert && (
<Input <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
label="Unterschrift am (optional)" <Input
type="date" label="Unterschrift am (optional)"
value={form.signedAt} type="date"
onChange={(e) => set('signedAt', e.target.value)} value={form.signedAt}
/> onChange={(e) => set('signedAt', e.target.value)}
{form.type === 'SACHWERT' && ( />
<label className="flex items-center gap-2 text-sm mt-7"> <label className="flex items-center gap-2 text-sm mt-7">
<input <input
type="checkbox" type="checkbox"
@@ -305,8 +308,8 @@ function CreditNoteFormModal({
/> />
Ware erhalten (Empfang bestätigt) Ware erhalten (Empfang bestätigt)
</label> </label>
)} </div>
</div> )}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notiz (optional)</label> <label className="block text-sm font-medium text-gray-700 mb-1">Notiz (optional)</label>
@@ -318,9 +321,11 @@ function CreditNoteFormModal({
/> />
</div> </div>
{form.type === 'GELD' && !editing && ( {!editing && (
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Der Überweisungsbeleg kann nach dem Speichern in der Liste hochgeladen werden. {isSachwert
? 'Das unterschriebene Dokument kann nach dem Speichern in der Liste hochgeladen werden.'
: 'Der Überweisungsbeleg kann nach dem Speichern in der Liste hochgeladen werden.'}
</p> </p>
)} )}
@@ -336,7 +341,7 @@ function CreditNoteFormModal({
} }
// ---------- Beleg-Upload (nur GELD) ---------- // ---------- Beleg-Upload (nur GELD) ----------
function ReceiptControls({ cn, canEdit, onChanged }: { cn: CreditNote; canEdit: boolean; onChanged: () => void }) { function ReceiptControls({ cn, canEdit, onChanged, label = 'Beleg' }: { cn: CreditNote; canEdit: boolean; onChanged: () => void; label?: string }) {
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
const uploadMutation = useMutation({ const uploadMutation = useMutation({
mutationFn: (file: File) => creditNoteApi.uploadReceipt(cn.id, file), mutationFn: (file: File) => creditNoteApi.uploadReceipt(cn.id, file),
@@ -353,17 +358,17 @@ function ReceiptControls({ cn, canEdit, onChanged }: { cn: CreditNote; canEdit:
return ( return (
<span className="inline-flex items-center gap-2 text-xs"> <span className="inline-flex items-center gap-2 text-xs">
<a href={fileUrl(cn.receiptPath, { inline: true })} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1"> <a href={fileUrl(cn.receiptPath, { inline: true })} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
<Eye className="w-3.5 h-3.5" /> Beleg <Eye className="w-3.5 h-3.5" /> {label}
</a> </a>
{canEdit && ( {canEdit && (
<button onClick={() => removeMutation.mutate()} className="text-gray-400 hover:text-red-600" title="Beleg entfernen"> <button onClick={() => removeMutation.mutate()} className="text-gray-400 hover:text-red-600" title={`${label} entfernen`}>
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
</button> </button>
)} )}
</span> </span>
); );
} }
if (!canEdit) return <span className="text-xs text-gray-400">kein Beleg</span>; if (!canEdit) return <span className="text-xs text-gray-400">kein {label}</span>;
return ( return (
<> <>
<input <input
@@ -378,7 +383,7 @@ function ReceiptControls({ cn, canEdit, onChanged }: { cn: CreditNote; canEdit:
disabled={uploadMutation.isPending} disabled={uploadMutation.isPending}
className="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800" className="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800"
> >
<Upload className="w-3.5 h-3.5" /> {uploadMutation.isPending ? 'lädt …' : 'Beleg'} <Upload className="w-3.5 h-3.5" /> {uploadMutation.isPending ? 'lädt …' : label}
</button> </button>
</> </>
); );
@@ -497,11 +502,14 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
<div className="text-xs text-gray-500 mt-0.5">an Bankkonto: <span className="font-mono">{card.iban}</span> ({card.accountHolder})</div> <div className="text-xs text-gray-500 mt-0.5">an Bankkonto: <span className="font-mono">{card.iban}</span> ({card.accountHolder})</div>
) : null; ) : null;
})()} })()}
{cn.type === 'GELD' && ( <div className="mt-1">
<div className="mt-1"> <ReceiptControls
<ReceiptControls cn={cn} canEdit={canEdit} onChanged={refresh} /> cn={cn}
</div> canEdit={canEdit}
)} onChanged={refresh}
label={cn.type === 'GELD' ? 'Überweisungsbeleg' : 'Unterschr. Dokument'}
/>
</div>
</div> </div>
{canEdit && ( {canEdit && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">