docs: scripts de génération des fiches produit et technique DSI/RSSI/DPO
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
522
scripts/generate_fiche_produit.py
Normal file
522
scripts/generate_fiche_produit.py
Normal file
@@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Génère la fiche produit Pseudonymisation en DOCX (deux versions)."""
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Cm, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||
from docx.oxml.ns import qn
|
||||
import os
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def set_cell_shading(cell, color_hex: str):
|
||||
"""Applique une couleur de fond à une cellule."""
|
||||
shading = cell._element.get_or_add_tcPr()
|
||||
shading_elem = shading.makeelement(qn('w:shd'), {
|
||||
qn('w:val'): 'clear',
|
||||
qn('w:color'): 'auto',
|
||||
qn('w:fill'): color_hex,
|
||||
})
|
||||
shading.append(shading_elem)
|
||||
|
||||
|
||||
def add_heading_styled(doc, text, level=1, color=RGBColor(0x1A, 0x56, 0x8E)):
|
||||
h = doc.add_heading(text, level=level)
|
||||
for run in h.runs:
|
||||
run.font.color.rgb = color
|
||||
return h
|
||||
|
||||
|
||||
def add_bullet(doc, text, bold_prefix=None):
|
||||
p = doc.add_paragraph(style='List Bullet')
|
||||
if bold_prefix:
|
||||
run = p.add_run(bold_prefix)
|
||||
run.bold = True
|
||||
p.add_run(text)
|
||||
else:
|
||||
p.add_run(text)
|
||||
return p
|
||||
|
||||
|
||||
def add_para(doc, text, bold=False, italic=False, space_after=Pt(6)):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
p.paragraph_format.space_after = space_after
|
||||
return p
|
||||
|
||||
|
||||
def add_table_row(table, cells_data, header=False, header_color='1A568E'):
|
||||
row = table.add_row()
|
||||
for i, text in enumerate(cells_data):
|
||||
cell = row.cells[i]
|
||||
cell.text = ''
|
||||
p = cell.paragraphs[0]
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(10)
|
||||
if header:
|
||||
run.bold = True
|
||||
run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
set_cell_shading(cell, header_color)
|
||||
p.paragraph_format.space_before = Pt(4)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
|
||||
|
||||
def set_table_style(table):
|
||||
"""Style sobre pour les tableaux."""
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
for p in cell.paragraphs:
|
||||
p.paragraph_format.space_before = Pt(3)
|
||||
p.paragraph_format.space_after = Pt(3)
|
||||
|
||||
|
||||
def add_section_break(doc):
|
||||
doc.add_page_break()
|
||||
|
||||
|
||||
# ── Couleurs ─────────────────────────────────────────────────────────
|
||||
|
||||
BLUE_DARK = RGBColor(0x1A, 0x56, 0x8E)
|
||||
BLUE_LIGHT = 'D6E8F7'
|
||||
GRAY_LIGHT = 'F2F2F2'
|
||||
GREEN = RGBColor(0x2E, 0x7D, 0x32)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# VERSION 1 — Synthétique (1 page recto, orientation portrait)
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_version_1(doc):
|
||||
# ── Titre ──
|
||||
title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
for run in title.runs:
|
||||
run.font.color.rgb = BLUE_DARK
|
||||
run.font.size = Pt(22)
|
||||
|
||||
# Sous-titre
|
||||
sub = doc.add_paragraph()
|
||||
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = sub.add_run('Fiche produit — Version synthétique')
|
||||
run.font.size = Pt(12)
|
||||
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
|
||||
run.italic = True
|
||||
sub.paragraph_format.space_after = Pt(16)
|
||||
|
||||
# ── En bref ──
|
||||
add_heading_styled(doc, 'En bref', level=2)
|
||||
add_para(doc,
|
||||
'Logiciel d\'anonymisation automatique de documents médicaux, '
|
||||
'fonctionnant 100% en local sur un poste Windows. '
|
||||
'Aucune donnée ne sort de l\'établissement. '
|
||||
'Un fichier entre, un fichier anonymisé sort.',
|
||||
space_after=Pt(10))
|
||||
|
||||
# ── Tableau comparatif ──
|
||||
add_heading_styled(doc, 'Pourquoi cet outil ?', level=2)
|
||||
table = doc.add_table(rows=0, cols=2)
|
||||
table.style = 'Table Grid'
|
||||
add_table_row(table, ['Situation actuelle', 'Avec l\'outil'], header=True)
|
||||
comparisons = [
|
||||
('Anonymisation manuelle, chronophage\net source d\'erreurs',
|
||||
'Traitement automatique\nen quelques minutes'),
|
||||
('Risque d\'oubli de données\npersonnelles (RGPD)',
|
||||
'Taux de détection > 99%\nvalidé sur corpus réel'),
|
||||
('Mobilisation de personnel qualifié\nsur une tâche répétitive',
|
||||
'L\'équipe TIM vérifie,\nelle ne produit plus à la main'),
|
||||
]
|
||||
for left, right in comparisons:
|
||||
add_table_row(table, [left, right])
|
||||
set_table_style(table)
|
||||
doc.add_paragraph() # espace
|
||||
|
||||
# ── Points clés ──
|
||||
add_heading_styled(doc, 'Points clés', level=2)
|
||||
points = [
|
||||
('100% hors-ligne — ', 'aucune donnée ne transite par internet'),
|
||||
('Un seul exécutable — ', 'pas d\'installation, pas de serveur, pas de GPU'),
|
||||
('14 formats acceptés — ', 'PDF, Word, ODT, RTF, images, HTML...'),
|
||||
('4 moteurs d\'IA — ', 'spécialisés en français clinique, fonctionnent en parallèle'),
|
||||
('Paramétrable — ', 'whitelist et blacklist modifiables par l\'établissement'),
|
||||
('Irréversible — ', 'masquage par rectangles noirs, pas de calque amovible'),
|
||||
]
|
||||
for bold_part, normal_part in points:
|
||||
add_bullet(doc, normal_part, bold_prefix=bold_part)
|
||||
|
||||
# ── Ce qui est masqué / préservé ──
|
||||
add_heading_styled(doc, 'Ce qui est masqué / préservé', level=2)
|
||||
table2 = doc.add_table(rows=0, cols=2)
|
||||
table2.style = 'Table Grid'
|
||||
add_table_row(table2, ['Masqué automatiquement', 'Préservé intégralement'], header=True)
|
||||
add_table_row(table2, [
|
||||
'Noms, prénoms, adresses,\ntéléphones, n° de sécu,\ndates de naissance,\nnoms d\'établissements,\ncodes-barres patients',
|
||||
'Diagnostics, traitements,\nposologies, actes médicaux,\nrésultats d\'examens,\ndates de séjour,\ncodage CIM / CCAM'
|
||||
])
|
||||
set_table_style(table2)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── Cas d'usage ──
|
||||
add_heading_styled(doc, 'Cas d\'usage', level=2)
|
||||
usages = [
|
||||
('Contrôle T2A — ', 'anonymiser les pièces justificatives avant transmission'),
|
||||
('Recherche clinique — ', 'constituer des corpus anonymisés'),
|
||||
('Audits qualité / HAS — ', 'partager des dossiers sans exposition de données'),
|
||||
('Formation — ', 'utiliser des cas réels anonymisés pour l\'enseignement'),
|
||||
]
|
||||
for bold_part, normal_part in usages:
|
||||
add_bullet(doc, normal_part, bold_prefix=bold_part)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# VERSION 2 — Détaillée (multi-pages, par audience)
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def build_version_2(doc):
|
||||
# ── Page de titre ──
|
||||
for _ in range(4):
|
||||
doc.add_paragraph()
|
||||
|
||||
title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
for run in title.runs:
|
||||
run.font.color.rgb = BLUE_DARK
|
||||
run.font.size = Pt(26)
|
||||
|
||||
doc.add_paragraph()
|
||||
sub = doc.add_paragraph()
|
||||
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = sub.add_run('Fiche produit détaillée')
|
||||
run.font.size = Pt(14)
|
||||
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
|
||||
|
||||
doc.add_paragraph()
|
||||
audiences = doc.add_paragraph()
|
||||
audiences.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = audiences.add_run('Direction générale · DSI · Médecins · Équipe TIM')
|
||||
run.font.size = Pt(11)
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
run.italic = True
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 1. Le problème ──
|
||||
add_heading_styled(doc, 'Le problème', level=1)
|
||||
add_para(doc,
|
||||
'Les établissements de santé manipulent quotidiennement des documents '
|
||||
'contenant des données personnelles de patients : comptes-rendus opératoires, '
|
||||
'résultats d\'examens, courriers médicaux, rapports d\'anatomo-pathologie...')
|
||||
add_para(doc,
|
||||
'Lors des contrôles T2A, audits qualité, recherches cliniques ou échanges '
|
||||
'inter-établissements, ces documents doivent être anonymisés conformément '
|
||||
'au RGPD et au cadre réglementaire santé.')
|
||||
add_para(doc,
|
||||
'L\'anonymisation manuelle est lente, coûteuse, et faillible : '
|
||||
'un oubli suffit à exposer l\'identité d\'un patient.',
|
||||
bold=True, space_after=Pt(12))
|
||||
|
||||
# ── 2. La solution ──
|
||||
add_heading_styled(doc, 'La solution', level=1)
|
||||
add_para(doc,
|
||||
'Un logiciel autonome, fonctionnant intégralement en local, qui anonymise '
|
||||
'automatiquement vos documents en quelques minutes :')
|
||||
bullets = [
|
||||
'Détecte et masque les noms, prénoms, adresses, téléphones, numéros de sécurité sociale, dates de naissance, noms d\'établissements',
|
||||
'Préserve le contenu médical utile : diagnostics, traitements, actes, résultats, codage',
|
||||
'Produit un PDF anonymisé prêt à transmettre, avec masquage irréversible',
|
||||
]
|
||||
for b in bullets:
|
||||
add_bullet(doc, b)
|
||||
doc.add_paragraph()
|
||||
|
||||
# ── Tableau comparatif ──
|
||||
add_heading_styled(doc, 'Avant / Après', level=2)
|
||||
table = doc.add_table(rows=0, cols=2)
|
||||
table.style = 'Table Grid'
|
||||
add_table_row(table, ['Situation actuelle', 'Avec l\'outil'], header=True)
|
||||
comparisons = [
|
||||
('Anonymisation manuelle, chronophage et source d\'erreurs',
|
||||
'Traitement automatique en quelques minutes'),
|
||||
('Risque d\'oubli de données personnelles (RGPD, AI Act)',
|
||||
'Taux de détection > 99% validé sur corpus réel de contrôle T2A'),
|
||||
('Mobilisation de personnel qualifié sur une tâche répétitive',
|
||||
'L\'équipe TIM se concentre sur la vérification, pas la production'),
|
||||
('Dépendance à des outils cloud ou des prestataires externes',
|
||||
'100% local, aucune donnée ne quitte l\'établissement'),
|
||||
]
|
||||
for left, right in comparisons:
|
||||
add_table_row(table, [left, right])
|
||||
set_table_style(table)
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 3. Conformité et sécurité (DG + DSI) ──
|
||||
add_heading_styled(doc, 'Conformité et sécurité', level=1)
|
||||
tag = doc.add_paragraph()
|
||||
run = tag.add_run('→ Direction générale, DSI')
|
||||
run.italic = True
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
tag.paragraph_format.space_after = Pt(8)
|
||||
|
||||
security_points = [
|
||||
('Aucune connexion réseau', 'Le logiciel fonctionne entièrement hors-ligne. Aucun appel à un serveur externe, aucune API cloud, aucune télémétrie.'),
|
||||
('Aucune donnée exfiltrée', 'Le traitement se fait intégralement en mémoire locale. Les documents source ne sont ni copiés ni transmis.'),
|
||||
('Conformité RGPD / AI Act', 'L\'intelligence artificielle est embarquée dans l\'exécutable. Pas de sous-traitance, pas de transfert de données à un tiers.'),
|
||||
('Masquage irréversible', 'Les zones anonymisées sont remplacées par des rectangles noirs directement dans le PDF. Il ne s\'agit pas d\'un calque amovible : l\'information est définitivement supprimée.'),
|
||||
('Traçabilité', 'Les fichiers anonymisés sont nommés avec le préfixe "ANON_" pour identification immédiate.'),
|
||||
]
|
||||
for title_text, desc in security_points:
|
||||
p = doc.add_paragraph()
|
||||
run_title = p.add_run(title_text + ' — ')
|
||||
run_title.bold = True
|
||||
p.add_run(desc)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 4. Déploiement (DSI) ──
|
||||
add_heading_styled(doc, 'Déploiement et maintenance', level=1)
|
||||
tag = doc.add_paragraph()
|
||||
run = tag.add_run('→ DSI')
|
||||
run.italic = True
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
tag.paragraph_format.space_after = Pt(8)
|
||||
|
||||
add_heading_styled(doc, 'Prérequis', level=2)
|
||||
prereqs = [
|
||||
'Windows 10 ou 11 (64 bits)',
|
||||
'Poste bureautique standard — pas de GPU dédié, pas de serveur requis',
|
||||
'Pas de droits administrateur nécessaires pour l\'exécution',
|
||||
'Pas de connexion internet requise',
|
||||
]
|
||||
for pr in prereqs:
|
||||
add_bullet(doc, pr)
|
||||
|
||||
add_heading_styled(doc, 'Installation', level=2)
|
||||
install_steps = [
|
||||
'Copier l\'exécutable (un seul fichier .exe) sur le poste',
|
||||
'Au premier lancement, le fichier de configuration est créé automatiquement à côté de l\'exécutable',
|
||||
'C\'est prêt — aucune autre manipulation nécessaire',
|
||||
]
|
||||
for i, step in enumerate(install_steps, 1):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(f'{i}. ')
|
||||
run.bold = True
|
||||
p.add_run(step)
|
||||
|
||||
add_heading_styled(doc, 'Mise à jour', level=2)
|
||||
add_para(doc, 'Remplacer l\'exécutable par la nouvelle version. La configuration personnalisée de l\'établissement est conservée (fichier séparé).')
|
||||
|
||||
add_heading_styled(doc, 'Configuration inter-établissements', level=2)
|
||||
add_para(doc,
|
||||
'Les paramètres (whitelist, blacklist) sont exportables au format JSON depuis l\'interface. '
|
||||
'Un établissement peut envoyer sa configuration par email. '
|
||||
'Le service central fusionne les configurations et renvoie un fichier YAML consolidé.')
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 5. Utilisation quotidienne (TIM) ──
|
||||
add_heading_styled(doc, 'Utilisation quotidienne', level=1)
|
||||
tag = doc.add_paragraph()
|
||||
run = tag.add_run('→ Équipe TIM, médecins')
|
||||
run.italic = True
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
tag.paragraph_format.space_after = Pt(8)
|
||||
|
||||
add_heading_styled(doc, 'En 3 étapes', level=2)
|
||||
steps = [
|
||||
('Sélectionner', 'Choisir un fichier ou un dossier entier depuis l\'interface'),
|
||||
('Lancer', 'Cliquer sur "Lancer l\'anonymisation"'),
|
||||
('Récupérer', 'Les documents anonymisés sont créés dans le dossier de sortie, prêts à transmettre'),
|
||||
]
|
||||
for title_text, desc in steps:
|
||||
p = doc.add_paragraph()
|
||||
run_title = p.add_run(f'{title_text} — ')
|
||||
run_title.bold = True
|
||||
run_title.font.color.rgb = BLUE_DARK
|
||||
p.add_run(desc)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
|
||||
add_heading_styled(doc, 'Formats acceptés (14)', level=2)
|
||||
table_fmt = doc.add_table(rows=0, cols=2)
|
||||
table_fmt.style = 'Table Grid'
|
||||
add_table_row(table_fmt, ['Type', 'Formats'], header=True)
|
||||
formats = [
|
||||
('Documents', 'PDF, Word (.docx), ODT, RTF, TXT, HTML'),
|
||||
('Images', 'JPEG, PNG, TIFF, BMP'),
|
||||
]
|
||||
for type_name, fmt_list in formats:
|
||||
add_table_row(table_fmt, [type_name, fmt_list])
|
||||
set_table_style(table_fmt)
|
||||
doc.add_paragraph()
|
||||
|
||||
add_para(doc,
|
||||
'Les images et documents scannés sont traités par reconnaissance optique de caractères (OCR) '
|
||||
'intégrée — pas de logiciel tiers nécessaire.',
|
||||
italic=True)
|
||||
|
||||
add_heading_styled(doc, 'Paramétrage', level=2)
|
||||
params = [
|
||||
('Whitelist — ', 'liste de termes à ne jamais masquer (noms de services, sigles internes, noms de logiciels...). Modifiable directement dans l\'interface.'),
|
||||
('Blacklist — ', 'liste de termes à toujours masquer (noms de praticiens spécifiques...). Modifiable directement dans l\'interface.'),
|
||||
('Export/Import — ', 'les paramètres sont échangeables entre établissements par simple fichier JSON envoyé par email.'),
|
||||
]
|
||||
for bold_part, normal_part in params:
|
||||
add_bullet(doc, normal_part, bold_prefix=bold_part)
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 6. Fiabilité (Médecins + TIM) ──
|
||||
add_heading_styled(doc, 'Fiabilité de la détection', level=1)
|
||||
tag = doc.add_paragraph()
|
||||
run = tag.add_run('→ Médecins, équipe TIM')
|
||||
run.italic = True
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
tag.paragraph_format.space_after = Pt(8)
|
||||
|
||||
add_para(doc,
|
||||
'Le logiciel combine 4 moteurs d\'intelligence artificielle spécialisés en français clinique '
|
||||
'qui fonctionnent simultanément et se recoupent pour maximiser la détection :')
|
||||
|
||||
engines = [
|
||||
('Modèle CamemBERT médical', 'entraîné spécifiquement sur des documents cliniques français'),
|
||||
('Modèle de détection d\'entités', 'reconnaissance zero-shot de données personnelles'),
|
||||
('Modèle spécialisé', 'fine-tuné sur plus de 1 000 documents médicaux réels'),
|
||||
('Bases de référence nationales', '219 000 noms de famille, 36 000 prénoms, 7 300 médicaments, 108 000 établissements de santé'),
|
||||
]
|
||||
for title_text, desc in engines:
|
||||
add_bullet(doc, ' — ' + desc, bold_prefix=title_text)
|
||||
doc.add_paragraph()
|
||||
|
||||
add_para(doc,
|
||||
'Principe de précaution : en cas de doute, le logiciel masque. '
|
||||
'Il est préférable de masquer un terme médical par excès '
|
||||
'plutôt que de laisser visible un nom de patient.',
|
||||
bold=True, space_after=Pt(8))
|
||||
|
||||
add_para(doc,
|
||||
'Taux de détection validé à plus de 99% sur un corpus réel de documents '
|
||||
'de contrôle T2A : comptes-rendus opératoires, anatomo-pathologie, bactériologie, '
|
||||
'anesthésie, courriers médicaux, comptes-rendus d\'hospitalisation.')
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ── 7. Cas d'usage ──
|
||||
add_heading_styled(doc, 'Cas d\'usage', level=1)
|
||||
|
||||
use_cases = [
|
||||
('Contrôle T2A',
|
||||
'Anonymiser les pièces justificatives (comptes-rendus, résultats, courriers) '
|
||||
'avant transmission à l\'organisme de contrôle.'),
|
||||
('Recherche clinique',
|
||||
'Constituer des jeux de données anonymisés à partir de dossiers patients réels, '
|
||||
'sans risque d\'identification.'),
|
||||
('Audits qualité et certification HAS',
|
||||
'Partager des cas cliniques lors des visites de certification '
|
||||
'sans exposer l\'identité des patients.'),
|
||||
('Échanges inter-établissements',
|
||||
'Transmettre des comptes-rendus médicaux anonymisés dans le cadre '
|
||||
'de parcours de soins partagés.'),
|
||||
('Formation et enseignement',
|
||||
'Utiliser des cas cliniques réels anonymisés pour la formation '
|
||||
'des internes et des équipes soignantes.'),
|
||||
]
|
||||
for title_text, desc in use_cases:
|
||||
p = doc.add_paragraph()
|
||||
run_title = p.add_run(title_text + '\n')
|
||||
run_title.bold = True
|
||||
run_title.font.size = Pt(11)
|
||||
run_desc = p.add_run(desc)
|
||||
run_desc.font.size = Pt(10)
|
||||
p.paragraph_format.space_after = Pt(10)
|
||||
|
||||
# ── 8. Questions fréquentes ──
|
||||
add_heading_styled(doc, 'Questions fréquentes', level=1)
|
||||
|
||||
faq = [
|
||||
('Combien de temps prend le traitement d\'un document ?',
|
||||
'En moyenne quelques dizaines de secondes par document. '
|
||||
'Un lot de 20 à 30 documents se traite en 10 à 15 minutes sur un poste standard.'),
|
||||
('Que faire si un nom n\'a pas été détecté ?',
|
||||
'Ajouter le terme dans la blacklist via l\'interface. '
|
||||
'Il sera systématiquement masqué lors des prochains traitements.'),
|
||||
('Que faire si un terme médical a été masqué par erreur ?',
|
||||
'Ajouter le terme dans la whitelist via l\'interface. '
|
||||
'Il ne sera plus jamais masqué.'),
|
||||
('Faut-il une connexion internet ?',
|
||||
'Non. Le logiciel fonctionne intégralement hors-ligne. '
|
||||
'L\'intelligence artificielle est embarquée dans l\'exécutable.'),
|
||||
('Les documents originaux sont-ils modifiés ?',
|
||||
'Non. Le logiciel crée une copie anonymisée. '
|
||||
'Les documents originaux restent intacts.'),
|
||||
('Peut-on traiter un dossier entier d\'un coup ?',
|
||||
'Oui. Il suffit de sélectionner un dossier au lieu d\'un fichier. '
|
||||
'Tous les documents du dossier seront traités séquentiellement.'),
|
||||
]
|
||||
for question, answer in faq:
|
||||
p = doc.add_paragraph()
|
||||
run_q = p.add_run(question + '\n')
|
||||
run_q.bold = True
|
||||
run_q.font.size = Pt(10)
|
||||
run_a = p.add_run(answer)
|
||||
run_a.font.size = Pt(10)
|
||||
p.paragraph_format.space_after = Pt(8)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# Construction du document final
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main():
|
||||
doc = Document()
|
||||
|
||||
# ── Style par défaut ──
|
||||
style = doc.styles['Normal']
|
||||
style.font.name = 'Calibri'
|
||||
style.font.size = Pt(10)
|
||||
style.paragraph_format.space_after = Pt(4)
|
||||
|
||||
# Marges
|
||||
for section in doc.sections:
|
||||
section.top_margin = Cm(1.5)
|
||||
section.bottom_margin = Cm(1.5)
|
||||
section.left_margin = Cm(2)
|
||||
section.right_margin = Cm(2)
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# VERSION 1 — Synthétique
|
||||
# ════════════════════════════════════════════
|
||||
build_version_1(doc)
|
||||
|
||||
# ── Séparation ──
|
||||
add_section_break(doc)
|
||||
sep = doc.add_paragraph()
|
||||
sep.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
for _ in range(6):
|
||||
doc.add_paragraph()
|
||||
sep2 = doc.add_paragraph()
|
||||
sep2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = sep2.add_run('— Version détaillée ci-après —')
|
||||
run.font.size = Pt(14)
|
||||
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
|
||||
run.italic = True
|
||||
|
||||
add_section_break(doc)
|
||||
|
||||
# ════════════════════════════════════════════
|
||||
# VERSION 2 — Détaillée
|
||||
# ════════════════════════════════════════════
|
||||
build_version_2(doc)
|
||||
|
||||
# ── Sauvegarde ──
|
||||
output_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
||||
'Fiche_Produit_Pseudonymisation.docx')
|
||||
doc.save(output_path)
|
||||
print(f'Fiche produit générée : {output_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user