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:
@@ -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;
|
||||||
@@ -156,6 +156,12 @@ model Customer {
|
|||||||
phone String?
|
phone String?
|
||||||
mobile String?
|
mobile String?
|
||||||
taxNumber 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
|
businessRegistrationPath String? // PDF-Pfad zur Gewerbeanmeldung
|
||||||
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
|
commercialRegisterPath String? // PDF-Pfad zum Handelsregisterauszug
|
||||||
commercialRegisterNumber String? // Handelsregisternummer (Text)
|
commercialRegisterNumber String? // Handelsregisternummer (Text)
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ export async function formDefaults(req: AuthRequest, res: Response): Promise<voi
|
|||||||
if (blockPortal(req, res)) return;
|
if (blockPortal(req, res)) return;
|
||||||
const contractId = idParam(req, res, 'contractId');
|
const contractId = idParam(req, res, 'contractId');
|
||||||
if (contractId === null) return;
|
if (contractId === null) return;
|
||||||
const [customerType, nextNumber] = await Promise.all([
|
const [defaults, nextNumber] = await Promise.all([
|
||||||
creditNoteService.getDefaultCustomerType(contractId),
|
creditNoteService.getCreditNoteDefaults(contractId),
|
||||||
numberRangeService.previewNextNumber(),
|
numberRangeService.previewNextNumber(),
|
||||||
]);
|
]);
|
||||||
res.json({ success: true, data: { customerType, nextNumber } } as ApiResponse);
|
res.json({ success: true, data: { ...defaults, nextNumber } } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, 'Fehler beim Laden der Vorbelegung');
|
handleError(res, error, 'Fehler beim Laden der Vorbelegung');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,13 +132,26 @@ export async function getCreditNoteById(id: number) {
|
|||||||
return prisma.creditNote.findUnique({ where: { id } });
|
return prisma.creditNote.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ermittelt den Default-Kundentyp aus dem Vertrag (Firma vs. Privat).
|
// Ermittelt die Formular-Vorbelegung aus dem Kunden:
|
||||||
export async function getDefaultCustomerType(contractId: number): Promise<CreditNoteCustomerType> {
|
// - 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({
|
const contract = await prisma.contract.findUnique({
|
||||||
where: { id: contractId },
|
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(
|
export async function createCreditNote(
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export async function createCustomer(data: {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
taxNumber?: string;
|
taxNumber?: string;
|
||||||
|
vatExempt?: boolean;
|
||||||
businessRegistration?: string;
|
businessRegistration?: string;
|
||||||
commercialRegister?: string;
|
commercialRegister?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -173,6 +174,7 @@ export async function updateCustomer(
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
taxNumber?: string;
|
taxNumber?: string;
|
||||||
|
vatExempt?: boolean;
|
||||||
businessRegistration?: string;
|
businessRegistration?: string;
|
||||||
commercialRegister?: string;
|
commercialRegister?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ const CUSTOMER_UPDATABLE_FIELDS = [
|
|||||||
'phone',
|
'phone',
|
||||||
'mobile',
|
'mobile',
|
||||||
'taxNumber',
|
'taxNumber',
|
||||||
|
'vatExempt',
|
||||||
'commercialRegisterNumber',
|
'commercialRegisterNumber',
|
||||||
'notes',
|
'notes',
|
||||||
'portalEnabled',
|
'portalEnabled',
|
||||||
|
|||||||
+6
-1
@@ -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`.
|
`GET/PUT/DELETE /credit-notes/:id`, `GET/PUT /credit-notes/number-range`.
|
||||||
Portal-Token explizit geblockt (interner Bereich), CREATE/UPDATE/DELETE
|
Portal-Token explizit geblockt (interner Bereich), CREATE/UPDATE/DELETE
|
||||||
auditiert.
|
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
|
Phase 3 (PDF + ZUGFeRD/PDF-A3, muss gegen ZUGFeRD-Validator geprüft
|
||||||
werden). USt-Einordnung (Vermittlung vs. Abschlussbonus) mit Steuerberater
|
werden). USt-Einordnung (Vermittlung vs. Abschlussbonus) mit Steuerberater
|
||||||
klären – Modell deckt beide über `vatRelevant` ab.
|
klären – Modell deckt beide über `vatRelevant` ab.
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function CustomerForm() {
|
|||||||
phone: data.phone || '',
|
phone: data.phone || '',
|
||||||
mobile: data.mobile || '',
|
mobile: data.mobile || '',
|
||||||
taxNumber: data.taxNumber || '',
|
taxNumber: data.taxNumber || '',
|
||||||
|
vatExempt: data.vatExempt === true || (data.vatExempt as any) === 'true',
|
||||||
commercialRegisterNumber: data.commercialRegisterNumber || '',
|
commercialRegisterNumber: data.commercialRegisterNumber || '',
|
||||||
notes: data.notes || '',
|
notes: data.notes || '',
|
||||||
birthPlace: data.birthPlace || '',
|
birthPlace: data.birthPlace || '',
|
||||||
@@ -229,6 +230,20 @@ export default function CustomerForm() {
|
|||||||
placeholder="z.B. HRB 12345"
|
placeholder="z.B. HRB 12345"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{isEdit && (
|
||||||
<p className="mt-4 text-sm text-gray-500">
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
Dokumente (Gewerbeanmeldung, Handelsregisterauszug) können nach dem Speichern in der Kundendetailansicht hochgeladen werden.
|
Dokumente (Gewerbeanmeldung, Handelsregisterauszug) können nach dem Speichern in der Kundendetailansicht hochgeladen werden.
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export interface Customer {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
taxNumber?: string;
|
taxNumber?: string;
|
||||||
|
vatExempt?: boolean;
|
||||||
businessRegistrationPath?: string;
|
businessRegistrationPath?: string;
|
||||||
commercialRegisterPath?: string;
|
commercialRegisterPath?: string;
|
||||||
commercialRegisterNumber?: string;
|
commercialRegisterNumber?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user