Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
13
pirp/tools/hash.php
Normal file
13
pirp/tools/hash.php
Normal 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";
|
||||
149
pirp/tools/import_excel_categories.php
Normal file
149
pirp/tools/import_excel_categories.php
Normal 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";
|
||||
44
pirp/tools/migrate_deductions.php
Normal file
44
pirp/tools/migrate_deductions.php
Normal 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);
|
||||
}
|
||||
72
pirp/tools/migrate_full_update.sql
Normal file
72
pirp/tools/migrate_full_update.sql
Normal 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;
|
||||
98
pirp/tools/migrate_journal.sql
Normal file
98
pirp/tools/migrate_journal.sql
Normal 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);
|
||||
30
pirp/tools/migrate_journal_auto.sql
Normal file
30
pirp/tools/migrate_journal_auto.sql
Normal 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';
|
||||
96
pirp/tools/migrate_journal_entries.php
Normal file
96
pirp/tools/migrate_journal_entries.php
Normal 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";
|
||||
}
|
||||
36
pirp/tools/migrate_journal_invoice_link.php
Normal file
36
pirp/tools/migrate_journal_invoice_link.php
Normal 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);
|
||||
}
|
||||
5
pirp/tools/migrate_journal_invoice_link.sql
Normal file
5
pirp/tools/migrate_journal_invoice_link.sql
Normal 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);
|
||||
15
pirp/tools/migrate_pdf.sql
Normal file
15
pirp/tools/migrate_pdf.sql
Normal 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;
|
||||
53
pirp/tools/migrate_pdfs.php
Normal file
53
pirp/tools/migrate_pdfs.php
Normal 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";
|
||||
42
pirp/tools/migrate_recurring.sql
Normal file
42
pirp/tools/migrate_recurring.sql
Normal 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);
|
||||
20
pirp/tools/migrate_storno_mahnung.sql
Normal file
20
pirp/tools/migrate_storno_mahnung.sql
Normal 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);
|
||||
19
pirp/tools/migrate_unique_constraints.sql
Normal file
19
pirp/tools/migrate_unique_constraints.sql
Normal 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;
|
||||
75
pirp/tools/run_migration.php
Normal file
75
pirp/tools/run_migration.php
Normal 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
4
pirp/tools/seed_dev.sql
Normal 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;
|
||||
Reference in New Issue
Block a user