from pathlib import Path
import json,shutil,hashlib,csv
from zipfile import ZipFile,ZIP_DEFLATED
from xml.sax.saxutils import escape
from reportlab.platypus import SimpleDocTemplate,Paragraph,Spacer,Table,TableStyle,PageBreak
from reportlab.lib.styles import getSampleStyleSheet,ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import pymupdf

ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'ESP_00VB_Livrables';WORK=ROOT/'_fabrication_00VB'
v=json.loads((WORK/'verification.json').read_text(encoding='utf-8'))
schema=json.loads((WORK/'schema_validation.json').read_text(encoding='utf-8'))
assert v['passed'] and all(not x.get('errors') and not x.get('exception') for x in schema)
pdfmetrics.registerFont(TTFont('CalibriLocal','C:/Windows/Fonts/calibri.ttf'))
pdfmetrics.registerFont(TTFont('CalibriBold','C:/Windows/Fonts/calibrib.ttf'))
styles=getSampleStyleSheet()
styles.add(ParagraphStyle(name='BodyFR',fontName='CalibriLocal',fontSize=10.5,leading=14,textColor=colors.HexColor('#16324F'),spaceAfter=9))
styles.add(ParagraphStyle(name='TitleFR',fontName='CalibriBold',fontSize=24,leading=29,textColor=colors.HexColor('#16324F'),spaceAfter=14))
styles.add(ParagraphStyle(name='HeadFR',fontName='CalibriBold',fontSize=14,leading=18,textColor=colors.HexColor('#007D8A'),spaceAfter=10))
styles.add(ParagraphStyle(name='CellFR',fontName='CalibriLocal',fontSize=9,leading=11,textColor=colors.HexColor('#16324F')))
def par(s,style='BodyFR'):return Paragraph(escape(str(s)).replace('\n','<br/>'),styles[style])
story=[par('Résultats de vérification','TitleFR'),par('ESP 00VB • Contrôles effectués le 14 septembre 2026'),par(f'{len(v["tests"])} contrôles de calcul réussis sur {len(v["tests"])}. Aucune cellule en erreur après recalcul du classeur. Les {len(schema)} fichiers Office passent la validation structurelle Open XML SDK 3.5.1.'),par('Portée des contrôles','HeadFR'),par('Le fichier .xlsx a été ouvert et recalculé dans LibreOffice Calc. Les données ont été modifiées temporairement pour vérifier la propagation des montants, la détection des anomalies et la justification d’un écart de durée. Elles ont été rétablies avant la fermeture, sans remplacer le classeur original par une conversion.'),par('La validation de la structure vérifie les formats et les éléments des fichiers; elle ne prouve pas le fonctionnement d’une fusion Word, d’une liaison Excel–Word ou de la coédition. Ces essais restent à exécuter dans Microsoft Office. Les aperçus PDF ont été générés avec LibreOffice.'),par('Journal des essais de calcul','HeadFR')]
rows=[[par(s,'CellFR') for s in ['Contrôle','Attendu','Observé','État']]]
for t in v['tests']:
    rows.append([par(t['test'],'CellFR'),par(t['expected'],'CellFR'),par(t['observed'],'CellFR'),par('Réussi' if t['passed'] else 'Échec','CellFR')])
