Gutschriften: Auszahlungskonto (Bankkonto des Kunden) fuer Ueberweisungen
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen als das Vertrags-Abbuchkonto: - CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit Loeschen einer Bankkarte die Gutschrift nicht mitreisst). - Formular: Dropdown mit allen Bankkonten des Kunden (Default = Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards + contractBankCardId. - Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags gehoert (kein Fremdkonto unterschieben). - PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter 'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das Auszahlungskonto. Lokal verifiziert (Anlage + PDF). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
-- Auszahlungskonto (Bankkarte des Kunden) fuer Geld-Gutschriften.
|
||||||
|
-- ON DELETE SET NULL: Loeschen der Bankkarte reisst die Gutschrift nicht mit.
|
||||||
|
ALTER TABLE `CreditNote` ADD COLUMN IF NOT EXISTS `payoutBankCardId` INT NULL;
|
||||||
|
|
||||||
|
-- FK nur anlegen, wenn noch nicht vorhanden (idempotent via information_schema).
|
||||||
|
SET @fk := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'CreditNote'
|
||||||
|
AND CONSTRAINT_NAME = 'CreditNote_payoutBankCardId_fkey');
|
||||||
|
SET @sql := IF(@fk = 0,
|
||||||
|
'ALTER TABLE `CreditNote` ADD CONSTRAINT `CreditNote_payoutBankCardId_fkey` FOREIGN KEY (`payoutBankCardId`) REFERENCES `BankCard`(`id`) ON DELETE SET NULL ON UPDATE CASCADE',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'CreditNote'
|
||||||
|
AND INDEX_NAME = 'CreditNote_payoutBankCardId_idx');
|
||||||
|
SET @sql2 := IF(@idx = 0,
|
||||||
|
'CREATE INDEX `CreditNote_payoutBankCardId_idx` ON `CreditNote`(`payoutBankCardId`)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt2 FROM @sql2; EXECUTE stmt2; DEALLOCATE PREPARE stmt2;
|
||||||
@@ -360,6 +360,7 @@ model BankCard {
|
|||||||
description String? @db.Text // Freitext-Notiz (z.B. "Geschäftskonto", "gemeinsames Konto mit Partner")
|
description String? @db.Text // Freitext-Notiz (z.B. "Geschäftskonto", "gemeinsames Konto mit Partner")
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
contracts Contract[]
|
contracts Contract[]
|
||||||
|
creditNotePayouts CreditNote[] @relation("CreditNotePayoutBankCard")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
@@ -1008,6 +1009,13 @@ model CreditNote {
|
|||||||
// Sachwert: Bestätigung "Ware erhalten".
|
// Sachwert: Bestätigung "Ware erhalten".
|
||||||
goodsReceived Boolean @default(false)
|
goodsReceived Boolean @default(false)
|
||||||
|
|
||||||
|
// Auszahlungskonto (nur GELD): Bankkarte des Kunden, auf die überwiesen
|
||||||
|
// wird. Kann ein anderes Konto sein als das Abbuchkonto des Vertrags.
|
||||||
|
// ON DELETE SET NULL, damit das Löschen einer Bankkarte die Gutschrift
|
||||||
|
// (historischer Beleg) nicht mitreißt.
|
||||||
|
payoutBankCardId Int?
|
||||||
|
payoutBankCard BankCard? @relation("CreditNotePayoutBankCard", fields: [payoutBankCardId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
// Überweisungsbeleg (nur GELD) – hochgeladene Datei.
|
// Überweisungsbeleg (nur GELD) – hochgeladene Datei.
|
||||||
receiptPath String?
|
receiptPath String?
|
||||||
// Generiertes Gutschrift-Dokument (PDF bzw. ZUGFeRD-Hybrid) – Phase 3.
|
// Generiertes Gutschrift-Dokument (PDF bzw. ZUGFeRD-Hybrid) – Phase 3.
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export interface CreateCreditNoteInput {
|
|||||||
place?: string | null;
|
place?: string | null;
|
||||||
signedAt?: string | null;
|
signedAt?: string | null;
|
||||||
goodsReceived?: boolean;
|
goodsReceived?: boolean;
|
||||||
|
payoutBankCardId?: number | null; // nur GELD: Auszahlungskonto des Kunden
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +105,16 @@ function validateAndNormalize(input: CreateCreditNoteInput) {
|
|||||||
|
|
||||||
const amounts = computeAmounts({ amount, vatRelevant, amountBasis, vatRate });
|
const amounts = computeAmounts({ amount, vatRelevant, amountBasis, vatRate });
|
||||||
|
|
||||||
|
// Auszahlungskonto nur bei GELD relevant; bei Sachwert immer leeren.
|
||||||
|
let payoutBankCardId: number | null = null;
|
||||||
|
if (type === 'GELD' && input.payoutBankCardId != null && input.payoutBankCardId !== ('' as unknown)) {
|
||||||
|
const parsed = Number(input.payoutBankCardId);
|
||||||
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
||||||
|
throw new ApiError(400, 'Ungültiges Auszahlungskonto');
|
||||||
|
}
|
||||||
|
payoutBankCardId = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
sachwertDescription: type === 'SACHWERT' ? input.sachwertDescription!.trim() : null,
|
sachwertDescription: type === 'SACHWERT' ? input.sachwertDescription!.trim() : null,
|
||||||
@@ -117,10 +128,23 @@ function validateAndNormalize(input: CreateCreditNoteInput) {
|
|||||||
place: input.place?.trim() || null,
|
place: input.place?.trim() || null,
|
||||||
signedAt,
|
signedAt,
|
||||||
goodsReceived: !!input.goodsReceived,
|
goodsReceived: !!input.goodsReceived,
|
||||||
|
payoutBankCardId,
|
||||||
notes: input.notes?.trim() || null,
|
notes: input.notes?.trim() || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stellt sicher, dass die gewählte Bankkarte dem Kunden des Vertrags gehört
|
||||||
|
// (kein Fremdkonto unterschieben).
|
||||||
|
async function assertBankCardBelongsToContract(contractId: number, bankCardId: number) {
|
||||||
|
const [contract, card] = await Promise.all([
|
||||||
|
prisma.contract.findUnique({ where: { id: contractId }, select: { customerId: true } }),
|
||||||
|
prisma.bankCard.findUnique({ where: { id: bankCardId }, select: { customerId: true } }),
|
||||||
|
]);
|
||||||
|
if (!card || !contract || card.customerId !== contract.customerId) {
|
||||||
|
throw new ApiError(400, 'Das gewählte Auszahlungskonto gehört nicht zum Kunden dieses Vertrags.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCreditNotesByContract(contractId: number) {
|
export async function getCreditNotesByContract(contractId: number) {
|
||||||
return prisma.creditNote.findMany({
|
return prisma.creditNote.findMany({
|
||||||
where: { contractId },
|
where: { contractId },
|
||||||
@@ -139,18 +163,33 @@ export async function getCreditNoteById(id: number) {
|
|||||||
// WICHTIG: Das ist nur die Vorbelegung. Jede angelegte Gutschrift speichert
|
// WICHTIG: Das ist nur die Vorbelegung. Jede angelegte Gutschrift speichert
|
||||||
// ihren eigenen Snapshot; ein späterer Statuswechsel des Kunden ändert
|
// ihren eigenen Snapshot; ein späterer Statuswechsel des Kunden ändert
|
||||||
// bestehende Gutschriften nicht.
|
// bestehende Gutschriften nicht.
|
||||||
export async function getCreditNoteDefaults(
|
export async function getCreditNoteDefaults(contractId: number) {
|
||||||
contractId: number,
|
|
||||||
): Promise<{ customerType: CreditNoteCustomerType; vatRelevant: boolean }> {
|
|
||||||
const contract = await prisma.contract.findUnique({
|
const contract = await prisma.contract.findUnique({
|
||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
select: { customer: { select: { type: true, vatExempt: true } } },
|
select: {
|
||||||
|
bankCardId: true,
|
||||||
|
customer: {
|
||||||
|
select: {
|
||||||
|
type: true,
|
||||||
|
vatExempt: true,
|
||||||
|
bankCards: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, iban: true, accountHolder: true, bankName: true, description: true },
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const isBusiness = contract?.customer?.type === 'BUSINESS';
|
const isBusiness = contract?.customer?.type === 'BUSINESS';
|
||||||
const vatExempt = !!contract?.customer?.vatExempt;
|
const vatExempt = !!contract?.customer?.vatExempt;
|
||||||
return {
|
return {
|
||||||
customerType: isBusiness ? 'FIRMA' : 'PRIVAT',
|
customerType: (isBusiness ? 'FIRMA' : 'PRIVAT') as CreditNoteCustomerType,
|
||||||
vatRelevant: isBusiness && !vatExempt,
|
vatRelevant: isBusiness && !vatExempt,
|
||||||
|
// Bankkarten des Kunden für das Auszahlungskonto-Dropdown; die
|
||||||
|
// Vertrags-Abbuchkarte als Default-Vorschlag markiert.
|
||||||
|
bankCards: contract?.customer?.bankCards ?? [],
|
||||||
|
contractBankCardId: contract?.bankCardId ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +204,9 @@ export async function createCreditNote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalized = validateAndNormalize(input);
|
const normalized = validateAndNormalize(input);
|
||||||
|
if (normalized.payoutBankCardId) {
|
||||||
|
await assertBankCardBelongsToContract(contractId, normalized.payoutBankCardId);
|
||||||
|
}
|
||||||
const number = await assignNextNumber();
|
const number = await assignNextNumber();
|
||||||
|
|
||||||
return prisma.creditNote.create({
|
return prisma.creditNote.create({
|
||||||
@@ -183,6 +225,9 @@ export async function updateCreditNote(id: number, input: CreateCreditNoteInput)
|
|||||||
throw new ApiError(404, 'Gutschrift nicht gefunden');
|
throw new ApiError(404, 'Gutschrift nicht gefunden');
|
||||||
}
|
}
|
||||||
const normalized = validateAndNormalize(input);
|
const normalized = validateAndNormalize(input);
|
||||||
|
if (normalized.payoutBankCardId) {
|
||||||
|
await assertBankCardBelongsToContract(existing.contractId, normalized.payoutBankCardId);
|
||||||
|
}
|
||||||
// Nummer bleibt unverändert (einmal vergeben = fix). Ein evtl. schon
|
// Nummer bleibt unverändert (einmal vergeben = fix). Ein evtl. schon
|
||||||
// erzeugtes PDF ist nach inhaltlicher Änderung veraltet → Pfad leeren
|
// erzeugtes PDF ist nach inhaltlicher Änderung veraltet → Pfad leeren
|
||||||
// (der User erzeugt es bei Bedarf neu).
|
// (der User erzeugt es bei Bedarf neu).
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ async function loadData(creditNoteId: number) {
|
|||||||
address: true,
|
address: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
payoutBankCard: { select: { iban: true, accountHolder: true, bankName: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!cn) throw new ApiError(404, 'Gutschrift nicht gefunden');
|
if (!cn) throw new ApiError(404, 'Gutschrift nicht gefunden');
|
||||||
@@ -121,8 +122,19 @@ export async function generateCreditNotePdf(creditNoteId: number): Promise<{ buf
|
|||||||
doc.fontSize(10);
|
doc.fontSize(10);
|
||||||
if (cn.type === 'SACHWERT') {
|
if (cn.type === 'SACHWERT') {
|
||||||
doc.text(`Ware erhalten: ${cn.goodsReceived ? 'Ja' : 'Nein'}`);
|
doc.text(`Ware erhalten: ${cn.goodsReceived ? 'Ja' : 'Nein'}`);
|
||||||
} else if (company.iban) {
|
} else {
|
||||||
doc.text(`Auszahlung per Überweisung. Bankverbindung: ${company.bankName} · IBAN ${company.iban} · BIC ${company.bic}`.trim());
|
doc.text('Auszahlung per Überweisung.');
|
||||||
|
if (company.iban) {
|
||||||
|
doc.fontSize(9).fillColor('#333')
|
||||||
|
.text(`Unsere Bankverbindung: ${[company.bankName, `IBAN ${company.iban}`, company.bic ? `BIC ${company.bic}` : ''].filter(Boolean).join(' · ')}`);
|
||||||
|
}
|
||||||
|
// Auszahlungskonto des Kunden (kann vom Abbuchkonto abweichen).
|
||||||
|
if (cn.payoutBankCard) {
|
||||||
|
const holder = cn.payoutBankCard.accountHolder ? ` (${cn.payoutBankCard.accountHolder})` : '';
|
||||||
|
doc.fontSize(9).fillColor('#333')
|
||||||
|
.text(`an Bankkonto: ${cn.payoutBankCard.iban}${holder}${cn.payoutBankCard.bankName ? ` · ${cn.payoutBankCard.bankName}` : ''}`);
|
||||||
|
}
|
||||||
|
doc.fillColor('#000');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Unterschriftsblock ----
|
// ---- Unterschriftsblock ----
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
`POST /credit-notes/:id/pdf`, „PDF"-Button in der Section (Ansehen/Erzeugen).
|
`POST /credit-notes/:id/pdf`, „PDF"-Button in der Section (Ansehen/Erzeugen).
|
||||||
PDF-Pfad wird bei inhaltlicher Änderung geleert (Neu-Erzeugung). Lokal
|
PDF-Pfad wird bei inhaltlicher Änderung geleert (Neu-Erzeugung). Lokal
|
||||||
verifiziert (valides PDF).
|
verifiziert (valides PDF).
|
||||||
|
- **Auszahlungskonto (nur GELD):** `CreditNote.payoutBankCardId` +
|
||||||
|
Migration (FK ON DELETE SET NULL). Dropdown im Formular mit ALLEN
|
||||||
|
Bankkonten des Kunden (Default = Vertrags-Abbuchkonto), da die Auszahlung
|
||||||
|
auf ein anderes Konto gehen kann. Server prüft, dass die Bankkarte dem
|
||||||
|
Kunden des Vertrags gehört. PDF zeigt bei Überweisung „Unsere
|
||||||
|
Bankverbindung" + „an Bankkonto: <Kunden-IBAN>". Section-Zeile zeigt das
|
||||||
|
Auszahlungskonto.
|
||||||
- **Offen:** Phase 3b Teil 2 (ZUGFeRD-XML EN 16931, Typ 381, in PDF/A-3
|
- **Offen:** Phase 3b Teil 2 (ZUGFeRD-XML EN 16931, Typ 381, in PDF/A-3
|
||||||
einbetten – **muss gegen ZUGFeRD-Validator** geprüft werden). USt-
|
einbetten – **muss gegen ZUGFeRD-Validator** geprüft werden). USt-
|
||||||
Einordnung (Vermittlung vs. Abschlussbonus) mit Steuerberater klären –
|
Einordnung (Vermittlung vs. Abschlussbonus) mit Steuerberater klären –
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Input from '../ui/Input';
|
|||||||
import Select from '../ui/Select';
|
import Select from '../ui/Select';
|
||||||
import Badge from '../ui/Badge';
|
import Badge from '../ui/Badge';
|
||||||
import { creditNoteApi } from '../../services/api';
|
import { creditNoteApi } from '../../services/api';
|
||||||
import type { CreditNote, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '../../types';
|
import type { CreditNote, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis, CreditNotePayoutBankCard } from '../../types';
|
||||||
|
|
||||||
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
|
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@ function calcAmounts(amount: number, vatRelevant: boolean, basis: CreditNoteAmou
|
|||||||
|
|
||||||
const todayIso = () => new Date().toISOString().split('T')[0];
|
const todayIso = () => new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
interface FormDefaults {
|
||||||
|
customerType: CreditNoteCustomerType;
|
||||||
|
vatRelevant: boolean;
|
||||||
|
bankCards: CreditNotePayoutBankCard[];
|
||||||
|
contractBankCardId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
type: CreditNoteType;
|
type: CreditNoteType;
|
||||||
sachwertDescription: string;
|
sachwertDescription: string;
|
||||||
@@ -47,10 +54,11 @@ interface FormState {
|
|||||||
place: string;
|
place: string;
|
||||||
signedAt: string;
|
signedAt: string;
|
||||||
goodsReceived: boolean;
|
goodsReceived: boolean;
|
||||||
|
payoutBankCardId: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyForm(defaults?: { customerType: CreditNoteCustomerType; vatRelevant: boolean }): FormState {
|
function emptyForm(defaults?: FormDefaults): FormState {
|
||||||
return {
|
return {
|
||||||
type: 'GELD',
|
type: 'GELD',
|
||||||
sachwertDescription: '',
|
sachwertDescription: '',
|
||||||
@@ -63,6 +71,8 @@ function emptyForm(defaults?: { customerType: CreditNoteCustomerType; vatRelevan
|
|||||||
place: '',
|
place: '',
|
||||||
signedAt: '',
|
signedAt: '',
|
||||||
goodsReceived: false,
|
goodsReceived: false,
|
||||||
|
// Vorschlag: das Abbuchkonto des Vertrags (kann umgestellt werden).
|
||||||
|
payoutBankCardId: defaults?.contractBankCardId ? String(defaults.contractBankCardId) : '',
|
||||||
notes: '',
|
notes: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,6 +91,7 @@ function formFromCreditNote(cn: CreditNote): FormState {
|
|||||||
place: cn.place ?? '',
|
place: cn.place ?? '',
|
||||||
signedAt: cn.signedAt ? cn.signedAt.split('T')[0] : '',
|
signedAt: cn.signedAt ? cn.signedAt.split('T')[0] : '',
|
||||||
goodsReceived: cn.goodsReceived,
|
goodsReceived: cn.goodsReceived,
|
||||||
|
payoutBankCardId: cn.payoutBankCardId ? String(cn.payoutBankCardId) : '',
|
||||||
notes: cn.notes ?? '',
|
notes: cn.notes ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -95,11 +106,12 @@ function CreditNoteFormModal({
|
|||||||
}: {
|
}: {
|
||||||
contractId: number;
|
contractId: number;
|
||||||
editing: CreditNote | null;
|
editing: CreditNote | null;
|
||||||
defaults?: { customerType: CreditNoteCustomerType; vatRelevant: boolean };
|
defaults?: FormDefaults;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [form, setForm] = useState<FormState>(editing ? formFromCreditNote(editing) : emptyForm(defaults));
|
const [form, setForm] = useState<FormState>(editing ? formFromCreditNote(editing) : emptyForm(defaults));
|
||||||
|
const bankCards = defaults?.bankCards ?? [];
|
||||||
const set = <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((f) => ({ ...f, [key]: value }));
|
const set = <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((f) => ({ ...f, [key]: value }));
|
||||||
|
|
||||||
const preview = calcAmounts(parseFloat(form.amount), form.vatRelevant, form.amountBasis, parseFloat(form.vatRate) || 0);
|
const preview = calcAmounts(parseFloat(form.amount), form.vatRelevant, form.amountBasis, parseFloat(form.vatRate) || 0);
|
||||||
@@ -118,6 +130,7 @@ function CreditNoteFormModal({
|
|||||||
place: form.place || null,
|
place: form.place || null,
|
||||||
signedAt: form.signedAt || null,
|
signedAt: form.signedAt || null,
|
||||||
goodsReceived: form.type === 'SACHWERT' ? form.goodsReceived : false,
|
goodsReceived: form.type === 'SACHWERT' ? form.goodsReceived : false,
|
||||||
|
payoutBankCardId: form.type === 'GELD' && form.payoutBankCardId ? Number(form.payoutBankCardId) : null,
|
||||||
notes: form.notes || null,
|
notes: form.notes || null,
|
||||||
};
|
};
|
||||||
return editing ? creditNoteApi.update(editing.id, payload) : creditNoteApi.create(contractId, payload);
|
return editing ? creditNoteApi.update(editing.id, payload) : creditNoteApi.create(contractId, payload);
|
||||||
@@ -221,6 +234,25 @@ function CreditNoteFormModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{form.type === 'GELD' && (
|
||||||
|
bankCards.length > 0 ? (
|
||||||
|
<Select
|
||||||
|
label="Auszahlungskonto (Bankkonto des Kunden)"
|
||||||
|
value={form.payoutBankCardId}
|
||||||
|
onChange={(e) => set('payoutBankCardId', e.target.value)}
|
||||||
|
options={bankCards.map((bc) => ({
|
||||||
|
value: String(bc.id),
|
||||||
|
label: `${bc.iban} (${bc.accountHolder})${bc.description ? ` – ${bc.description}` : ''}`,
|
||||||
|
}))}
|
||||||
|
placeholder="Kein Konto angeben"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Für diesen Kunden sind keine Bankkonten hinterlegt – Auszahlungskonto kann daher nicht gewählt werden.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Datum der Gutschrift"
|
label="Datum der Gutschrift"
|
||||||
@@ -431,6 +463,12 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
|
|||||||
<div className="text-sm text-gray-600 mt-0.5">{cn.sachwertDescription}</div>
|
<div className="text-sm text-gray-600 mt-0.5">{cn.sachwertDescription}</div>
|
||||||
)}
|
)}
|
||||||
{cn.notes && <div className="text-xs text-gray-500 mt-0.5 italic">{cn.notes}</div>}
|
{cn.notes && <div className="text-xs text-gray-500 mt-0.5 italic">{cn.notes}</div>}
|
||||||
|
{cn.type === 'GELD' && cn.payoutBankCardId && (() => {
|
||||||
|
const card = defaults?.bankCards.find((bc) => bc.id === cn.payoutBankCardId);
|
||||||
|
return card ? (
|
||||||
|
<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' && (
|
{cn.type === 'GELD' && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<ReceiptControls cn={cn} canEdit={canEdit} onChanged={refresh} />
|
<ReceiptControls cn={cn} canEdit={canEdit} onChanged={refresh} />
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export interface CreditNote {
|
|||||||
place?: string | null;
|
place?: string | null;
|
||||||
signedAt?: string | null;
|
signedAt?: string | null;
|
||||||
goodsReceived: boolean;
|
goodsReceived: boolean;
|
||||||
|
payoutBankCardId?: number | null;
|
||||||
receiptPath?: string | null;
|
receiptPath?: string | null;
|
||||||
pdfPath?: string | null;
|
pdfPath?: string | null;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
@@ -89,10 +90,20 @@ export interface CreditNote {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreditNotePayoutBankCard {
|
||||||
|
id: number;
|
||||||
|
iban: string;
|
||||||
|
accountHolder: string;
|
||||||
|
bankName?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreditNoteDefaults {
|
export interface CreditNoteDefaults {
|
||||||
customerType: CreditNoteCustomerType;
|
customerType: CreditNoteCustomerType;
|
||||||
vatRelevant: boolean;
|
vatRelevant: boolean;
|
||||||
nextNumber: string;
|
nextNumber: string;
|
||||||
|
bankCards: CreditNotePayoutBankCard[];
|
||||||
|
contractBankCardId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreditNoteNumberRange {
|
export interface CreditNoteNumberRange {
|
||||||
|
|||||||
Reference in New Issue
Block a user