Metadata-Version: 2.5
Name: iil-doc-templates
Version: 0.11.2
Summary: Reusable Django document template system — create, edit, fill templates with PDF extraction and LLM prefill
Author-email: Achim Dehnert <achim.dehnert@iil.gmbh>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: django<6.0,>=5.0
Requires-Dist: iil-concept-templates>=0.4.0
Requires-Dist: python-docx>=1.1
Requires-Dist: segno>=1.6
Provides-Extra: all
Requires-Dist: iil-aifw>=0.5.0; extra == 'all'
Requires-Dist: iil-django-tenancy>=0.1.0; extra == 'all'
Requires-Dist: pdfplumber>=0.10; extra == 'all'
Requires-Dist: python-docx>=1.1.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-django; extra == 'dev'
Requires-Dist: python-docx>=1.1.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Provides-Extra: docx
Requires-Dist: python-docx>=1.1.0; extra == 'docx'
Provides-Extra: llm
Requires-Dist: iil-aifw>=0.5.0; extra == 'llm'
Provides-Extra: pdf
Requires-Dist: pdfplumber>=0.10; extra == 'pdf'
Provides-Extra: tenancy
Requires-Dist: iil-django-tenancy>=0.1.0; extra == 'tenancy'
Description-Content-Type: text/markdown

# iil-doc-templates

Reusable Django document template system — create, edit, fill templates with PDF extraction and LLM prefill.

## Installation

```bash
pip install iil-doc-templates
```

With optional dependencies:
```bash
pip install iil-doc-templates[pdf]   # pdfplumber for PDF extraction
pip install iil-doc-templates[llm]   # iil-aifw for LLM prefill
pip install iil-doc-templates[docx]  # python-docx for the .docx template import
pip install iil-doc-templates[all]   # everything
```

## Quick Start

```python
# settings.py
INSTALLED_APPS = [
    ...
    "doc_templates",
]

# urls.py
path("doc-templates/", include("doc_templates.urls")),
```

Then run migrations:
```bash
python manage.py migrate doc_templates
```

## Features

- **DocumentTemplate** — Reusable document templates with JSON structure (sections + fields)
- **DocumentInstance** — Filled-out documents based on templates
- **PDF Upload** — Extract structure from PDF using `iil-concept-templates`
- **Interactive Editor** — Drag & drop sections, CRUD fields, table columns
- **LLM Prefill** — AI-powered field completion via `iil-aifw`
- **PDF Export** — Generate PDF from filled documents via `concept_templates.document_renderer`
- **Multi-Tenant** — All queries filter by `tenant_id`

## Architecture

```
iil-concept-templates (Pure Python)      iil-doc-templates (Django)
├── Pydantic Schemas                     ├── Django Models
├── PDF Structure Extraction     ──►     ├── CRUD Views + URLs
├── Analysis, Prefill, Export            ├── HTML Templates (6)
└── No Django dependency                 ├── Admin Integration
                                         └── Uses concept_templates internally
```

## Field Types

| Type | Description |
|------|-------------|
| `textarea` | Multi-line text (default) |
| `text` | Single-line text |
| `number` | Numeric input |
| `date` | Date picker |
| `boolean` | Yes/No toggle |
| `table` | Table with configurable columns |

## URL Patterns

| URL | View | Description |
|-----|------|-------------|
| `/` | `template-list` | List all templates + instances |
| `/create/` | `template-create` | Create empty template |
| `/upload/` | `template-upload` | Create template from PDF |
| `/<pk>/edit/` | `template-edit` | Interactive template editor |
| `/<pk>/delete/` | `template-delete` | Delete template |
| `/<pk>/instance/create/` | `instance-create` | Create document from template |
| `/instance/<pk>/edit/` | `instance-edit` | Edit document fields |
| `/instance/<pk>/delete/` | `instance-delete` | Delete document |
| `/instance/<pk>/prefill/` | `instance-prefill` | LLM prefill endpoint |
| `/instance/<pk>/pdf/` | `instance-pdf` | PDF export |

## Vorlagen-Repository (ADR-042)

Das Modul `doc_templates.vorlagen` ist das **Vorlagen-Repository** von SchreibAssist
nach `meiki:ADR-042`. Die Vorlagen eines Amtes liegen in der **Postgres-Datenbank des
Produkts** — kein Dateibaum, kein DMS, kein Git-Pfad. Das Paket bringt das Geruest mit
und ausdruecklich **keine Hausinhalte**: der Weg hinein ist der Upload.

Additiv zum Bestand: `DocumentTemplate` und `DocumentInstance` bleiben unveraendert.

### Die fuenf Tabellen

| Modell | Haelt | Mandant |
|---|---|---|
| `Fachbereich` | Einheit eines Hauses als **Referenz** (`schluessel`), nicht als Amtstext | `tenant_id` |
| `Vorlage` | die Identitaet einer Vorlage — unique je `tenant_id` + Fachbereich + `schluessel` | `tenant_id` |
| `VorlagenVersion` | den Inhalt: `text` mit `{{platzhalter}}`, `felder`, `bloecke`, `qr`, Herkunft (`quelle_sha256`) | ueber `Vorlage` |
| `Feldtabelle` | Seriendruckfeld → Vorlagenfeld, je Haus (Guenzburg `PE_ANREDE1`, Traunstein `ANREDE`) | `tenant_id` |
| `VorlagenAudit` | wer wann was — ein Eintrag je Workflow-Uebergang | ueber `Vorlage` |

