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
docxtplriempie 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
openpyxle ricaricate il modello per ogni documento. - Formattate numeri e date nello script e verificate i segnaposto prima di generare.