Gutschrift: nur mit Empfaengeradresse anlegbar (Rechnung > Liefer)
Eine Gutschrift/ein Lieferschein braucht eine Empfaengeradresse aufs Dokument. Rechnungsadresse hat Vorrang, sonst Lieferadresse. Ist keine von beiden hinterlegt -> Anlegen blockiert. - Frontend: Klick auf 'Gutschrift anlegen' prueft defaults.hasRecipient- Address; wenn false -> Modal-OK-Meldung statt Formular. - Backend Defense-in-Depth: createCreditNote wirft 400, wenn weder billingAddressId noch addressId gesetzt. getCreditNoteDefaults liefert hasRecipientAddress. Verifiziert: ohne Adresse -> hasRecipientAddress false + create 400; mit Adresse -> ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -196,6 +196,8 @@ export async function getCreditNoteDefaults(contractId: number) {
|
|||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
select: {
|
select: {
|
||||||
bankCardId: true,
|
bankCardId: true,
|
||||||
|
addressId: true,
|
||||||
|
billingAddressId: true,
|
||||||
customer: {
|
customer: {
|
||||||
select: {
|
select: {
|
||||||
type: true,
|
type: true,
|
||||||
@@ -218,6 +220,9 @@ export async function getCreditNoteDefaults(contractId: number) {
|
|||||||
// Vertrags-Abbuchkarte als Default-Vorschlag markiert.
|
// Vertrags-Abbuchkarte als Default-Vorschlag markiert.
|
||||||
bankCards: contract?.customer?.bankCards ?? [],
|
bankCards: contract?.customer?.bankCards ?? [],
|
||||||
contractBankCardId: contract?.bankCardId ?? null,
|
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,
|
input: CreateCreditNoteInput,
|
||||||
createdBy?: string,
|
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) {
|
if (!contract) {
|
||||||
throw new ApiError(404, 'Vertrag nicht gefunden');
|
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);
|
const normalized = validateAndNormalize(input);
|
||||||
if (normalized.payoutBankCardId) {
|
if (normalized.payoutBankCardId) {
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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)
|
- [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,
|
- **Beleg-Upload jetzt für beide Arten:** bei Geld die Überweisungsbestätigung,
|
||||||
bei Sachwert das **unterschriebene Dokument**. `ReceiptControls` in der Liste
|
bei Sachwert das **unterschriebene Dokument**. `ReceiptControls` in der Liste
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ function ReceiptControls({ cn, canEdit, onChanged, label = 'Beleg' }: { cn: Cred
|
|||||||
export default function CreditNotesSection({ contractId, canEdit }: { contractId: number; canEdit: boolean }) {
|
export default function CreditNotesSection({ contractId, canEdit }: { contractId: number; canEdit: boolean }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [showNoAddress, setShowNoAddress] = useState(false);
|
||||||
const [editing, setEditing] = useState<CreditNote | null>(null);
|
const [editing, setEditing] = useState<CreditNote | null>(null);
|
||||||
|
|
||||||
const { data: listRes } = useQuery({
|
const { data: listRes } = useQuery({
|
||||||
@@ -425,7 +426,15 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
|
|||||||
onError: (err: Error) => toast.error(err.message || 'PDF fehlgeschlagen'),
|
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 openEdit = (cn: CreditNote) => { setEditing(cn); setShowForm(true); };
|
||||||
|
|
||||||
const totalGross = creditNotes.reduce((sum, cn) => sum + (cn.amountGross || 0), 0);
|
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(); }}
|
onSaved={() => { setShowForm(false); refresh(); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showNoAddress && (
|
||||||
|
<Modal isOpen onClose={() => setShowNoAddress(false)} title="Keine Adresse hinterlegt" size="sm">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setShowNoAddress(false)}>OK</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export interface CreditNoteDefaults {
|
|||||||
nextNumber: string;
|
nextNumber: string;
|
||||||
bankCards: CreditNotePayoutBankCard[];
|
bankCards: CreditNotePayoutBankCard[];
|
||||||
contractBankCardId: number | null;
|
contractBankCardId: number | null;
|
||||||
|
hasRecipientAddress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreditNoteNumberRange {
|
export interface CreditNoteNumberRange {
|
||||||
|
|||||||
Reference in New Issue
Block a user