Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style

This commit is contained in:
2026-02-27 22:47:08 +01:00
committed by erik
parent 5a70f775f1
commit 93faae5cc8
1463 changed files with 306917 additions and 0 deletions

13
pirp/tools/hash.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
// Passwort-Hash-Generator für Admin-User
if (PHP_SAPI !== 'cli') {
echo "Nur CLI.\n";
exit(1);
}
if ($argc < 2) {
echo "Verwendung: php tools/hash.php SUPERGEHEIMES_PASSWORT\n";
exit(1);
}
$password = $argv[1];
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash . "\n";

View File

@@ -0,0 +1,149 @@
<?php
/**
* Import: Kategorien aus "Journal 2021.xlsx" in die Datenbank importieren.
* Prüft vor jedem Insert auf Duplikate (Name + Type/Side).
*
* Ausführen: php tools/import_excel_categories.php
*/
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
$pdo = get_db();
echo "Import: Kategorien aus Excel-Tabelle\n";
echo str_repeat('=', 50) . "\n";
$inserted = 0;
$skipped = 0;
// ============================================================
// 1. journal_revenue_categories - Wareneingang
// ============================================================
echo "\n--- Wareneingang-Kategorien ---\n";
$wareneingang = [
['name' => 'Handy 0%', 'vat_rate' => 0, 'sort_order' => 1],
['name' => 'PVG 7%', 'vat_rate' => 7, 'sort_order' => 2],
['name' => 'Zig. 19%', 'vat_rate' => 19, 'sort_order' => 3],
['name' => 'Allg. 19%', 'vat_rate' => 19, 'sort_order' => 4],
];
$check_rev = $pdo->prepare('SELECT COUNT(*) FROM journal_revenue_categories WHERE name = :name AND category_type = :type');
$insert_rev = $pdo->prepare('INSERT INTO journal_revenue_categories (name, category_type, vat_rate, sort_order, is_active) VALUES (:name, :type, :vat, :sort, TRUE)');
foreach ($wareneingang as $cat) {
$check_rev->execute([':name' => $cat['name'], ':type' => 'wareneingang']);
if ($check_rev->fetchColumn() > 0) {
echo " SKIP: {$cat['name']} (existiert bereits)\n";
$skipped++;
continue;
}
$insert_rev->execute([
':name' => $cat['name'],
':type' => 'wareneingang',
':vat' => $cat['vat_rate'],
':sort' => $cat['sort_order'],
]);
echo " OK: {$cat['name']}\n";
$inserted++;
}
// ============================================================
// 2. journal_revenue_categories - Erlöse
// ============================================================
echo "\n--- Erlös-Kategorien ---\n";
$erloese = [
['name' => 'DP/Handy 0%', 'vat_rate' => 0, 'sort_order' => 1],
['name' => 'PVG 7%', 'vat_rate' => 7, 'sort_order' => 2],
['name' => 'Zig. 19%', 'vat_rate' => 19, 'sort_order' => 3],
['name' => 'Allg. 19%', 'vat_rate' => 19, 'sort_order' => 4],
['name' => 'Sonst. 19%', 'vat_rate' => 19, 'sort_order' => 5],
];
foreach ($erloese as $cat) {
$check_rev->execute([':name' => $cat['name'], ':type' => 'erloese']);
if ($check_rev->fetchColumn() > 0) {
echo " SKIP: {$cat['name']} (existiert bereits)\n";
$skipped++;
continue;
}
$insert_rev->execute([
':name' => $cat['name'],
':type' => 'erloese',
':vat' => $cat['vat_rate'],
':sort' => $cat['sort_order'],
]);
echo " OK: {$cat['name']}\n";
$inserted++;
}
// ============================================================
// 3. journal_deduction_categories
// ============================================================
echo "\n--- Abzugs-Kategorien ---\n";
$deductions = [
['name' => 'Skonto', 'sort_order' => 1],
['name' => 'Lotto', 'sort_order' => 2],
];
$check_ded = $pdo->prepare('SELECT COUNT(*) FROM journal_deduction_categories WHERE name = :name');
$insert_ded = $pdo->prepare('INSERT INTO journal_deduction_categories (name, sort_order, is_active) VALUES (:name, :sort, TRUE)');
foreach ($deductions as $cat) {
$check_ded->execute([':name' => $cat['name']]);
if ($check_ded->fetchColumn() > 0) {
echo " SKIP: {$cat['name']} (existiert bereits)\n";
$skipped++;
continue;
}
$insert_ded->execute([
':name' => $cat['name'],
':sort' => $cat['sort_order'],
]);
echo " OK: {$cat['name']}\n";
$inserted++;
}
// ============================================================
// 4. journal_expense_categories (alle side=soll)
// ============================================================
echo "\n--- Aufwands-Kategorien ---\n";
$expenses = [
['name' => 'Bankunkosten', 'sort_order' => 1],
['name' => 'KFZ-kosten', 'sort_order' => 2],
['name' => 'Personalkosten', 'sort_order' => 3],
['name' => 'Raumkosten', 'sort_order' => 4],
['name' => 'Tel./Fax', 'sort_order' => 5],
['name' => 'Allg./Werbekosten', 'sort_order' => 6],
['name' => 'Sonst. Deko', 'sort_order' => 7],
['name' => 'Bezugsnebenko.', 'sort_order' => 8],
];
$check_exp = $pdo->prepare('SELECT COUNT(*) FROM journal_expense_categories WHERE name = :name AND side = :side');
$insert_exp = $pdo->prepare('INSERT INTO journal_expense_categories (name, side, sort_order, is_active) VALUES (:name, :side, :sort, TRUE)');
foreach ($expenses as $cat) {
$check_exp->execute([':name' => $cat['name'], ':side' => 'soll']);
if ($check_exp->fetchColumn() > 0) {
echo " SKIP: {$cat['name']} (existiert bereits)\n";
$skipped++;
continue;
}
$insert_exp->execute([
':name' => $cat['name'],
':side' => 'soll',
':sort' => $cat['sort_order'],
]);
echo " OK: {$cat['name']}\n";
$inserted++;
}
// ============================================================
// Ergebnis
// ============================================================
echo "\n" . str_repeat('=', 50) . "\n";
echo "Fertig! Eingefügt: $inserted, Übersprungen: $skipped\n";

