feat(L1-L3): Dokumente-Generator — Briefpapier+Block-System+Drag&Drop-Editor+Renderer
Check Cross-Plugin Imports / check (push) Has been cancelled

- Briefpapier (letterheads): Seiten-Setup (A4/A5/Letter, Ränder), Header/Footer-Blöcke, Wasserzeichen, Logo-Upload (DocumentAsset, data:-URI-only)
- Druckvorlagen (print_templates): Block-Komposition mit Briefpapier-Ref + entity_type
- Block-Registry (document_blocks.py): text/image/shape(line/rect/circle)/table/spacer/divider/placeholder/pagebreak + Modul-Beiträge via document_blocks()-Contract
- Renderer (document_renderer.py): Blocks→HTML→PDF via WeasyPrint (SSRF-Sandbox data:-URI-only), @page-Frame mit running header/footer, Placeholder-Beispiel-Defaults gegen StrictUndefined
- Contract-Beitrag contacts: document_placeholders/document_data (#359-Muster wie importexport_entities)
- 13 neue Endpoints in documents.py: Letterhead-CRUD, Template-CRUD, Assets, document-blocks, document-placeholders, preview (HTML), render (PDF)
- Migration: Plugin-SQL 0003 (idempotent) + Alembic 0143 (Dual-Path, RLS fail-closed crm_api)
- Frontend: api/documents.ts, Settings→Dokumente (settings_pages), BlockEditor (@dnd-kit Palette/Canvas/Config/Live-Preview-iframe), LetterheadEditor, PrintTemplateEditor, DocumentGenerationDialog (global, ContactDetailPage-Integration)
- i18n de/en, api-documentation.md, plugin-development-guide.md, PROGRESS.md

Verifikation: 32/32 neue Tests + 9/9 Regressionen, tsc exit 0, Build OK 2.79s, Alembic-Fresh-DB 0143 mit RLS bewiesen, ruff clean
This commit is contained in:
Agent Zero
2026-08-29 09:49:16 +02:00
parent fa429c3a88
commit b311ab7aa1
25 changed files with 4510 additions and 11 deletions
@@ -0,0 +1,74 @@
-- Documents Generator (Phase L1-L3): letterheads, print_templates, document_assets
-- Dual-path safe: idempotent (IF NOT EXISTS); Alembic 0143 converges core installs.
CREATE TABLE IF NOT EXISTS letterheads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL DEFAULT '',
config JSONB NOT NULL DEFAULT '{}'::jsonb,
is_default BOOLEAN NOT NULL DEFAULT false,
tenant_id UUID NOT NULL,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_letterheads_tenant ON letterheads(tenant_id);
CREATE INDEX IF NOT EXISTS ix_letterheads_name ON letterheads(name);
CREATE TABLE IF NOT EXISTS print_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL DEFAULT '',
letterhead_id UUID REFERENCES letterheads(id) ON DELETE SET NULL,
entity_type VARCHAR(100) NOT NULL DEFAULT 'contact',
blocks JSONB NOT NULL DEFAULT '[]'::jsonb,
output_format VARCHAR(20) NOT NULL DEFAULT 'pdf',
tenant_id UUID NOT NULL,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_print_templates_tenant ON print_templates(tenant_id);
CREATE INDEX IF NOT EXISTS ix_print_templates_name ON print_templates(name);
CREATE TABLE IF NOT EXISTS document_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
letterhead_id UUID REFERENCES letterheads(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
size_bytes INTEGER NOT NULL DEFAULT 0,
storage_path VARCHAR(1024) NOT NULL,
tenant_id UUID NOT NULL,
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_document_assets_tenant ON document_assets(tenant_id);
CREATE INDEX IF NOT EXISTS ix_document_assets_letterhead ON document_assets(letterhead_id);
-- RLS fail-closed (matches migration 0084 pattern: FORCE + crm_api + USING/WITH CHECK)
DO $do$
DECLARE
t text;
BEGIN
FOREACH t IN ARRAY ARRAY['letterheads', 'print_templates', 'document_assets'] LOOP
BEGIN
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', t);
EXECUTE format('DROP POLICY IF EXISTS %I ON %I', t || '_tenant_isolation', t);
EXECUTE format(
'CREATE POLICY %I ON %I AS PERMISSIVE FOR ALL TO crm_api USING (tenant_id = NULLIF(current_setting(''app.current_tenant_id'', true), '''')::uuid) WITH CHECK (tenant_id = NULLIF(current_setting(''app.current_tenant_id'', true), '''')::uuid)',
t || '_tenant_isolation', t
);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'RLS setup skipped for %', t;
END;
END LOOP;
END
$do$;