Gutschriften Phase 2a: Kleinunternehmer-Flag am Kunden + USt-Default

Kunden-Feld vatExempt (Kleinunternehmer/USt-befreit §19) + Migration
(ADD COLUMN IF NOT EXISTS). Checkbox im Kundenformular nur fuer
Firmenkunden.

Gutschrift-Vorbelegung vatRelevant wird aus dem Kunden abgeleitet:
Firmenkunde ohne USt-Befreiung -> USt-relevant an (Netto), sonst aus
(wie Privat). Jede Gutschrift speichert ihren eigenen Snapshot, ein
spaeterer Statuswechsel des Kunden aendert bestehende Gutschriften
nicht.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 10:10:13 +02:00
co-authored by Claude Opus 4.8
parent f1c37a7d25
commit 3580c51cb1
9 changed files with 54 additions and 8 deletions
@@ -0,0 +1,3 @@
-- Kleinunternehmer/USt-befreit-Flag am Kunden. Steuert nur die Vorbelegung
-- neuer Gutschriften (Firmenkunde mit vatExempt=true -> keine USt-Vorbelegung).
ALTER TABLE `Customer` ADD COLUMN IF NOT EXISTS `vatExempt` BOOLEAN NOT NULL DEFAULT false;
+6
View File
@@ -156,6 +156,12 @@ model Customer {
phone String?
mobile String?
taxNumber String?
// Kleinunternehmer nach §19 UStG bzw. sonst USt-befreit: dann wird der
// (Firmen-)Kunde bei Gutschriften wie ein Privatkunde behandelt (keine USt),
// bis er ggf. USt-pflichtig wird. Steuert nur die VORBELEGUNG neuer
// Gutschriften; bereits erstellte Gutschriften speichern ihren eigenen
// Snapshot und bleiben von einem späteren Wechsel unberührt.
vatExempt Boolean @default(false)
businessRegistrationPath String? // PDF-Pfad zur Gewerbeanmeldung
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
commercialRegisterNumber String? // Handelsregisternummer (Text)
@@ -60,11 +60,11 @@ export async function formDefaults(req: AuthRequest, res: Response): Promise<voi
if (blockPortal(req, res)) return;
const contractId = idParam(req, res, 'contractId');
if (contractId === null) return;
const [customerType, nextNumber] = await Promise.all([
creditNoteService.getDefaultCustomerType(contractId),
const [defaults, nextNumber] = await Promise.all([
creditNoteService.getCreditNoteDefaults(contractId),
numberRangeService.previewNextNumber(),
]);
res.json({ success: true, data: { customerType, nextNumber } } as ApiResponse);
res.json({ success: true, data: { ...defaults, nextNumber } } as ApiResponse);
} catch (error) {
handleError(res, error, 'Fehler beim Laden der Vorbelegung');
}
+17 -4
View File
@@ -132,13 +132,26 @@ export async function getCreditNoteById(id: number) {
return prisma.creditNote.findUnique({ where: { id } });
}
// Ermittelt den Default-Kundentyp aus dem Vertrag (Firma vs. Privat).
export async function getDefaultCustomerType(contractId: number): Promise<CreditNoteCustomerType> {
// Ermittelt die Formular-Vorbelegung aus dem Kunden:
// - customerType: Firma vs. Privat (aus Kunde.type)
// - vatRelevant : Default nur bei Firmenkunde OHNE USt-Befreiung
// (Kleinunternehmer §19 → wie Privat, keine USt-Vorbelegung).
// WICHTIG: Das ist nur die Vorbelegung. Jede angelegte Gutschrift speichert
// ihren eigenen Snapshot; ein späterer Statuswechsel des Kunden ändert
// bestehende Gutschriften nicht.
export async function getCreditNoteDefaults(
contractId: number,
): Promise<{ customerType: CreditNoteCustomerType; vatRelevant: boolean }> {
const contract = await prisma.contract.findUnique({
where: { id: contractId },
select: { customer: { select: { type: true } } },
select: { customer: { select: { type: true, vatExempt: true } } },
});
return contract?.customer?.type === 'BUSINESS' ? 'FIRMA' : 'PRIVAT';
const isBusiness = contract?.customer?.type === 'BUSINESS';
const vatExempt = !!contract?.customer?.vatExempt;
return {
customerType: isBusiness ? 'FIRMA' : 'PRIVAT',
vatRelevant: isBusiness && !vatExempt,
};
}
export async function createCreditNote(
+2
View File
@@ -146,6 +146,7 @@ export async function createCustomer(data: {
phone?: string;
mobile?: string;
taxNumber?: string;
vatExempt?: boolean;
businessRegistration?: string;
commercialRegister?: string;
notes?: string;
@@ -173,6 +174,7 @@ export async function updateCustomer(
phone?: string;
mobile?: string;
taxNumber?: string;
vatExempt?: boolean;
businessRegistration?: string;
commercialRegister?: string;
notes?: string;
+1
View File
@@ -618,6 +618,7 @@ const CUSTOMER_UPDATABLE_FIELDS = [
'phone',
'mobile',
'taxNumber',
'vatExempt',
'commercialRegisterNumber',
'notes',
'portalEnabled',
+6 -1
View File
@@ -116,7 +116,12 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
`GET/PUT/DELETE /credit-notes/:id`, `GET/PUT /credit-notes/number-range`.
Portal-Token explizit geblockt (interner Bereich), CREATE/UPDATE/DELETE
auditiert.
- **Offen:** Phase 2 (Vertrag-UI + Beleg-Upload + Nummernkreis-UI),
- **Phase 2a (erledigt):** Kunden-Feld `vatExempt` (Kleinunternehmer §19,
Checkbox nur für Firmenkunden) + Migration. Gutschrift-Default `vatRelevant`
wird abgeleitet: Firmenkunde ohne USt-Befreiung → USt-relevant an, sonst
aus. Jede Gutschrift speichert ihren Snapshot → späterer Statuswechsel
ändert bestehende Gutschriften nicht. USt-Default „aus Kundentyp ableiten".
- **Offen:** Phase 2b (Vertrag-Tab „Gutschriften" + Beleg-Upload + Nummernkreis-UI),
Phase 3 (PDF + ZUGFeRD/PDF-A3, muss gegen ZUGFeRD-Validator geprüft
werden). USt-Einordnung (Vermittlung vs. Abschlussbonus) mit Steuerberater
klären Modell deckt beide über `vatRelevant` ab.
@@ -77,6 +77,7 @@ export default function CustomerForm() {
phone: data.phone || '',
mobile: data.mobile || '',
taxNumber: data.taxNumber || '',
vatExempt: data.vatExempt === true || (data.vatExempt as any) === 'true',
commercialRegisterNumber: data.commercialRegisterNumber || '',
notes: data.notes || '',
birthPlace: data.birthPlace || '',
@@ -229,6 +230,20 @@ export default function CustomerForm() {
placeholder="z.B. HRB 12345"
/>
</div>
<label className="mt-4 flex items-start gap-2 text-sm">
<input
type="checkbox"
{...register('vatExempt')}
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>
<span className="font-medium">Kleinunternehmer / USt-befreit (§19 UStG)</span>
<span className="block text-gray-500">
Wird bei Gutschriften wie ein Privatkunde behandelt (keine USt-Vorbelegung).
Ändert nur neue Gutschriften bereits erstellte bleiben unberührt.
</span>
</span>
</label>
{isEdit && (
<p className="mt-4 text-sm text-gray-500">
Dokumente (Gewerbeanmeldung, Handelsregisterauszug) können nach dem Speichern in der Kundendetailansicht hochgeladen werden.
+1
View File
@@ -109,6 +109,7 @@ export interface Customer {
phone?: string;
mobile?: string;
taxNumber?: string;
vatExempt?: boolean;
businessRegistrationPath?: string;
commercialRegisterPath?: string;
commercialRegisterNumber?: string;