t=Table(rows,colWidths=[235,87,87,65],repeatRows=1)
t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,0),colors.HexColor('#DBE8F0')),('VALIGN',(0,0),(-1,-1),'TOP'),('GRID',(0,0),(-1,-1),.3,colors.HexColor('#CED9E2')),('TOPPADDING',(0,0),(-1,-1),6),('BOTTOMPADDING',(0,0),(-1,-1),6),('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white,colors.HexColor('#F3F7FA')])]))
story.append(t);story.append(PageBreak());story.append(par('Formats des fichiers','HeadFR'))
for item in schema:story.append(par(item['file']+' — aucune erreur de schéma relevée.'))
story.extend([Spacer(1,12),par('Réception à compléter','HeadFR'),par('1. Exécuter le publipostage Word : 12 lettres, dont 6 avec le rabais.\n2. Actualiser les champs LINK et le contrôle calculé du bilan.\n3. Vérifier les modèles, les listes et les liens dans Office.\n4. Essayer la coédition à deux comptes dans l’espace autorisé.\n5. Ajouter les captures de menus Office et effectuer les exercices du guide.\n6. Personnaliser les renseignements personnels et répéter l’exposé de 20 minutes.')])
def footer(canvas,doc):
    canvas.setFont('CalibriLocal',8);canvas.setFillColor(colors.HexColor('#526477'));canvas.drawString(62,32,'Dossier de démonstration — contrôles techniques');canvas.drawRightString(550,32,str(doc.page))
SimpleDocTemplate(str(OUT/'12_Resultats_verification.pdf'),pagesize=letter,rightMargin=62,leftMargin=62,topMargin=56,bottomMargin=52).build(story,onFirstPage=footer,onLaterPages=footer)

for stem in ['01_Rapport_ESP_00VB','02_Guide_utilisation','08_Exemples_lettres_anniversaire','09_Presentation_orale_20min','09_Notes_presentation_orale','10_Parcours_professionnel_a_personnaliser','11_Grille_relecture']:
    src=WORK/'rendus'/(stem+'.pdf');assert src.exists();shutil.copy2(src,OUT/src.name)
readme='''DOSSIER ESP 00VB — À LIRE EN PREMIER

Fichiers préparés à partir de « Outil eva 00VB (ESP).pdf ».

COMMENCER ICI
1. Lire 01_Rapport_ESP_00VB.pdf (23 pages).
2. Ouvrir 03_Gestion_organisme.xlsx et suivre 02_Guide_utilisation.pdf.
3. Préparer l’oral avec 09_Presentation_orale_20min.pptx et ses notes.

CONTENU
- Rapport administratif avec analyse, planification, calendrier, tests et annexes.
- Guide d’utilisation des outils Word et Excel.
- Classeur de gestion : clients, rendez-vous, messages, rabais, facture, portrait et tests.
- Lettre anniversaire avec publipostage conditionnel.
- Deux modèles .dotx avec signatures des professionnelles.
- Bilan annuel Word avec liaisons au classeur, calculs et signets.
- Deux exemples de lettres attendues.
- Présentation de 15 diapositives; notes et minutage total prévu de 20 minutes.
- Trames de CV, lettre de candidature et préparation à l’entretien 00V6.
- Grille de relecture et résultats de vérification.

À PERSONNALISER
Le nom, le numéro DA, les personnes évaluatrices et la date de remise ne sont pas fournis.
Le CV, les expériences et les réponses à l’entretien doivent refléter votre parcours réel.
La partie 00V6 peut être omise si une équivalence ou substitution s’applique.
Les coordonnées de l’organisme et des professionnelles sont à compléter.

DONNÉES DE DÉMONSTRATION
Les 12 clients et les 28 rendez-vous sont fictifs. Les dates et tarifs sont hypothétiques.
Promotion : 1er septembre 2026 au 31 août 2027; historique : année précédente.
Les montants sont hors taxes; aucun traitement fiscal réel n’a été déterminé.
Le classeur contient 50 lignes de clients et 300 lignes de rendez-vous/messages préparées.

VÉRIFICATIONS
31 contrôles de calcul réussis après recalcul réel dans LibreOffice Calc.
Les 12 fichiers Office passent le contrôle de structure Open XML SDK.
Word et Excel ne sont pas installés sur le poste de préparation.
Avant remise, exécuter les essais Office décrits dans le rapport : fusion, liaisons,
champs calculés, modèles, validation de saisie et navigation. Tester la coédition à deux comptes.
Les figures de l’annexe sont des captures d’aperçus PDF; ajouter les captures des menus
Word et Excel selon le parcours indiqué. La répétition orale reste à effectuer.

LIENS ENTRE FICHIERS
Extraire toute l’archive ZIP avant d’ouvrir les outils. Conserver les fichiers dans le même dossier.
Si Word demande la source du publipostage, choisir le classeur 03 puis la feuille Fusion$,
et filtrer Envoyer = Oui. Si le bilan garde d’anciens montants, rétablir le chemin du classeur
dans Fichier > Informations > Modifier les liaisons vers les fichiers, puis actualiser.
Le guide fournit toutes les étapes. Les PDF sont des versions de consultation figées.

Aucun document n’a été envoyé aux spécialistes ou à des clients.
'''
(OUT/'00_LIRE_EN_PREMIER.txt').write_text(readme,encoding='utf-8-sig')
captures=OUT/'Captures_apercus';captures.mkdir(exist_ok=True)
for name in ['portrait.png','facture.png','tests.png','lettres.png']:shutil.copy2(WORK/'captures'/name,captures/name)
(captures/'Nature_des_captures.txt').write_text('Captures des aperçus PDF des outils créés, rendus avec LibreOffice.\nElles montrent les résultats de démonstration. Elles ne sont pas des captures de menus Microsoft Office.\nVoir l’annexe A du rapport pour les captures d’étapes à réaliser dans Word et Excel.\n',encoding='utf-8-sig')
# Content and archive integrity checks.
for file in OUT.glob('*.pdf'):
    doc=pymupdf.open(file)
    assert len(doc)>0
    assert all(len(pg.get_text().strip())>30 for pg in doc),file.name
    print(file.name,len(doc),'pages')
zip_path=ROOT/'ESP_00VB_Dossier.zip'
with ZipFile(zip_path,'w',ZIP_DEFLATED) as z:
    for file in sorted(OUT.rglob('*')):
        if file.is_file():z.write(file,file.relative_to(ROOT))
with ZipFile(zip_path) as z:assert z.testzip() is None;print('Archive :',len(z.namelist()),'fichiers,',zip_path.stat().st_size,'octets')
print('Dossier préparé et vérifié.')
