diff --git a/backend/src/services/creditNote.service.ts b/backend/src/services/creditNote.service.ts index 2da1cdde..60fbfe68 100644 --- a/backend/src/services/creditNote.service.ts +++ b/backend/src/services/creditNote.service.ts @@ -196,6 +196,8 @@ export async function getCreditNoteDefaults(contractId: number) { where: { id: contractId }, select: { bankCardId: true, + addressId: true, + billingAddressId: true, customer: { select: { type: true, @@ -218,6 +220,9 @@ export async function getCreditNoteDefaults(contractId: number) { // Vertrags-Abbuchkarte als Default-Vorschlag markiert. bankCards: contract?.customer?.bankCards ?? [], contractBankCardId: contract?.bankCardId ?? null, + // Für die Empfängeradresse auf dem Beleg: Rechnungsadresse hat Vorrang, + // sonst Lieferadresse. Ohne beide kann kein Beleg erstellt werden. + hasRecipientAddress: !!(contract?.billingAddressId || contract?.addressId), }; } @@ -226,10 +231,21 @@ export async function createCreditNote( input: CreateCreditNoteInput, createdBy?: string, ) { - const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { id: true } }); + const contract = await prisma.contract.findUnique({ + where: { id: contractId }, + select: { id: true, addressId: true, billingAddressId: true }, + }); if (!contract) { throw new ApiError(404, 'Vertrag nicht gefunden'); } + // Empfängeradresse für den Beleg: Rechnungsadresse bevorzugt, sonst + // Lieferadresse. Ohne beide kann kein Beleg erzeugt werden. + if (!contract.billingAddressId && !contract.addressId) { + throw new ApiError( + 400, + 'Keine Rechnungs- oder Lieferadresse am Vertrag hinterlegt. Bitte zuerst eine Adresse zuordnen.', + ); + } const normalized = validateAndNormalize(input); if (normalized.payoutBankCardId) { diff --git a/docs/todo.md b/docs/todo.md index 710ce537..7388f499 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,13 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🚫 Gutschrift nur mit Empfängeradresse (Rechnung > Liefer)** (2026-08-12) + - Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine + Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**. + Ist **keine** von beiden hinterlegt → **Modal-OK-Meldung** statt Formular. + - Backend Defense-in-Depth: `createCreditNote` wirft **400**, wenn weder + `billingAddressId` noch `addressId` gesetzt ist. `getCreditNoteDefaults` + liefert `hasRecipientAddress` fürs Frontend. Verifiziert. - [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 diff --git a/frontend/src/components/contracts/CreditNotesSection.tsx b/frontend/src/components/contracts/CreditNotesSection.tsx index 105a7d56..ba26ed4c 100644 --- a/frontend/src/components/contracts/CreditNotesSection.tsx +++ b/frontend/src/components/contracts/CreditNotesSection.tsx @@ -393,6 +393,7 @@ function ReceiptControls({ cn, canEdit, onChanged, label = 'Beleg' }: { cn: Cred export default function CreditNotesSection({ contractId, canEdit }: { contractId: number; canEdit: boolean }) { const queryClient = useQueryClient(); const [showForm, setShowForm] = useState(false); + const [showNoAddress, setShowNoAddress] = useState(false); const [editing, setEditing] = useState(null); const { data: listRes } = useQuery({ @@ -425,7 +426,15 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId onError: (err: Error) => toast.error(err.message || 'PDF fehlgeschlagen'), }); - const openCreate = () => { setEditing(null); setShowForm(true); }; + const openCreate = () => { + // Ohne Empfängeradresse (Rechnungs- ODER Lieferadresse) kein Beleg möglich. + if (defaults && !defaults.hasRecipientAddress) { + setShowNoAddress(true); + return; + } + setEditing(null); + setShowForm(true); + }; const openEdit = (cn: CreditNote) => { setEditing(cn); setShowForm(true); }; const totalGross = creditNotes.reduce((sum, cn) => sum + (cn.amountGross || 0), 0); @@ -548,6 +557,21 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId onSaved={() => { setShowForm(false); refresh(); }} /> )} + + {showNoAddress && ( + setShowNoAddress(false)} title="Keine Adresse hinterlegt" size="sm"> +
+

+ Für diesen Vertrag ist weder eine Rechnungs- noch eine Lieferadresse hinterlegt. + Ohne Empfängeradresse kann keine Gutschrift/kein Lieferschein erstellt werden. + Bitte ordne dem Vertrag zuerst eine Adresse zu. +

+
+ +
+
+
+ )} ); } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 89a745b6..d657368b 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -105,6 +105,7 @@ export interface CreditNoteDefaults { nextNumber: string; bankCards: CreditNotePayoutBankCard[]; contractBankCardId: number | null; + hasRecipientAddress: boolean; } export interface CreditNoteNumberRange {