Alle mandantentragenden Modelle sind **fail-closed** (`platform:ADR-269`):

```python
from doc_templates.vorlagen import tenant_context
from doc_templates.vorlagen.models import Vorlage

Vorlage.objects.all()                    # [] — ohne Mandant gibt es nichts
Vorlage.objects.for_tenant(4711)         # der benannte Weg
with tenant_context(4711):
    Vorlage.objects.all()                # gefiltert auf 4711
Vorlage.objects.alle_mandanten(grund="Betriebsstatistik")   # Begruendung Pflicht
```

### Der Pin

```python
vorlage.freigegebene_version(date(2026, 3, 1))
```

Genau eine Fassung: die spaeteste mit `gueltig_ab <= stichtag`, deren Gueltigkeit noch
laeuft. **Ein Entwurf zaehlt nie** — auch nicht als einzige Fassung. Eine abgeloeste
Fassung zaehlt fuer Stichtage aus ihrer Zeit, damit ein Bescheid von damals erklaerbar
bleibt.

### Freigabe-Workflow

```
Entwurf --einreichen--> Review --freigeben--> Freigegeben --(neue Freigabe)--> Abgeloest
   ^                       |
   +----zurueckgeben-------+
```

```python
from doc_templates.vorlagen.freigabe import einreichen, freigeben, zurueckgeben

einreichen(version, akteur="sb1", begruendung="Upload geprueft")
freigeben(version, akteur="leitung", gueltig_ab=date(2026, 4, 1))
```

`freigeben` setzt die bisher freigegebene Fassung auf **Abgeloest** und traegt ihr
`gueltig_bis = gueltig_ab` ein — sie wird ersetzt, nicht ueberschrieben. Ein ungueltiger
Uebergang ist ein `ValueError`, kein stilles No-op; Begruendung ist Pflicht bei
`einreichen` und `zurueckgeben`.

### Word-Import

```python
from doc_templates.vorlagen.importer import importiere_docx

version = importiere_docx(datei, vorlage, feldtabelle, akteur="sb1")   # Status: Entwurf
version.unbekannte_felder     # Seriendruckfelder ohne Zuordnung — gemeldet, nie verworfen
version.ist_dublette          # True: dieselbe Datei lag schon vor, keine neue Version
```

Vier Eigenschaften, die `ADR-042` Festlegung 2 tragen ("der Import prueft und lehnt ab"):

1. **Beide Word-Bauformen** eines Seriendruckfeldes werden gelesen — `w:fldSimple` mit
   `w:instr` *und* die `w:instrText`-Folge zwischen `w:fldChar`-Markierungen. Wer nur
   eine liest, findet in der Haelfte der Dateien nichts und haelt sie fuer leer.
2. **Unbekanntes wird gemeldet.** Ein Feld ohne Eintrag in der Feldtabelle steht in
   `unbekannte_felder` und bleibt im Text als `«NAME»` sichtbar unaufgeloest. Die
   **Freigabe lehnt** eine solche Fassung ab; der Ausweg `unvollstaendig_zulassen=True`
   braucht eine Begruendung und steht im Audit.
3. **Die Vorbelegung wird nicht uebernommen.** Word haelt in einem Feld die Anweisung
   *und* das zuletzt angezeigte Ergebnis; das Ergebnis stammt aus dem letzten Seriendruck
   und kann personenbezogen sein.
4. **Derselbe Upload zweimal erzeugt keine zweite Version** — die Identitaet der Datei
   ist ihr SHA-256, nicht ihr Dateiname.

**Eingabefelder** (`FILLIN`, `ASK` mit `REF`) sind Felder wie Seriendruckfelder, in
beiden Bauformen. Der Name kommt aus der Aufforderung: ein einzelnes Wort oder der Text
in Anfuehrungszeichen, Leerzeichen zusammengefasst, Schalter wie `\d "…"`, `\o` und
`\* MERGEFORMAT` gehoeren nicht dazu. Ohne Aufforderung heisst das Feld
`Eingabe#<n> (<vorlage>)`, gezaehlt in Lesereihenfolge. Bei `ASK` traegt das `REF` auf
dessen Textmarke den Wert, das `ASK` selbst bleibt leer. Andere Felder (`PAGE`, `DATE`,
`REF` auf eine gewoehnliche Textmarke) sind keine Felder der Vorlage.

Tabellen werden zu `bloecke` (`{typ, pos, zeilen}`), Bildinhalte ignoriert.
Die Aufloesung ueber die Feldtabelle ist **case-insensitiv**: dieselbe Vorlage schreibt
denselben Namen mal `WG_ADAT`, mal `wg_adat`.

## Dependencies

- **Required**: Django ≥ 5.0, iil-concept-templates ≥ 0.4.0
- **Optional**: pdfplumber (PDF extraction), iil-aifw (LLM prefill), python-docx (.docx template import)
