Files
erp/correspondance/lettre.py
T
arcodangeandClaude Opus 5 6abc437c81 feat(correspondance): le format de lettre commerciale devient un gabarit réutilisable
Fixé avec le dossier KissMetrics du 24/08 et validé par l'opérateur : « on peut
se souvenir de ce format pour les communications avec KM et futurs
prospects/clients ». Il ne servira à rien s'il faut le reconstituer à chaque
fois — d'où un gabarit, un script, et les raisons écrites.

LE TEST DU GABARIT : la lettre du 24/08 s'en régénère à l'identique, au mot
près, vérifié par diff du texte extrait. Un gabarit qui ne reproduit pas son
propre exemple n'est pas un gabarit.

LES CHOIX, ET POURQUOI. Charter pour le texte — dessinée par Matthew Carter
pour tenir le petit corps là où d'autres se délitent — et Optima pour les
titres. Les deux incorporées au PDF : le rendu est le même chez le
destinataire. Chiffres elzéviriens, pour que les montants s'alignent au lieu de
faire des bâtons. Deux encadrés, deux usages : le gris porte une question, le
rouge sourd ce qui doit être lu en diagonale — UN SEUL par lettre, deux et plus
rien ne ressort. Les tableaux portent un <thead>, sans quoi ils perdent leur
en-tête en se coupant entre deux pages.

LE PIÈGE, consigné parce qu'il coûte une heure à qui le redécouvre : weasyprint
NE SAIT PAS rendre Apple Color Emoji. C'est un format bitmap sbix qu'il ignore,
et les trois emblèmes 🏹💻🪽 sortent en carrés vides — sans erreur, sans
avertissement. D'où correspondance/emblemes/*.png, rendus une fois pour toutes
par test/emoji2png.ts, qui passe par Chromium : lui lit la police système.

Les PNG sont incorporés en base64 À LA GÉNÉRATION, pas dans le gabarit : un
gabarit de 400 Ko dont 97 % de charabia ne se relit pas. Le PDF produit reste
autonome.

Les trois emblèmes viennent du site — « Gabriel 🪽 Radureau, pour 🏹 réussir vos
projets 💻 ». L'arc et l'ange sont dans le nom lui-même.

lettre.py proteste au-delà de deux pages : une lettre d'affaires qui déborde ne
se lit pas, et la tentation est alors de rétrécir la typographie plutôt que le
propos. C'est le propos qu'il faut resserrer.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-24 18:17:55 +02:00

97 lines
4.4 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Produit une lettre commerciale Arcodange en PDF, depuis le gabarit maison.
POURQUOI CE SCRIPT EXISTE. Deux choses ne se font pas à la main sans se tromper.
1. `weasyprint` NE SAIT PAS rendre Apple Color Emoji : c'est un format bitmap
`sbix` qu'il ignore, et les trois emblèmes 🏹💻🪽 sortent en carrés vides.
Il faut les incorporer en images. Elles sont ici en PNG, rendues une fois
pour toutes par `test/emoji2png.ts` (Chromium, lui, lit la police système).
2. Un gabarit qui porterait ces images en base64 pèserait 400 Ko dont 97 % de
charabia. On les garde en fichiers et on les incorpore À LA GÉNÉRATION,
pour que le gabarit reste relisible et que le PDF reste autonome.
Usage :
python3 correspondance/lettre.py corps.html sortie.pdf \
--titre "Contracts and invoices — cycles M1 to M4" \
--date "24 August 2026" --prenom Evan \
--destinataire "Evan Sforzo" --fonction "Chief Executive Officer" \
--societe "Kissmetrics Inc." \
--adresse "2850 34th Street North, 307 — St. Petersburg, Florida 33713 — United States" \
--copie "Hendrik Rootering" \
--pied "Arcodange × Kissmetrics Inc. — 24 August 2026"
`corps.html` ne contient que le corps : les <h2>, <p>, <table>, encadrés. L'en-tête,
le bloc destinataire, la signature et le pied de page viennent du gabarit.
Sans `--copie`, la ligne « cc » disparaît.
"""
import argparse, base64, pathlib, re, subprocess, sys
ICI = pathlib.Path(__file__).parent
GABARIT = ICI / "lettre.template.html"
EMBLEMES = {"ARC": "arc.png", "LAPTOP": "laptop.png", "AILE": "aile.png"}
def data_uri(chemin: pathlib.Path) -> str:
return "data:image/png;base64," + base64.b64encode(chemin.read_bytes()).decode()
def main() -> int:
a = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
a.add_argument("corps"); a.add_argument("sortie")
for champ in ("titre", "date", "prenom", "destinataire", "fonction",
"societe", "adresse", "pied"):
a.add_argument(f"--{champ}", required=True)
a.add_argument("--copie", default="")
a.add_argument("--garder-html", action="store_true",
help="conserve le HTML intermédiaire à côté du PDF")
o = a.parse_args()
html = GABARIT.read_text(encoding="utf-8")
for cle, fichier in EMBLEMES.items():
p = ICI / "emblemes" / fichier
if not p.exists():
print(f"emblème manquant : {p}\n"
f" le régénérer : cd test && deno run -A emoji2png.ts <dossier> 🏹 💻 🪽",
file=sys.stderr)
return 2
html = html.replace("{{" + cle + "}}", data_uri(p))
for cle, val in (("TITRE", o.titre), ("DATE", o.date), ("PRENOM", o.prenom),
("DESTINATAIRE", o.destinataire), ("FONCTION", o.fonction),
("SOCIETE", o.societe), ("ADRESSE", o.adresse),
("COPIE", o.copie), ("PIED", o.pied)):
html = html.replace("{{" + cle + "}}", val)
# Pas de destinataire en copie : on retire la ligne entière, pas seulement
# son contenu, sinon il reste un « cc » orphelin.
if not o.copie:
html = re.sub(r'<br>\s*<span class="a"[^>]*>cc</span>\s*&nbsp;\s*', "", html)
# Le corps remplace tout ce qui sépare le sous-titre de la signature.
corps = pathlib.Path(o.corps).read_text(encoding="utf-8")
deb = html.index("<h1>")
fin = html.index('<div class="signature">')
entete = html[deb:html.index("<p>Dear ")]
html = html[:deb] + entete + f"<p>Dear {o.prenom},</p>\n\n" + corps + "\n\n" + html[fin:]
tmp = pathlib.Path(o.sortie).with_suffix(".html")
tmp.write_text(html, encoding="utf-8")
r = subprocess.run(["weasyprint", str(tmp), o.sortie])
if r.returncode == 0 and not o.garder_html:
tmp.unlink()
if r.returncode == 0:
pages = subprocess.run(["pdfinfo", o.sortie], capture_output=True, text=True).stdout
n = next((l.split()[1] for l in pages.splitlines() if l.startswith("Pages")), "?")
print(f"ok — {o.sortie} ({n} page(s))")
if n not in ("1", "2"):
print(" ATTENTION : au-delà de deux pages, une lettre d'affaires se lit mal. "
"Resserrer le corps plutôt que la typographie.", file=sys.stderr)
return r.returncode
if __name__ == "__main__":
raise SystemExit(main())