documents — overview
Generates prescriptions, medical certificates, referral letters and radiology requests as branded PDFs with configurable clinic letterhead (name, logo, address, registration number).
What it is
Standard clinic-scoped CRUD over a flat GeneratedDocument list: create, list (filterable by patient, document type and status, paginated), get, update (partial via exclude_unset), delete (soft-delete / archive). A POST /documents/generate endpoint renders the document as a branded PDF and publishes DOCUMENT_GENERATED on the event bus (consumed by activity_journal), and GET /documents/{id}/download streams the rendered file.
Clinic letterhead overrides are configurable per clinic via GET/PUT /documents/settings/letterhead (stored namespaced under clinic.settings["documents"]["letterhead"]).
Cross-module reads: patients (for the patient demographics block in the PDF). No cross-module writes. Prescription medication lines are free text entered in the form — there is no catalog coupling.
Document types
| Type | Key content fields |
|---|---|
prescription | diagnosis, medications (name/dose/frequency/duration/notes), notes |
medical_certificate | diagnosis, description, recommendations, valid_from, valid_until |
referral | referred_to, specialty, reason, clinical_summary, notes |
radiology_request | exam_type, region, clinical_question, notes |
Content is stored as JSONB — each document type has a Pydantic schema that validates the structure, but the column itself is schemaless for forward compatibility.
Integrity guarantees
- Documents are scoped per clinic (every query filters by
clinic_id). - Soft-delete via
statuscolumn (draft→generated→archived). - No hard deletes — document history is preserved.
PDF generation
The generate endpoint:
- Fetches the document + clinic + patient.
- Resolves the letterhead from
clinic.settings["documents"]["letterhead"], falling back per-key to the clinic's native profile (name, address, phone, email, tax_id). - Renders an HTML template (
pdf.py) with the document content, patient demographics and clinic branding —body-only if WeasyPrint is unavailable it falls back to raw HTML. - Renders via WeasyPrint off the event loop (
asyncio.to_thread, same pattern as billing). - Persists the file at
storage/documents/{clinic_id}/{document_id}.pdfand stores the relativefile_pathon the row — only after the render succeeds does the status flip togenerated, so no event fires for a document without a real file behind it. - Publishes
DOCUMENT_GENERATED.
GET /documents/{id}/download streams the persisted bytes back as an application/pdf attachment (404 if the document was never generated or the file is missing; documents the download doesn't re-render).
Data model
generated_documents—id,clinic_id,patient_id,document_type,title,status,content(JSONB),file_path(nullable),created_by(nullable FK tousers.id), timestamps.
Lifecycle
installable=True, auto_install=False (ships inactive, the admin activates it from the module admin UI), removable=True. Own Alembic branch (documents), rooted independently on core "0001". The initial migration FKs to patients.id; because patients has no branch label of its own, doc_0001 declares depends_on = ("pat_0003",) (same pattern as patient_relationships.prel_0001) so a fresh install orders the patients chain first.