Refactor financial transaction handling: Consolidate Einnahmen and Ausgaben into Buchung model, update routes and UI components, and add new migration scripts for database schema changes.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
-- Add source linking fields to Buchung model
|
||||
ALTER TABLE `buchungen` ADD COLUMN `betriebseinnahmeId` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `betriebsausgabeId` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `linkedBuchungId` VARCHAR(191) NULL;
|
||||
|
||||
-- Add unique constraints for 1:1 relations
|
||||
ALTER TABLE `buchungen` ADD UNIQUE INDEX `buchungen_betriebseinnahmeId_key`(`betriebseinnahmeId`);
|
||||
ALTER TABLE `buchungen` ADD UNIQUE INDEX `buchungen_betriebsausgabeId_key`(`betriebsausgabeId`);
|
||||
|
||||
-- Add foreign key constraints
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_betriebseinnahmeId_fkey` FOREIGN KEY (`betriebseinnahmeId`) REFERENCES `betriebseinnahmen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_betriebsausgabeId_fkey` FOREIGN KEY (`betriebsausgabeId`) REFERENCES `betriebsausgaben`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE `buchungen` ADD CONSTRAINT `buchungen_linkedBuchungId_fkey` FOREIGN KEY (`linkedBuchungId`) REFERENCES `buchungen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- Add indexes for linked fields
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_betriebsausgabeId_idx`(`betriebsausgabeId`);
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_linkedBuchungId_idx`(`linkedBuchungId`);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add buchungId field to invoices table
|
||||
ALTER TABLE `invoices` ADD COLUMN `buchungId` VARCHAR(191) NULL;
|
||||
|
||||
-- Create unique index for the foreign key
|
||||
ALTER TABLE `invoices` ADD UNIQUE INDEX `invoices_buchungId_key`(`buchungId`);
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE `invoices` ADD CONSTRAINT `invoices_buchungId_fkey`
|
||||
FOREIGN KEY (`buchungId`) REFERENCES `buchungen`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- CreateTable for BuchungKategorie
|
||||
CREATE TABLE `buchung_kategorien` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`companyId` VARCHAR(191) NOT NULL,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`typ` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `buchung_kategorien_companyId_idx`(`companyId`),
|
||||
UNIQUE INDEX `buchung_kategorien_companyId_name_typ_key`(`companyId`, `name`, `typ`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey for BuchungKategorie
|
||||
ALTER TABLE `buchung_kategorien` ADD CONSTRAINT `buchung_kategorien_companyId_fkey` FOREIGN KEY (`companyId`) REFERENCES `companies`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AlterTable `buchungen` - add new columns
|
||||
ALTER TABLE `buchungen` ADD COLUMN `kategorie` VARCHAR(191) NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `steuersatz` INT NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `zahlungsart` ENUM('KASSE', 'BANK') NULL;
|
||||
ALTER TABLE `buchungen` ADD COLUMN `isBusinessRecord` BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- Add index for isBusinessRecord
|
||||
ALTER TABLE `buchungen` ADD INDEX `buchungen_isBusinessRecord_idx`(`isBusinessRecord`);
|
||||
|
||||
-- Migrate existing data from betriebseinnahmen/betriebsausgaben to Buchung
|
||||
-- This is handled by the post-migration script
|
||||
|
||||
-- AlterTable `betriebseinnahmen` - change kategorie from Enum to String
|
||||
ALTER TABLE `betriebseinnahmen` MODIFY `kategorie` VARCHAR(191) NOT NULL;
|
||||
ALTER TABLE `betriebseinnahmen` MODIFY `steuersatz` INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable `betriebsausgaben` - change kategorie from Enum to String
|
||||
ALTER TABLE `betriebsausgaben` MODIFY `kategorie` VARCHAR(191) NOT NULL;
|
||||
ALTER TABLE `betriebsausgaben` MODIFY `steuersatz` INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Drop old foreign key constraints from buchungen (if they exist from previous migration)
|
||||
-- These will be re-added if needed, but for now we're consolidating
|
||||
|
||||
-- Note: The enum columns EinnahmeKategorie and AusgabeKategorie are automatically
|
||||
-- dropped by Prisma when they're no longer referenced in the schema.
|
||||
@@ -0,0 +1,61 @@
|
||||
-- Migration: Drop legacy betriebseinnahmen and betriebsausgaben tables
|
||||
-- This consolidates all transaction data into the buchungen table
|
||||
|
||||
-- STEP 1: Copy existing betriebseinnahmen into buchungen
|
||||
-- Only copy those that don't already have a linked Buchung (via betriebseinnahmeId)
|
||||
INSERT INTO `buchungen` (id, companyId, date, account, type, amount, description,
|
||||
kategorie, steuersatz, zahlungsart, isBusinessRecord, createdAt, updatedAt)
|
||||
SELECT
|
||||
CONCAT('migr-ein-', e.id),
|
||||
e.companyId,
|
||||
e.datum,
|
||||
e.zahlungsart,
|
||||
'EINLAGE',
|
||||
e.betrag,
|
||||
e.beschreibung,
|
||||
e.kategorie,
|
||||
e.steuersatz,
|
||||
e.zahlungsart,
|
||||
true,
|
||||
e.createdAt,
|
||||
e.updatedAt
|
||||
FROM `betriebseinnahmen` e
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `buchungen` b WHERE b.betriebseinnahmeId = e.id
|
||||
);
|
||||
|
||||
-- STEP 2: Copy existing betriebsausgaben into buchungen
|
||||
INSERT INTO `buchungen` (id, companyId, date, account, type, amount, description,
|
||||
kategorie, steuersatz, zahlungsart, isBusinessRecord, createdAt, updatedAt)
|
||||
SELECT
|
||||
CONCAT('migr-aus-', a.id),
|
||||
a.companyId,
|
||||
a.datum,
|
||||
a.zahlungsart,
|
||||
'ENTNAHME',
|
||||
a.betrag,
|
||||
a.beschreibung,
|
||||
a.kategorie,
|
||||
a.steuersatz,
|
||||
a.zahlungsart,
|
||||
true,
|
||||
a.createdAt,
|
||||
a.updatedAt
|
||||
FROM `betriebsausgaben` a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `buchungen` b WHERE b.betriebsausgabeId = a.id
|
||||
);
|
||||
|
||||
-- STEP 3: Remove FK constraints before dropping columns/tables
|
||||
ALTER TABLE `buchungen` DROP FOREIGN KEY `buchungen_betriebseinnahmeId_fkey`;
|
||||
ALTER TABLE `buchungen` DROP FOREIGN KEY `buchungen_betriebsausgabeId_fkey`;
|
||||
|
||||
-- STEP 4: Remove old linking columns from buchungen
|
||||
ALTER TABLE `buchungen` DROP INDEX `buchungen_betriebseinnahmeId_key`;
|
||||
ALTER TABLE `buchungen` DROP INDEX `buchungen_betriebsausgabeId_key`;
|
||||
ALTER TABLE `buchungen` DROP COLUMN `betriebseinnahmeId`;
|
||||
ALTER TABLE `buchungen` DROP COLUMN `betriebsausgabeId`;
|
||||
|
||||
-- STEP 5: Drop old tables
|
||||
DROP TABLE `betriebsausgaben`;
|
||||
DROP TABLE `betriebseinnahmen`;
|
||||
+83
-127
@@ -45,41 +45,53 @@ model AuditLog {
|
||||
}
|
||||
|
||||
model Company {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
legalForm String?
|
||||
taxId String?
|
||||
vatId String?
|
||||
address String
|
||||
zip String
|
||||
city String
|
||||
country String @default("DE")
|
||||
email String?
|
||||
phone String?
|
||||
website String?
|
||||
bankIban String?
|
||||
bankBic String?
|
||||
bankName String?
|
||||
invoicePrefix String @default("RE")
|
||||
invoiceSequence Int @default(0)
|
||||
kleinunternehmer Boolean @default(false)
|
||||
archived Boolean @default(false)
|
||||
archivedAt DateTime?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
customers Customer[]
|
||||
invoices Invoice[]
|
||||
services Service[]
|
||||
betriebsausgaben Betriebsausgabe[]
|
||||
betriebseinnahmen Betriebseinnahme[]
|
||||
anlagegueter Anlagegut[]
|
||||
buchungen Buchung[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
legalForm String?
|
||||
taxId String?
|
||||
vatId String?
|
||||
address String
|
||||
zip String
|
||||
city String
|
||||
country String @default("DE")
|
||||
email String?
|
||||
phone String?
|
||||
website String?
|
||||
bankIban String?
|
||||
bankBic String?
|
||||
bankName String?
|
||||
invoicePrefix String @default("RE")
|
||||
invoiceSequence Int @default(0)
|
||||
kleinunternehmer Boolean @default(false)
|
||||
archived Boolean @default(false)
|
||||
archivedAt DateTime?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
customers Customer[]
|
||||
invoices Invoice[]
|
||||
services Service[]
|
||||
anlagegueter Anlagegut[]
|
||||
buchungen Buchung[]
|
||||
kategorien BuchungKategorie[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("companies")
|
||||
}
|
||||
|
||||
model BuchungKategorie {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
name String // e.g., "Fußpflege", "Miete", "Privateinlagen"
|
||||
typ String // "EINNAHME" or "AUSGABE"
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([companyId, name, typ])
|
||||
@@index([companyId])
|
||||
@@map("buchung_kategorien")
|
||||
}
|
||||
|
||||
enum TransactionAccount {
|
||||
KASSE
|
||||
BANK
|
||||
@@ -91,19 +103,28 @@ enum TransactionType {
|
||||
}
|
||||
|
||||
model Buchung {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
date DateTime
|
||||
account TransactionAccount
|
||||
type TransactionType
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
date DateTime
|
||||
account TransactionAccount
|
||||
type TransactionType
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
description String? @db.Text
|
||||
kategorie String? // Name of BuchungKategorie (nullable for manual transactions)
|
||||
steuersatz Int? // Tax rate: 0, 7, 19 (nullable for non-business records)
|
||||
zahlungsart Zahlungsart? // KASSE or BANK
|
||||
isBusinessRecord Boolean @default(false) // True if this is from Einnahme/Ausgabe
|
||||
linkedBuchungId String?
|
||||
linkedBuchung Buchung? @relation("BuchungLink", fields: [linkedBuchungId], references: [id], onDelete: SetNull)
|
||||
linkedFrom Buchung[] @relation("BuchungLink")
|
||||
invoice Invoice? // Back-relation: Invoice -> Buchung (via buchungId)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([date])
|
||||
@@index([isBusinessRecord])
|
||||
@@map("buchungen")
|
||||
}
|
||||
|
||||
@@ -142,25 +163,27 @@ model Customer {
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
id String @id @default(cuid())
|
||||
number String?
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
issueDate DateTime
|
||||
deliveryDate DateTime?
|
||||
dueDate DateTime
|
||||
status InvoiceStatus @default(DRAFT)
|
||||
kleinunternehmer Boolean @default(false)
|
||||
notes String? @db.Text
|
||||
items InvoiceItem[]
|
||||
netTotal Decimal @db.Decimal(10, 2)
|
||||
taxTotal Decimal @db.Decimal(10, 2)
|
||||
grossTotal Decimal @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
id String @id @default(cuid())
|
||||
number String?
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
issueDate DateTime
|
||||
deliveryDate DateTime?
|
||||
dueDate DateTime
|
||||
status InvoiceStatus @default(DRAFT)
|
||||
kleinunternehmer Boolean @default(false)
|
||||
notes String? @db.Text
|
||||
items InvoiceItem[]
|
||||
netTotal Decimal @db.Decimal(10, 2)
|
||||
taxTotal Decimal @db.Decimal(10, 2)
|
||||
grossTotal Decimal @db.Decimal(10, 2)
|
||||
buchungId String? @unique // Link to auto-created Buchung when PAID
|
||||
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@unique([companyId, number])
|
||||
@@map("invoices")
|
||||
@@ -179,36 +202,6 @@ enum Zahlungsart {
|
||||
BANK
|
||||
}
|
||||
|
||||
enum EinnahmeKategorie {
|
||||
FUSSPFLEGE
|
||||
PRIVATEINLAGEN
|
||||
DARLEHEN
|
||||
STEUERERSTATTUNGEN
|
||||
VERSICHERUNGSERSTATTUNGEN
|
||||
ZINSERTRAEGE
|
||||
VERMIETUNG_VERPACHTUNG
|
||||
VERAEUSSERUNGSERLOES
|
||||
EIGENVERBRAUCH
|
||||
SONSTIGE_EINNAHMEN
|
||||
}
|
||||
|
||||
model Betriebseinnahme {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
kategorie EinnahmeKategorie
|
||||
betrag Decimal @db.Decimal(10, 2)
|
||||
steuersatz Decimal @db.Decimal(5, 2) @default(0)
|
||||
zahlungsart Zahlungsart @default(BANK)
|
||||
datum DateTime
|
||||
beschreibung String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([datum])
|
||||
@@map("betriebseinnahmen")
|
||||
}
|
||||
|
||||
model Anlagegut {
|
||||
id String @id @default(cuid())
|
||||
@@ -228,43 +221,6 @@ model Anlagegut {
|
||||
@@map("anlagegueter")
|
||||
}
|
||||
|
||||
enum AusgabeKategorie {
|
||||
WAREN_ROHSTOFFE
|
||||
GERINGWERTIGE_WIRTSCHAFTSGUETER
|
||||
ABSCHREIBUNGEN
|
||||
MIETE
|
||||
STROM_WASSER
|
||||
TELEKOMMUNIKATION
|
||||
FORTBILDUNG_MESSEN
|
||||
BEITRAEGE
|
||||
VERSICHERUNGEN
|
||||
WERBEKOSTEN
|
||||
ZINSEN
|
||||
REISEKOSTEN
|
||||
REPARATUREN_INSTANDHALTUNG
|
||||
BUEROBEDARF
|
||||
REPRAESENTATIONSKOSTEN
|
||||
SONSTIGER_BETRIEBSBEDARF
|
||||
NEBENKOSTEN_GELDVERKEHR
|
||||
}
|
||||
|
||||
model Betriebsausgabe {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
kategorie AusgabeKategorie
|
||||
betrag Decimal @db.Decimal(10, 2)
|
||||
steuersatz Decimal @db.Decimal(5, 2) @default(0)
|
||||
zahlungsart Zahlungsart @default(BANK)
|
||||
datum DateTime
|
||||
beschreibung String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([datum])
|
||||
@@map("betriebsausgaben")
|
||||
}
|
||||
|
||||
model InvoiceItem {
|
||||
id String @id @default(cuid())
|
||||
|
||||
+53
-1
@@ -136,8 +136,60 @@ async function main() {
|
||||
data: { invoiceSequence: 1 },
|
||||
});
|
||||
|
||||
// Seed BuchungKategorien for demo company
|
||||
const einnahmeKategorien = [
|
||||
"Fußpflege",
|
||||
"Privateinlagen",
|
||||
"Darlehen",
|
||||
"Steuererstattungen",
|
||||
"Versicherungserstattungen",
|
||||
"Zinsertrage",
|
||||
"Vermietung/Verpachtung",
|
||||
"Veräußerungserlös",
|
||||
"Eigenverbrauch",
|
||||
"Sonstige Einnahmen",
|
||||
];
|
||||
|
||||
const ausgabeKategorien = [
|
||||
"Waren und Rohstoffe",
|
||||
"Geringwertige Wirtschaftsgüter",
|
||||
"Abschreibungen",
|
||||
"Miete",
|
||||
"Strom und Wasser",
|
||||
"Telekommunikation",
|
||||
"Fortbildung und Messen",
|
||||
"Beiträge",
|
||||
"Versicherungen",
|
||||
"Werbekosten",
|
||||
"Zinsen",
|
||||
"Reisekosten",
|
||||
"Reparaturen und Instandhaltung",
|
||||
"Bürobedarf",
|
||||
"Repräsentationskosten",
|
||||
"Sonstiger Betriebsbedarf",
|
||||
"Nebenkosten Geldverkehr",
|
||||
];
|
||||
|
||||
for (const name of einnahmeKategorien) {
|
||||
await prisma.buchungKategorie.upsert({
|
||||
where: { companyId_name_typ: { companyId: company.id, name, typ: "EINNAHME" } },
|
||||
update: {},
|
||||
create: { companyId: company.id, name, typ: "EINNAHME" },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${einnahmeKategorien.length} Einnahme categories`);
|
||||
|
||||
for (const name of ausgabeKategorien) {
|
||||
await prisma.buchungKategorie.upsert({
|
||||
where: { companyId_name_typ: { companyId: company.id, name, typ: "AUSGABE" } },
|
||||
update: {},
|
||||
create: { companyId: company.id, name, typ: "AUSGABE" },
|
||||
});
|
||||
}
|
||||
console.log(`✓ Seeded ${ausgabeKategorien.length} Ausgabe categories`);
|
||||
|
||||
console.log("\n✅ Seed complete!");
|
||||
console.log("Login: anna@example.de / demo123");
|
||||
console.log("Login: anna@example.de / annas_password");
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user