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?
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -618,6 +618,7 @@ const CUSTOMER_UPDATABLE_FIELDS = [
|
||||
'phone',
|
||||
'mobile',
|
||||
'taxNumber',
|
||||
'vatExempt',
|
||||
'commercialRegisterNumber',
|
||||
'notes',
|
||||
'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`.
|
||||
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.
|
||||
|
||||
@@ -109,6 +109,7 @@ export interface Customer {
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
taxNumber?: string;
|
||||
vatExempt?: boolean;
|
||||
businessRegistrationPath?: string;
|
||||
commercialRegisterPath?: string;
|
||||
commercialRegisterNumber?: string;
|
||||
|
||||
Reference in New Issue
Block a user