View File

@@ -0,0 +1,44 @@
<?php
/**
* Migration: Erstellt journal_deduction_categories Tabelle für customizable Abzüge
* Ausführen: php tools/migrate_deductions.php
*/
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
$pdo = get_db();
echo "Migration: journal_deduction_categories\n";
try {
// Prüfen ob Tabelle existiert
$stmt = $pdo->query("SELECT to_regclass('public.journal_deduction_categories')");
$exists = $stmt->fetchColumn();
if ($exists) {
echo "Tabelle journal_deduction_categories existiert bereits.\n";
} else {
// Tabelle erstellen
$pdo->exec("CREATE TABLE journal_deduction_categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
echo "Tabelle journal_deduction_categories erstellt.\n";
// Standard-Abzüge einfügen
$pdo->exec("INSERT INTO journal_deduction_categories (name, sort_order, is_active) VALUES
('Skonto', 1, TRUE),
('Lotto', 2, TRUE)
");
echo "Standard-Abzüge (Skonto, Lotto) eingefügt.\n";
}
echo "Migration erfolgreich!\n";
} catch (PDOException $e) {
echo "Fehler: " . $e->getMessage() . "\n";
exit(1);
}

View File

@@ -0,0 +1,72 @@
-- PIRP Vollständige Migration (pirp -> pirp-dev)
-- Erstellt am: 2026-02-07
--
-- Führt alle neuen Migrationen in der richtigen Reihenfolge aus:
-- 1. invoice_id für journal_entries (Rechnungsverknüpfung)
-- 2. Zahlungsdaten und MwSt-Felder
-- 3. Unique Constraints
BEGIN;
-- ============================================================
-- 1. Journal-Rechnungen-Verknüpfung (migrate_journal_invoice_link.sql)
-- ============================================================
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id);
-- ============================================================
-- 2. Journal-Auto-Buchung (migrate_journal_auto.sql)
-- ============================================================
-- 2a. Rechnungen: Zahlungsdatum hinzufügen
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS payment_date DATE;
-- Bestehende bezahlte Rechnungen: invoice_date als Fallback
UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL;
-- 2b. Ausgaben: MwSt-Felder und Kategorien-Verknüpfung
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS vat_rate NUMERIC(5,2) DEFAULT 0;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_net NUMERIC(12,2);
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_vat NUMERIC(12,2) DEFAULT 0;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS payment_date DATE;
-- Bestehende Ausgaben: amount als Netto (kein MwSt-Split bekannt), Zahlungsdatum = Ausgabedatum
UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL;
UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL;
-- 2c. Journal-Einträge: Ausgaben-Verknüpfung und Quell-Typ
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS expense_id INTEGER REFERENCES expenses(id) ON DELETE SET NULL;
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'manual';
CREATE INDEX IF NOT EXISTS idx_journal_entries_expense ON journal_entries(expense_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_source ON journal_entries(source_type);
-- Bestehende Rechnungs-Buchungen als source_type markieren
UPDATE journal_entries SET source_type = 'invoice_payment' WHERE invoice_id IS NOT NULL AND source_type = 'manual';
-- ============================================================
-- 3. Unique Constraints (migrate_unique_constraints.sql)
-- ============================================================
-- Zuerst evtl. vorhandene Duplikate bereinigen (behalte nur den neuesten)
DELETE FROM journal_entries a
USING journal_entries b
WHERE a.invoice_id IS NOT NULL
AND a.invoice_id = b.invoice_id
AND a.id < b.id;
DELETE FROM journal_entries a
USING journal_entries b
WHERE a.expense_id IS NOT NULL
AND a.expense_id = b.expense_id
AND a.id < b.id;
-- Unique-Indexes erstellen (nur wenn nicht vorhanden)
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_invoice ON journal_entries(invoice_id) WHERE invoice_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_expense ON journal_entries(expense_id) WHERE expense_id IS NOT NULL;
COMMIT;
-- Erfolgsmeldung
SELECT 'Migration erfolgreich abgeschlossen' AS status;

View File

@@ -0,0 +1,98 @@
-- Journal-Modul: Buchführungsjournal für PIRP
-- Migration: Alle Journal-Tabellen erstellen
-- Jahre
CREATE TABLE IF NOT EXISTS journal_years (
id SERIAL PRIMARY KEY,
year INTEGER NOT NULL UNIQUE,
is_closed BOOLEAN NOT NULL DEFAULT FALSE,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Lieferanten
CREATE TABLE IF NOT EXISTS journal_suppliers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Erlös-/Wareneingang-Kategorien
CREATE TABLE IF NOT EXISTS journal_revenue_categories (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category_type VARCHAR(20) NOT NULL CHECK (category_type IN ('wareneingang', 'erloese')),
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Aufwandskategorien
CREATE TABLE IF NOT EXISTS journal_expense_categories (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
side VARCHAR(10) NOT NULL DEFAULT 'soll' CHECK (side IN ('soll', 'soll_haben')),
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Journal-Einträge (Buchungen)
CREATE TABLE IF NOT EXISTS journal_entries (
id SERIAL PRIMARY KEY,
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE RESTRICT,
entry_date DATE NOT NULL,
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
description TEXT NOT NULL,
attachment_note TEXT,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
supplier_id INTEGER REFERENCES journal_suppliers(id) ON DELETE SET NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Kontenverteilung pro Buchung
CREATE TABLE IF NOT EXISTS journal_entry_accounts (
id SERIAL PRIMARY KEY,
entry_id INTEGER NOT NULL REFERENCES journal_entries(id) ON DELETE CASCADE,
account_type VARCHAR(20) NOT NULL,
side VARCHAR(5) NOT NULL CHECK (side IN ('soll', 'haben')),
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
revenue_category_id INTEGER REFERENCES journal_revenue_categories(id) ON DELETE SET NULL,
expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL,
note TEXT
);
-- Monatliche Zusammenfassung (manuelle Korrekturen)
CREATE TABLE IF NOT EXISTS journal_monthly_summary (
id SERIAL PRIMARY KEY,
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
manual_corrections JSONB DEFAULT '{}',
notes TEXT,
UNIQUE(year_id, month)
);
-- Umsatz-Zusammenfassungsposten (z.B. "Reinigung", "RMV", "Handy")
CREATE TABLE IF NOT EXISTS journal_summary_items (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Monatliche Werte für Zusammenfassungsposten
CREATE TABLE IF NOT EXISTS journal_monthly_summary_values (
id SERIAL PRIMARY KEY,
year_id INTEGER NOT NULL REFERENCES journal_years(id) ON DELETE CASCADE,
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
summary_item_id INTEGER NOT NULL REFERENCES journal_summary_items(id) ON DELETE CASCADE,
amount NUMERIC(12,2) NOT NULL DEFAULT 0,
UNIQUE(year_id, month, summary_item_id)
);
-- Indizes für Performance
CREATE INDEX IF NOT EXISTS idx_journal_entries_year_month ON journal_entries(year_id, month);
CREATE INDEX IF NOT EXISTS idx_journal_entries_date ON journal_entries(entry_date);
CREATE INDEX IF NOT EXISTS idx_journal_entry_accounts_entry ON journal_entry_accounts(entry_id);
CREATE INDEX IF NOT EXISTS idx_journal_entry_accounts_type ON journal_entry_accounts(account_type);

View File

@@ -0,0 +1,30 @@
-- Journal-Auto-Buchung: Datenbank-Migration
-- Erweitert invoices, expenses und journal_entries für automatische Journaleinträge
-- Zufluss-/Abflussprinzip (§ 11 EStG)
-- 1. Rechnungen: Zahlungsdatum hinzufügen
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS payment_date DATE;
-- Bestehende bezahlte Rechnungen: invoice_date als Fallback
UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL;
-- 2. Ausgaben: MwSt-Felder und Kategorien-Verknüpfung
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS vat_rate NUMERIC(5,2) DEFAULT 0;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_net NUMERIC(12,2);
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS total_vat NUMERIC(12,2) DEFAULT 0;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS expense_category_id INTEGER REFERENCES journal_expense_categories(id) ON DELETE SET NULL;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS payment_date DATE;
-- Bestehende Ausgaben: amount als Netto (kein MwSt-Split bekannt), Zahlungsdatum = Ausgabedatum
UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL;
UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL;
-- 3. Journal-Einträge: Ausgaben-Verknüpfung und Quell-Typ
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS expense_id INTEGER REFERENCES expenses(id) ON DELETE SET NULL;
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'manual';
CREATE INDEX IF NOT EXISTS idx_journal_entries_expense ON journal_entries(expense_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_source ON journal_entries(source_type);
-- Bestehende Rechnungs-Buchungen als source_type markieren
UPDATE journal_entries SET source_type = 'invoice_payment' WHERE invoice_id IS NOT NULL AND source_type = 'manual';

View File

@@ -0,0 +1,96 @@
<?php
/**
* Migrationstool: Erstellt nachträglich Journalbuchungen für alle bezahlten
* Rechnungen und Ausgaben, die noch keinen Journaleintrag haben.
*
* Setzt außerdem payment_date für bestehende bezahlte Belege, falls noch nicht gesetzt.
*
* Ausführung: php tools/migrate_journal_entries.php
*/
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/invoice_functions.php';
require_once __DIR__ . '/../src/journal_functions.php';
require_once __DIR__ . '/../src/expense_functions.php';
echo "=== PIRP Journal-Migration ===\n\n";
$pdo = get_db();
// 1. payment_date für bezahlte Rechnungen setzen (Fallback: invoice_date)
echo "1. Payment-Dates für Rechnungen setzen...\n";
$stmt = $pdo->query("UPDATE invoices SET payment_date = invoice_date WHERE paid = TRUE AND payment_date IS NULL");
$count = $stmt->rowCount();
echo " $count Rechnungen aktualisiert.\n\n";
// 2. payment_date für bezahlte Ausgaben setzen (Fallback: expense_date)
echo "2. Payment-Dates für Ausgaben setzen...\n";
$stmt = $pdo->query("UPDATE expenses SET payment_date = expense_date WHERE paid = TRUE AND payment_date IS NULL");
$count = $stmt->rowCount();
echo " $count Ausgaben aktualisiert.\n\n";
// 3. total_net für Ausgaben setzen (Fallback: amount, keine MwSt)
echo "3. Netto-Beträge für Ausgaben setzen...\n";
$stmt = $pdo->query("UPDATE expenses SET total_net = amount, total_vat = 0 WHERE total_net IS NULL");
$count = $stmt->rowCount();
echo " $count Ausgaben aktualisiert.\n\n";
// 4. Bezahlte Rechnungen ohne Journaleintrag
echo "4. Bezahlte Rechnungen ohne Journalbuchung...\n";
$stmt = $pdo->query("SELECT i.id, i.invoice_number, i.total_gross, i.payment_date, c.name AS customer_name
FROM invoices i
JOIN customers c ON c.id = i.customer_id
LEFT JOIN journal_entries je ON je.invoice_id = i.id
WHERE i.paid = TRUE AND je.id IS NULL
ORDER BY i.invoice_date ASC");
$unbooked_invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
$inv_ok = 0;
$inv_err = 0;
foreach ($unbooked_invoices as $inv) {
try {
$entry_id = create_journal_entry_from_invoice((int)$inv['id']);
echo " OK: {$inv['invoice_number']} ({$inv['customer_name']}) -> Journal #{$entry_id}\n";
$inv_ok++;
} catch (Exception $e) {
echo " FEHLER: {$inv['invoice_number']}: {$e->getMessage()}\n";
$inv_err++;
}
}
echo " Ergebnis: $inv_ok erstellt, $inv_err Fehler.\n\n";
// 5. Bezahlte Ausgaben ohne Journaleintrag
echo "5. Bezahlte Ausgaben ohne Journalbuchung...\n";
$stmt = $pdo->query("SELECT e.id, e.description, e.amount, e.payment_date
FROM expenses e
LEFT JOIN journal_entries je ON je.expense_id = e.id
WHERE e.paid = TRUE AND je.id IS NULL
ORDER BY e.expense_date ASC");
$unbooked_expenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
$exp_ok = 0;
$exp_err = 0;
foreach ($unbooked_expenses as $exp) {
try {
$entry_id = create_journal_entry_from_expense((int)$exp['id']);
echo " OK: \"{$exp['description']}\" ({$exp['amount']} EUR) -> Journal #{$entry_id}\n";
$exp_ok++;
} catch (Exception $e) {
echo " FEHLER: \"{$exp['description']}\": {$e->getMessage()}\n";
$exp_err++;
}
}
echo " Ergebnis: $exp_ok erstellt, $exp_err Fehler.\n\n";
// Zusammenfassung
echo "=== Zusammenfassung ===\n";
echo "Rechnungen: $inv_ok gebucht" . ($inv_err > 0 ? ", $inv_err Fehler" : '') . "\n";
echo "Ausgaben: $exp_ok gebucht" . ($exp_err > 0 ? ", $exp_err Fehler" : '') . "\n";
$total = $inv_ok + $exp_ok;
if ($total === 0) {
echo "\nKeine fehlenden Buchungen gefunden. Alles aktuell.\n";
} else {
echo "\n$total Buchungen insgesamt erstellt.\n";
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Migration: Fügt invoice_id Spalte zu journal_entries hinzu
* Ausführen: php tools/migrate_journal_invoice_link.php
*/
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
$pdo = get_db();
echo "Migration: journal_entries.invoice_id\n";
try {
// Prüfen ob Spalte existiert
$stmt = $pdo->query("SELECT column_name FROM information_schema.columns
WHERE table_name = 'journal_entries' AND column_name = 'invoice_id'");
$exists = $stmt->fetch();
if ($exists) {
echo "Spalte invoice_id existiert bereits.\n";
} else {
// Spalte hinzufügen
$pdo->exec("ALTER TABLE journal_entries ADD COLUMN invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL");
echo "Spalte invoice_id hinzugefügt.\n";
// Index erstellen
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id)");
echo "Index erstellt.\n";
}
echo "Migration erfolgreich!\n";
} catch (PDOException $e) {
echo "Fehler: " . $e->getMessage() . "\n";
exit(1);
}

View File

@@ -0,0 +1,5 @@
-- Journal-Rechnungen-Verknüpfung
-- Fügt invoice_id zu journal_entries hinzu
ALTER TABLE journal_entries ADD COLUMN IF NOT EXISTS invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_journal_entries_invoice ON journal_entries(invoice_id);

View File

@@ -0,0 +1,15 @@
-- GoBD-konforme PDF-Speicherung: Erweiterung der invoices-Tabelle
-- Migration für unveränderliche PDF-Archivierung
-- Ausführen mit: psql -U pirp_user -d pirp -f tools/migrate_pdf.sql
-- Pfad zur archivierten PDF-Datei (relativ zu public/uploads/invoices/)
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_path TEXT;
-- SHA-256 Hash des PDF-Inhalts zur Integritätsprüfung
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_hash VARCHAR(64);
-- Zeitstempel der PDF-Generierung
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_generated_at TIMESTAMPTZ;
-- Index für schnelle Abfragen nach fehlenden PDFs
CREATE INDEX IF NOT EXISTS idx_invoices_pdf_path ON invoices(pdf_path) WHERE pdf_path IS NULL;

View File

@@ -0,0 +1,53 @@
<?php
/**
* Migrationsscript: Generiert PDFs für alle bestehenden Rechnungen
*
* ACHTUNG: Einmalig ausführen!
* Bei bestehenden Rechnungen werden die AKTUELLEN Firmen-/Kundendaten verwendet.
*
* Ausführung: php tools/migrate_pdfs.php
*/
if (PHP_SAPI !== 'cli') {
echo "Nur CLI.\n";
exit(1);
}
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/pdf_functions.php';
echo "PDF-Migration gestartet...\n";
// Verzeichnis erstellen falls nicht vorhanden
$uploadDir = __DIR__ . '/../public/uploads/invoices';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
echo "Verzeichnis erstellt: $uploadDir\n";
}
$pdo = get_db();
$stmt = $pdo->query("SELECT id, invoice_number FROM invoices WHERE pdf_path IS NULL ORDER BY id");
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Gefunden: " . count($invoices) . " Rechnungen ohne archivierte PDF\n";
$success = 0;
$failed = 0;
foreach ($invoices as $inv) {
echo "Verarbeite {$inv['invoice_number']}... ";
$result = archive_invoice_pdf($inv['id']);
if ($result) {
echo "OK -> $result\n";
$success++;
} else {
echo "FEHLER\n";
$failed++;
}
}
echo "\nMigration abgeschlossen.\n";
echo "Erfolgreich: $success\n";
echo "Fehlgeschlagen: $failed\n";

View File

@@ -0,0 +1,42 @@
-- Wiederkehrende Rechnungen (Abo-Rechnungen)
-- Migration ausführen mit: psql -U pirp_user -d pirp -f tools/migrate_recurring.sql
-- Abo-Vorlagen
CREATE TABLE IF NOT EXISTS recurring_templates (
id SERIAL PRIMARY KEY,
template_name VARCHAR(100) NOT NULL,
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
interval_type VARCHAR(20) NOT NULL CHECK (interval_type IN ('monthly', 'quarterly', 'yearly')),
start_date DATE NOT NULL,
end_date DATE,
next_due_date DATE NOT NULL,
vat_mode VARCHAR(10) NOT NULL DEFAULT 'klein',
vat_rate NUMERIC(5,2) NOT NULL DEFAULT 19.00,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
notes_internal TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Positionen der Abo-Vorlage
CREATE TABLE IF NOT EXISTS recurring_template_items (
id SERIAL PRIMARY KEY,
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
position_no INTEGER NOT NULL,
description TEXT NOT NULL,
quantity NUMERIC(12,2) NOT NULL DEFAULT 1,
unit_price NUMERIC(12,2) NOT NULL DEFAULT 0
);
-- Log der generierten Rechnungen
CREATE TABLE IF NOT EXISTS recurring_log (
id SERIAL PRIMARY KEY,
template_id INTEGER NOT NULL REFERENCES recurring_templates(id) ON DELETE CASCADE,
invoice_id INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
generated_at TIMESTAMPTZ DEFAULT now(),
due_date DATE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'generated'
);
-- Indices
CREATE INDEX IF NOT EXISTS idx_recurring_next_due ON recurring_templates(next_due_date) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_recurring_log_template ON recurring_log(template_id);

View File

@@ -0,0 +1,20 @@
-- Migration: Storno-Rechnungen + Mahnungen
-- Anwendung: psql -U pirp_user -d pirp -f tools/migrate_storno_mahnung.sql
-- Storno: zwei neue Spalten in invoices
ALTER TABLE invoices
ADD COLUMN IF NOT EXISTS storno_of INTEGER REFERENCES invoices(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS is_storno BOOLEAN NOT NULL DEFAULT FALSE;
-- Mahnungen: neue Tabelle
CREATE TABLE IF NOT EXISTS mahnungen (
id SERIAL PRIMARY KEY,
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
mahnung_date DATE NOT NULL,
level INTEGER NOT NULL DEFAULT 1 CHECK (level IN (1,2,3)),
fee_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
pdf_path TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_mahnungen_invoice_id ON mahnungen(invoice_id);

View File

@@ -0,0 +1,19 @@
-- Migration: Unique-Constraints für Journal-Einträge
-- Verhindert Doppelbuchungen (max. 1 Journal-Eintrag pro Rechnung/Ausgabe)
-- Zuerst evtl. vorhandene Duplikate bereinigen (behalte nur den neuesten)
DELETE FROM journal_entries a
USING journal_entries b
WHERE a.invoice_id IS NOT NULL
AND a.invoice_id = b.invoice_id
AND a.id < b.id;
DELETE FROM journal_entries a
USING journal_entries b
WHERE a.expense_id IS NOT NULL
AND a.expense_id = b.expense_id
AND a.id < b.id;
-- Unique-Indexes erstellen
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_invoice ON journal_entries(invoice_id) WHERE invoice_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_journal_expense ON journal_entries(expense_id) WHERE expense_id IS NOT NULL;

View File

@@ -0,0 +1,75 @@
<?php
/**
* Einmalige Migration: Spalten hinzufügen und PDFs generieren
*/
if (PHP_SAPI !== 'cli') {
die("Nur CLI.\n");
}
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
$pdo = get_db();
echo "1. Prüfe/erstelle Spalten...\n";
// Spalten hinzufügen falls nicht vorhanden
$migrations = [
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_path TEXT",
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_hash VARCHAR(64)",
"ALTER TABLE invoices ADD COLUMN IF NOT EXISTS pdf_generated_at TIMESTAMPTZ",
];
foreach ($migrations as $sql) {
try {
$pdo->exec($sql);
echo " OK: $sql\n";
} catch (PDOException $e) {
echo " FEHLER: " . $e->getMessage() . "\n";
}
}
// Index erstellen
try {
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_invoices_pdf_path ON invoices(pdf_path) WHERE pdf_path IS NULL");
echo " OK: Index erstellt\n";
} catch (PDOException $e) {
// Index existiert möglicherweise schon
}
echo "\n2. Generiere PDFs für alte Rechnungen...\n";
require_once __DIR__ . '/../src/pdf_functions.php';
// Verzeichnis erstellen
$uploadDir = __DIR__ . '/../public/uploads/invoices';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
echo " Verzeichnis erstellt: $uploadDir\n";
}
$stmt = $pdo->query("SELECT id, invoice_number FROM invoices WHERE pdf_path IS NULL ORDER BY id");
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo " Gefunden: " . count($invoices) . " Rechnungen ohne PDF\n\n";
$success = 0;
$failed = 0;
foreach ($invoices as $inv) {
echo " Verarbeite {$inv['invoice_number']}... ";
$result = archive_invoice_pdf($inv['id']);
if ($result) {
echo "OK -> $result\n";
$success++;
} else {
echo "FEHLER\n";
$failed++;
}
}
echo "\n=== Migration abgeschlossen ===\n";
echo "Erfolgreich: $success\n";
echo "Fehlgeschlagen: $failed\n";

4
pirp/tools/seed_dev.sql Normal file
View File

@@ -0,0 +1,4 @@
-- Dev-Seed: Default-Login admin:admin
INSERT INTO users (username, password_hash)
VALUES ('admin', '$2b$10$PDnwnHN/2V3HrYqnfZtZUOtSHkIynW9olAp9W9RB4FZsh7KbKW5jq')
ON CONFLICT (username) DO NOTHING;