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 ----
doc.moveDown(3);
const y = doc.y;
const place = cn.place || company.city || '';
doc.fontSize(10).fillColor('#000');
doc.text('_______________________________', 50, y);
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');
// Nur bei Sachwert: das Dokument wird vom Kunden unterschrieben. Bei einer
// Geld-Überweisung ist keine Unterschrift nötig als Beleg dient die
// hochgeladene Überweisungsbestätigung.
if (cn.type === 'SACHWERT') {
doc.moveDown(3);
const y = doc.y;
const place = cn.place || company.city || '';
doc.fontSize(10).fillColor('#000');
doc.text('_______________________________', 50, y);
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) ----
// 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
- [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)
- Pentester-Hinweis R142: Beim Übergang Geld→betragsloser Sachwert wurde die
schon vergebene Gutschriftsnummer auf null gesetzt → **Lücke** in der GS-Serie.
@@ -280,22 +280,25 @@ function CreditNoteFormModal({
value={form.creditDate}
onChange={(e) => set('creditDate', e.target.value)}
/>
<Input
label="Ort (Unterschrift)"
value={form.place}
onChange={(e) => set('place', e.target.value)}
placeholder="z.B. Oldenburg"
/>
{/* Ort/Unterschrift nur bei Sachwert Überweisung wird nicht unterschrieben. */}
{isSachwert && (
<Input
label="Ort (Unterschrift)"
value={form.place}
onChange={(e) => set('place', e.target.value)}
placeholder="z.B. Oldenburg"
/>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Unterschrift am (optional)"
type="date"
value={form.signedAt}
onChange={(e) => set('signedAt', e.target.value)}
/>
{form.type === 'SACHWERT' && (
{isSachwert && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Unterschrift am (optional)"
type="date"
value={form.signedAt}
onChange={(e) => set('signedAt', e.target.value)}
/>
<label className="flex items-center gap-2 text-sm mt-7">
<input
type="checkbox"
@@ -305,8 +308,8 @@ function CreditNoteFormModal({
/>
Ware erhalten (Empfang bestätigt)
</label>
)}
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notiz (optional)</label>
@@ -318,9 +321,11 @@ function CreditNoteFormModal({
/>
</div>
{form.type === 'GELD' && !editing && (
{!editing && (
<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>
)}
@@ -336,7 +341,7 @@ function CreditNoteFormModal({
}
// ---------- 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 uploadMutation = useMutation({
mutationFn: (file: File) => creditNoteApi.uploadReceipt(cn.id, file),
@@ -353,17 +358,17 @@ function ReceiptControls({ cn, canEdit, onChanged }: { cn: CreditNote; canEdit:
return (
<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">
<Eye className="w-3.5 h-3.5" /> Beleg
<Eye className="w-3.5 h-3.5" /> {label}
</a>
{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" />
</button>
)}
</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 (
<>
<input
@@ -378,7 +383,7 @@ function ReceiptControls({ cn, canEdit, onChanged }: { cn: CreditNote; canEdit:
disabled={uploadMutation.isPending}
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>
</>
);
@@ -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>
) : null;
})()}
{cn.type === 'GELD' && (
<div className="mt-1">
<ReceiptControls cn={cn} canEdit={canEdit} onChanged={refresh} />
</div>
)}
<div className="mt-1">
<ReceiptControls
cn={cn}
canEdit={canEdit}
onChanged={refresh}
label={cn.type === 'GELD' ? 'Überweisungsbeleg' : 'Unterschr. Dokument'}
/>
</div>
</div>
{canEdit && (
<div className="flex items-center gap-1">