⟳ Automazione

Generare documenti Word da un modello con Python e docxtpl

Come produrre lettere, preventivi e contratti in Word partendo da un modello con segnaposto, usando Python, docxtpl e i dati di un foglio di calcolo Excel.

Preventivi, lettere di incarico, attestati, verbali: molti documenti d'ufficio sono sempre uguali tranne pochi campi. Compilarli a mano significa aprire il modello, cercare i punti da cambiare, sostituire, salvare con un nome nuovo. Su cinquanta destinatari il rischio di dimenticare un campo o lasciare il nome del cliente precedente è alto. Con Python e la libreria docxtpl il modello resta un normale file Word, curato graficamente da chi sa usarlo, e lo script si limita a riempirlo.

Perché docxtpl e non solo python-docx

python-docx è la libreria di base per leggere e scrivere file .docx: permette di aggiungere paragrafi, tabelle e immagini. Costruire però un documento ben impaginato interamente da codice è faticoso, e ogni modifica grafica richiede di toccare il programma. docxtpl si appoggia a python-docx e aggiunge un sistema di segnaposto: si scrive il modello in Word inserendo dei marcatori, e lo script li sostituisce con i dati. La grafica resta in mano a chi usa Word, la logica in mano allo script.

pip install docxtpl openpyxl

Preparare il modello

Nel documento Word si scrivono i segnaposto tra doppie parentesi graffe:

Spett.le {{ ragione_sociale }}
{{ indirizzo }}

Oggetto: preventivo n. {{ numero }} del {{ data }}

Il segnaposto può avere qualsiasi formattazione: se {{ ragione_sociale }} è in grassetto, anche il testo inserito lo sarà. Un consiglio pratico: scrivete ogni segnaposto in un colpo solo, senza correzioni a metà. Word a volte spezza internamente una parola in più frammenti di formattazione, e un segnaposto spezzato non viene riconosciuto. Se succede, basta cancellarlo e riscriverlo da capo.

Il primo documento

from docxtpl import DocxTemplate

modello = DocxTemplate("modello_preventivo.docx")
dati = {
    "ragione_sociale": "Rossi S.r.l.",
    "indirizzo": "Via Roma 10, Milano",
    "numero": "2025-041",
    "data": "12/03/2025",
}
modello.render(dati)
modello.save("preventivo_2025-041.docx")

Il dizionario associa ogni nome di segnaposto al suo valore. I nomi devono coincidere esattamente con quelli del modello, comprese maiuscole e minuscole.

Tabelle con righe variabili

Un preventivo ha un numero di voci diverso ogni volta. docxtpl usa la sintassi del motore Jinja2 per i cicli. In una tabella Word con una riga di intestazione si aggiungono tre righe:

Descrizione Quantità Importo
{%tr for v in voci %}
{{ v.descrizione }} {{ v.quantita }} {{ v.importo }}
{%tr endfor %}

Il prefisso tr indica che il ciclo riguarda le righe della tabella: le righe con i marcatori di inizio e fine scompaiono, quella centrale viene ripetuta per ogni voce.

dati["voci"] = [
    {"descrizione": "Analisi iniziale", "quantita": 1, "importo": "400,00"},
    {"descrizione": "Configurazione", "quantita": 3, "importo": "750,00"},
]

Esistono anche condizioni, come {% if sconto %}...{% endif %}, utili per frasi che devono comparire solo in alcuni casi.

Generare decine di documenti da Excel

Il passo naturale è leggere i dati da un foglio di calcolo, dove ogni riga è un destinatario e ogni colonna un campo con lo stesso nome del segnaposto.

from openpyxl import load_workbook
from docxtpl import DocxTemplate
from pathlib import Path

foglio = load_workbook("destinatari.xlsx", data_only=True).active
righe = list(foglio.iter_rows(values_only=True))
intestazioni = righe[0]
Path("output").mkdir(exist_ok=True)

for riga in righe[1:]:
    dati = dict(zip(intestazioni, riga))
    modello = DocxTemplate("modello_lettera.docx")
    modello.render(dati)
    modello.save(f"output/lettera_{dati['numero']}.docx")

Due dettagli importanti. Il modello va ricaricato a ogni giro, perché render modifica il documento in memoria. E data_only=True fa leggere a openpyxl il risultato delle formule invece del testo della formula.

Formattare numeri e date

I valori letti da Excel arrivano come numeri e date di Python, non come testo formattato. Un importo 1250.5 comparirebbe così nel documento. Conviene preparare i dati prima del render:

def euro(valore):
    return f"{valore:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

dati["totale"] = euro(dati["totale"])           # 1.250,50
dati["data"] = dati["data"].strftime("%d/%m/%Y")  # 12/03/2025

Tenere la formattazione nello script, e non nel foglio, evita sorprese quando qualcuno cambia il formato delle celle.

Controlli prima di inviare

Nel mio lavoro aggiungo sempre due verifiche. La prima: confrontare l'elenco dei segnaposto del modello con le colonne del foglio. docxtpl offre il metodo get_undeclared_template_variables(), che restituisce i nomi usati nel modello: se qualcuno manca tra le colonne, lo script si ferma prima di produrre documenti con campi vuoti. La seconda: aprire a campione due o tre documenti generati, in particolare quello con i testi più lunghi, per controllare che l'impaginazione regga.

In sintesi

  • docxtpl riempie un modello Word con segnaposto {{ nome }}, lasciando la grafica in Word.
  • Scrivete i segnaposto senza interruzioni per evitare che Word li spezzi.
  • Usate {%tr for %} per tabelle con un numero variabile di righe.
  • Leggete i dati da Excel con openpyxl e ricaricate il modello per ogni documento.
  • Formattate numeri e date nello script e verificate i segnaposto prima di generare.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche