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:
@@ -158,6 +158,10 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
||||
}
|
||||
|
||||
// ---- Unterschriftsblock ----
|
||||
// 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 || '';
|
||||
@@ -168,6 +172,7 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
||||
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
|
||||
|
||||
@@ -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,14 +280,18 @@ function CreditNoteFormModal({
|
||||
value={form.creditDate}
|
||||
onChange={(e) => set('creditDate', e.target.value)}
|
||||
/>
|
||||
{/* 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>
|
||||
|
||||
{isSachwert && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Unterschrift am (optional)"
|
||||
@@ -295,7 +299,6 @@ function CreditNoteFormModal({
|
||||
value={form.signedAt}
|
||||
onChange={(e) => set('signedAt', e.target.value)}
|
||||
/>
|
||||
{form.type === 'SACHWERT' && (
|
||||
<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>
|
||||
<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} />
|
||||
<ReceiptControls
|
||||
cn={cn}
|
||||
canEdit={canEdit}
|
||||
onChanged={refresh}
|
||||
label={cn.type === 'GELD' ? 'Überweisungsbeleg' : 'Unterschr. Dokument'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user