""" Génère le rapport de PFE ERP Rayhan en format DOCX professionnel. Usage : python3 generate_report.py """ from docx import Document from docx.shared import Pt, Cm, RGBColor, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL from docx.oxml.ns import qn from docx.oxml import OxmlElement import copy # ── Palette de couleurs ────────────────────────────────────────────────────── BLEU_FONCE = RGBColor(0x1F, 0x38, 0x64) # Titres principaux BLEU_MOYEN = RGBColor(0x2E, 0x74, 0xB5) # Titres secondaires BLEU_CLAIR = RGBColor(0xBD, 0xD7, 0xEE) # Fond en-têtes tableau GRIS_TEXTE = RGBColor(0x26, 0x26, 0x26) # Corps de texte BLANC = RGBColor(0xFF, 0xFF, 0xFF) # ── Helpers ────────────────────────────────────────────────────────────────── def set_cell_bg(cell, color_hex): tc = cell._tc tcPr = tc.get_or_add_tcPr() shd = OxmlElement('w:shd') shd.set(qn('w:val'), 'clear') shd.set(qn('w:color'), 'auto') shd.set(qn('w:fill'), color_hex) tcPr.append(shd) def add_page_number(paragraph): run = paragraph.add_run() fldChar1 = OxmlElement('w:fldChar') fldChar1.set(qn('w:fldCharType'), 'begin') instrText = OxmlElement('w:instrText') instrText.text = 'PAGE' fldChar2 = OxmlElement('w:fldChar') fldChar2.set(qn('w:fldCharType'), 'end') run._r.append(fldChar1) run._r.append(instrText) run._r.append(fldChar2) def set_col_width(table, col_idx, width_cm): for row in table.rows: row.cells[col_idx].width = Cm(width_cm) def styled_heading(doc, text, level, color=None): p = doc.add_paragraph(style=f'Heading {level}') run = p.add_run(text) run.font.name = 'Calibri' run.font.bold = True if level == 1: run.font.size = Pt(16) run.font.color.rgb = color or BLEU_FONCE p.paragraph_format.space_before = Pt(24) p.paragraph_format.space_after = Pt(8) elif level == 2: run.font.size = Pt(13) run.font.color.rgb = color or BLEU_MOYEN p.paragraph_format.space_before = Pt(16) p.paragraph_format.space_after = Pt(6) else: run.font.size = Pt(11) run.font.color.rgb = color or BLEU_MOYEN run.font.italic = True p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(4) return p def body(doc, text, space_after=6): p = doc.add_paragraph() run = p.add_run(text) run.font.name = 'Calibri' run.font.size = Pt(11) run.font.color.rgb = GRIS_TEXTE p.paragraph_format.space_after = Pt(space_after) p.paragraph_format.line_spacing = Pt(16) return p def bullet(doc, text): p = doc.add_paragraph(style='List Bullet') run = p.add_run(text) run.font.name = 'Calibri' run.font.size = Pt(11) run.font.color.rgb = GRIS_TEXTE p.paragraph_format.space_after = Pt(3) return p def add_table(doc, headers, rows, col_widths=None): table = doc.add_table(rows=1 + len(rows), cols=len(headers)) table.style = 'Table Grid' table.alignment = WD_TABLE_ALIGNMENT.CENTER # En-tête hdr = table.rows[0] for i, h in enumerate(headers): cell = hdr.cells[i] set_cell_bg(cell, '2E74B5') p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(h) run.font.name = 'Calibri' run.font.bold = True run.font.size = Pt(10) run.font.color.rgb = BLANC cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER # Données for r_idx, row_data in enumerate(rows): row = table.rows[r_idx + 1] bg = 'F2F7FC' if r_idx % 2 == 0 else 'FFFFFF' for c_idx, val in enumerate(row_data): cell = row.cells[c_idx] set_cell_bg(cell, bg) p = cell.paragraphs[0] run = p.add_run(val) run.font.name = 'Calibri' run.font.size = Pt(10) run.font.color.rgb = GRIS_TEXTE cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER if col_widths: for i, w in enumerate(col_widths): set_col_width(table, i, w) doc.add_paragraph() return table def add_code_block(doc, code_text): p = doc.add_paragraph() run = p.add_run(code_text) run.font.name = 'Courier New' run.font.size = Pt(9) run.font.color.rgb = RGBColor(0x1E, 0x1E, 0x1E) p.paragraph_format.left_indent = Cm(1) shading = OxmlElement('w:shd') shading.set(qn('w:val'), 'clear') shading.set(qn('w:color'), 'auto') shading.set(qn('w:fill'), 'F4F4F4') p._p.get_or_add_pPr().append(shading) p.paragraph_format.space_after = Pt(8) def separator(doc): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(4) p.paragraph_format.space_after = Pt(4) pPr = p._p.get_or_add_pPr() pBdr = OxmlElement('w:pBdr') bottom = OxmlElement('w:bottom') bottom.set(qn('w:val'), 'single') bottom.set(qn('w:sz'), '6') bottom.set(qn('w:space'), '1') bottom.set(qn('w:color'), '2E74B5') pBdr.append(bottom) pPr.append(pBdr) # ── Document ───────────────────────────────────────────────────────────────── doc = Document() # Marges for section in doc.sections: section.top_margin = Cm(2.5) section.bottom_margin = Cm(2.5) section.left_margin = Cm(3.0) section.right_margin = Cm(2.5) # Pied de page avec numéros de page for section in doc.sections: footer = section.footer fp = footer.paragraphs[0] fp.alignment = WD_ALIGN_PARAGRAPH.CENTER fp.add_run('ERP SUARL Rayhan — PFE Ali Guennari | Page ').font.name = 'Calibri' fp.runs[0].font.size = Pt(9) fp.runs[0].font.color.rgb = RGBColor(0x80, 0x80, 0x80) add_page_number(fp) # ════════════════════════════════════════════════════════════════════════════ # PAGE DE GARDE # ════════════════════════════════════════════════════════════════════════════ # Espacement haut for _ in range(4): doc.add_paragraph() # Titre principal p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run('ERP SUR MESURE') run.font.name = 'Calibri' run.font.size = Pt(28) run.font.bold = True run.font.color.rgb = BLEU_FONCE p2 = doc.add_paragraph() p2.alignment = WD_ALIGN_PARAGRAPH.CENTER run2 = p2.add_run('SUARL Rayhan') run2.font.name = 'Calibri' run2.font.size = Pt(22) run2.font.bold = True run2.font.color.rgb = BLEU_MOYEN separator(doc) for _ in range(2): doc.add_paragraph() # Sous-titre p3 = doc.add_paragraph() p3.alignment = WD_ALIGN_PARAGRAPH.CENTER run3 = p3.add_run('Rapport de Projet de Fin d\'Études') run3.font.name = 'Calibri' run3.font.size = Pt(14) run3.font.italic = True run3.font.color.rgb = GRIS_TEXTE for _ in range(3): doc.add_paragraph() # Infos infos = [ ('Étudiant', 'Ali Guennari'), ('Entreprise d\'accueil', 'SUARL Rayhan — Plasturgie, Tataouine, Tunisie'), ('Encadrant académique', 'À compléter'), ('Encadrant professionnel', 'À compléter'), ('Année universitaire', '2025 / 2026'), ] for label, val in infos: p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER r1 = p.add_run(f'{label} : ') r1.font.name = 'Calibri' r1.font.size = Pt(12) r1.font.bold = True r1.font.color.rgb = BLEU_FONCE r2 = p.add_run(val) r2.font.name = 'Calibri' r2.font.size = Pt(12) r2.font.color.rgb = GRIS_TEXTE p.paragraph_format.space_after = Pt(6) for _ in range(2): doc.add_paragraph() p_date = doc.add_paragraph() p_date.alignment = WD_ALIGN_PARAGRAPH.CENTER r_date = p_date.add_run('Avril 2026') r_date.font.name = 'Calibri' r_date.font.size = Pt(12) r_date.font.color.rgb = RGBColor(0x70, 0x70, 0x70) # Saut de page doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 1 — PRÉSENTATION DU PROJET # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 1 — Présentation du Projet', 1) separator(doc) styled_heading(doc, '1.1 Contexte', 2) body(doc, 'SUARL Rayhan est une entreprise tunisienne spécialisée dans la fabrication d\'emballages ' 'plastiques implantée à Tataouine. Son catalogue comprend quatre produits finis : sacs Bertel, ' 'sacs poubelles, sacs alimentaires et film rétractable, produits à partir de matières premières ' 'telles que le HDPE (Polyéthylène Haute Densité) et le LDPE (Polyéthylène Basse Densité).') body(doc, 'Face à une gestion encore manuelle de ses processus métier — achats, ventes, production et stock — ' 'la direction a exprimé le besoin d\'un système d\'information intégré permettant de centraliser ' 'et d\'automatiser l\'ensemble de ces opérations.') styled_heading(doc, '1.2 Objectif du Projet', 2) body(doc, 'Ce projet de fin d\'études consiste à concevoir et développer un ERP (Enterprise Resource ' 'Planning) sur mesure, adapté aux spécificités de SUARL Rayhan, couvrant les domaines suivants :') for item in [ 'Gestion du référentiel articles (matières premières, produits semi-finis, produits finis)', 'Cycle d\'achat complet : commandes fournisseurs → bons de réception → mise à jour du stock', 'Cycle de vente complet : commandes clients → bons de livraison → sortie de stock', 'Gestion de la production : nomenclatures BOM, ordres de fabrication avec suivi de statut', 'Suivi des stocks en temps réel avec alertes de seuil minimum', 'Tableau de bord KPI pour la direction (PDG)', ]: bullet(doc, item) styled_heading(doc, '1.3 Périmètre Fonctionnel', 2) add_table(doc, ['Module', 'Description'], [ ['Authentification', 'Connexion sécurisée avec rôles différenciés (6 rôles)'], ['Articles', 'Gestion du catalogue : MP, Produits Semi-Finis, Produits Finis'], ['Clients & Fournisseurs', 'Annuaire des tiers commerciaux'], ['Cycle Achat', 'Commande fournisseur → Réception → Entrée en stock automatique'], ['Cycle Vente', 'Commande client → Livraison → Sortie de stock automatique'], ['Production', 'Nomenclatures BOM + Ordres de Fabrication (planif./lancement/clôture)'], ['Stock', 'Mouvements horodatés, historique complet, alertes seuil minimum'], ['Tableau de bord', 'KPIs temps réel réservés au PDG'], ], col_widths=[5, 12] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 2 — ARCHITECTURE TECHNIQUE # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 2 — Architecture Technique', 1) separator(doc) styled_heading(doc, '2.1 Stack Technologique', 2) add_table(doc, ['Couche', 'Technologie', 'Version'], [ ['Backend API', 'Spring Boot', '3.2.5'], ['Langage', 'Java', '17 LTS'], ['Sécurité', 'Spring Security + JWT', 'JJWT 0.12.5'], ['Persistance', 'Spring Data JPA / Hibernate','6.4.4'], ['Base de données','MySQL', '8.0'], ['Conteneurisation','Docker + Docker Compose', '—'], ['Frontend', 'Flutter Web', '3.x'], ], col_widths=[5, 8, 4] ) styled_heading(doc, '2.2 Architecture N-Tiers', 2) body(doc, 'L\'API suit rigoureusement le patron d\'architecture Controller → Service → Repository → Model. ' 'Chaque couche a une responsabilité unique et clairement définie :') add_table(doc, ['Couche', 'Rôle', 'Exemple'], [ ['Controller', 'Reçoit la requête HTTP, valide, délègue au service, retourne la réponse', 'ArticleController.java'], ['Service', 'Contient la logique métier : règles, validations, transactions', 'StockService.java'], ['Repository', 'Abstraction d\'accès à la base de données (Spring Data JPA)', 'ArticleRepository.java'], ['Model', 'Entités JPA mappées sur les tables MySQL', 'Article.java'], ], col_widths=[4, 10, 5] ) styled_heading(doc, '2.3 Sécurité — Authentification JWT', 2) body(doc, 'Le système utilise une authentification stateless basée sur les JSON Web Tokens (RFC 7519). ' 'Le flux d\'authentification est le suivant :') steps = [ '1. Le client envoie ses identifiants à POST /api/auth/signin', '2. Le serveur valide les credentials et retourne un JWT signé (valable 24 heures)', '3. Le client inclut Authorization: Bearer dans chaque requête suivante', '4. Le filtre AuthTokenFilter intercepte et valide le token avant chaque endpoint sécurisé', ] for s in steps: body(doc, s, space_after=4) doc.add_paragraph() styled_heading(doc, '2.4 Rôles et Contrôle d\'Accès', 2) add_table(doc, ['Rôle', 'Périmètre d\'accès'], [ ['ROLE_PDG', 'Accès complet à tous les modules + tableau de bord'], ['ROLE_RESPONSABLE_VENTE', 'Commandes clients, bons de livraison, clients'], ['ROLE_RESPONSABLE_ACHAT', 'Commandes fournisseurs, bons de réception, fournisseurs'], ['ROLE_RESPONSABLE_PRODUCTION', 'Nomenclatures BOM, ordres de fabrication'], ['ROLE_MAGASINIER', 'Mouvements de stock, ajustements, historique'], ['ROLE_RH', 'Ressources humaines (module à venir)'], ], col_widths=[6, 11] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 3 — ENDPOINTS DE L'API # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 3 — Endpoints de l\'API REST', 1) separator(doc) styled_heading(doc, '3.1 Authentification', 2) add_table(doc, ['Méthode', 'URL', 'Description'], [ ['POST', '/api/auth/signin', 'Connexion — retourne un JWT Bearer'], ['POST', '/api/auth/signup', 'Créer un nouvel utilisateur'], ], col_widths=[3, 6, 9] ) styled_heading(doc, '3.2 Articles', 2) add_table(doc, ['Méthode', 'URL', 'Rôles autorisés'], [ ['GET', '/api/articles', 'Tous'], ['GET', '/api/articles/{id}', 'Tous'], ['GET', '/api/articles/type/{type}', 'Tous — types : MP, PSF, PF'], ['GET', '/api/articles/alertes-stock', 'PDG, Magasinier, Production'], ['POST', '/api/articles', 'PDG, Production, Magasinier'], ['PUT', '/api/articles/{id}', 'PDG, Production'], ['DELETE', '/api/articles/{id}', 'PDG (désactivation logique)'], ], col_widths=[3, 6.5, 7.5] ) styled_heading(doc, '3.3 Clients & Fournisseurs', 2) add_table(doc, ['Méthode', 'URL', 'Rôles autorisés'], [ ['GET', '/api/clients', 'PDG, Vente'], ['POST', '/api/clients', 'PDG, Vente'], ['PUT', '/api/clients/{id}', 'PDG, Vente'], ['GET', '/api/fournisseurs', 'PDG, Achat'], ['POST', '/api/fournisseurs', 'PDG, Achat'], ['PUT', '/api/fournisseurs/{id}', 'PDG, Achat'], ], col_widths=[3, 6.5, 7.5] ) styled_heading(doc, '3.4 Cycle d\'Achat', 2) add_table(doc, ['Méthode', 'URL', 'Description'], [ ['GET', '/api/purchase-orders', 'Liste des commandes fournisseurs'], ['POST', '/api/purchase-orders', 'Créer une commande fournisseur'], ['POST', '/api/purchase-orders/{id}/receive', 'Réceptionner → crée BR + entre en stock'], ], col_widths=[3, 7, 8] ) styled_heading(doc, '3.5 Cycle de Vente', 2) add_table(doc, ['Méthode', 'URL', 'Description'], [ ['GET', '/api/sales-orders', 'Liste des commandes clients'], ['POST', '/api/sales-orders', 'Créer une commande client'], ['POST', '/api/sales-orders/{id}/deliver', 'Livrer → crée BL + sort du stock'], ], col_widths=[3, 7, 8] ) styled_heading(doc, '3.6 Production', 2) add_table(doc, ['Méthode', 'URL', 'Description'], [ ['GET', '/api/production/bom/{produitId}', 'Nomenclature d\'un produit fini'], ['POST', '/api/production/bom', 'Définir une ligne de nomenclature'], ['DELETE', '/api/production/bom/{id}', 'Supprimer une ligne BOM'], ['GET', '/api/production/orders', 'Liste des ordres de fabrication'], ['POST', '/api/production/orders/plan', 'Planifier un OF'], ['POST', '/api/production/orders/{id}/launch', 'Lancer un OF → consomme les MP'], ['POST', '/api/production/orders/{id}/complete','Clôturer un OF → entre les PF en stock'], ], col_widths=[3, 8, 7] ) styled_heading(doc, '3.7 Stock & Tableau de Bord', 2) add_table(doc, ['Méthode', 'URL', 'Description'], [ ['GET', '/api/stock/historique/{articleId}', 'Historique des mouvements d\'un article'], ['POST', '/api/stock/adjust', 'Ajustement manuel du stock'], ['GET', '/api/dashboard', 'KPIs consolidés (PDG uniquement)'], ], col_widths=[3, 7.5, 7.5] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 4 — DÉPLOIEMENT # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 4 — Déploiement', 1) separator(doc) styled_heading(doc, '4.1 Infrastructure Docker', 2) body(doc, 'L\'application est containerisée via Docker Compose. Deux conteneurs coexistent ' 'dans un réseau Docker privé (rayhan-net) :') add_table(doc, ['Conteneur', 'Image', 'Rôle'], [ ['rayhan-mysql', 'mysql:8.0', 'Base de données — rayhan_erp_db'], ['rayhan-backend', 'Build local Maven', 'API REST Spring Boot — port 8080 interne'], ], col_widths=[4, 5, 9] ) styled_heading(doc, '4.2 Build Multi-Étapes (Dockerfile)', 2) body(doc, 'Le Dockerfile utilise un build multi-étapes pour minimiser la taille de l\'image finale. ' 'L\'étape de build (Maven + JDK 17) produit le JAR exécutable ; l\'étape d\'exécution ' 'utilise uniquement le JRE Alpine (image légère) :') add_code_block(doc, '# Étape 1 : Compilation Maven\n' 'FROM maven:3.9.6-eclipse-temurin-17 AS build\n' 'WORKDIR /app\n' 'COPY pom.xml .\n' 'RUN mvn dependency:go-offline\n' 'COPY src ./src\n' 'RUN mvn clean package -DskipTests\n\n' '# Étape 2 : Image d\'exécution légère\n' 'FROM eclipse-temurin:17-jre-alpine\n' 'WORKDIR /app\n' 'COPY --from=build /app/target/erp-1.0.0-SNAPSHOT.jar app.jar\n' 'EXPOSE 8080\n' 'ENTRYPOINT ["java", "-jar", "app.jar"]' ) styled_heading(doc, '4.3 Reverse Proxy HTTPS', 2) body(doc, 'En production, un reverse proxy (Nginx ou Traefik) se place devant le conteneur backend. ' 'Il prend en charge le certificat TLS et transmet les requêtes au port interne 8080. ' 'Spring Boot est configuré pour reconnaître les headers X-Forwarded-Proto et X-Forwarded-For ' 'via la propriété server.forward-headers-strategy=framework.') add_table(doc, ['Environnement', 'URL d\'accès'], [ ['Production (HTTPS)', 'https://rayhan-erp.bolbol.tn'], ['Développement local', 'http://localhost:8090'], ['Documentation API', 'https://rayhan-erp.bolbol.tn/swagger-ui/index.html'], ], col_widths=[5, 12] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 5 — TESTS ET VALIDATION # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 5 — Tests et Validation', 1) separator(doc) styled_heading(doc, '5.1 Interface Swagger UI', 2) body(doc, 'L\'API intègre Swagger UI (SpringDoc OpenAPI 2.5.0), une interface web interactive accessible ' 'depuis n\'importe quel navigateur sans installation. Elle permet de visualiser tous les endpoints, ' 'de s\'authentifier avec le JWT et de tester chaque fonctionnalité en un clic.') body(doc, 'Procédure de connexion via Swagger UI :') for s in [ '1. Ouvrir https://rayhan-erp.bolbol.tn/swagger-ui/index.html', '2. Exécuter POST /api/auth/signin avec {"username":"admin","password":"Rayhan2024!"}', '3. Copier la valeur du champ token dans la réponse JSON', '4. Cliquer sur le bouton Authorize (cadenas) en haut de la page', '5. Coller le token et cliquer Authorize — tous les endpoints sont maintenant accessibles', ]: body(doc, s, space_after=3) styled_heading(doc, '5.2 Scénario de Test Complet', 2) body(doc, 'Le tableau suivant décrit le scénario de test de bout en bout validant les quatre cycles métier :') add_table(doc, ['Étape', 'Action', 'Résultat attendu'], [ ['1', 'Connexion admin', 'Token JWT retourné'], ['2', 'Créer article MP-HDPE', 'Article créé, stock = 0'], ['3', 'Créer article PF-Sac Bertel', 'Article créé, stock = 0'], ['4', 'Créer commande fournisseur (1000 kg)', 'Statut CONFIRMEE, totalTTC calculé'], ['5', 'Réceptionner la commande', 'Stock HDPE = 1 000 kg'], ['6', 'Définir nomenclature BOM', '0,015 kg HDPE par Sac Bertel'], ['7', 'Planifier OF (10 000 unités)', 'Stock HDPE suffisant : 150 kg requis'], ['8', 'Lancer l\'OF', 'Stock HDPE = 850 kg (−150 kg consommés)'], ['9', 'Clôturer l\'OF', 'Stock Sac Bertel = 9 800 unités'], ['10', 'Créer commande client (5 000 unités)', 'Statut CONFIRMEE'], ['11', 'Livrer la commande', 'Stock Sac Bertel = 4 800 unités'], ['12', 'Consulter le tableau de bord', 'KPIs mis à jour en temps réel'], ], col_widths=[1.5, 6, 9.5] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # CHAPITRE 6 — FRONTEND FLUTTER WEB # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Chapitre 6 — Frontend Flutter Web', 1) separator(doc) styled_heading(doc, '6.1 Choix Technologique', 2) body(doc, 'Le frontend est développé avec Flutter Web, ce qui permet de produire une application web ' 'moderne à partir d\'une unique base de code. L\'application sera containerisée dans Docker ' 'et servie via Nginx, exactement comme le backend.') styled_heading(doc, '6.2 Architecture Flutter', 2) add_table(doc, ['Composant', 'Technologie', 'Rôle'], [ ['State management', 'Provider / Riverpod', 'Gestion de l\'état global (token, données)'], ['HTTP client', 'Dio', 'Appels API avec intercepteur JWT automatique'], ['Stockage local', 'shared_preferences', 'Persistance du token JWT'], ['Navigation', 'go_router', 'Routing déclaratif avec garde d\'authentification'], ['Conteneurisation', 'Docker + Nginx', 'Build Flutter → fichiers statiques servis par Nginx'], ], col_widths=[4, 4, 9] ) styled_heading(doc, '6.3 Écrans à Développer', 2) add_table(doc, ['Priorité', 'Écran', 'Endpoints utilisés'], [ ['1', 'Login', 'POST /api/auth/signin'], ['2', 'Tableau de bord', 'GET /api/dashboard'], ['3', 'Articles', 'GET/POST/PUT /api/articles'], ['4', 'Commandes vente', 'GET/POST /api/sales-orders + /deliver'], ['5', 'Commandes achat', 'GET/POST /api/purchase-orders + /receive'], ['6', 'Production', 'GET/POST /api/production/bom + /orders'], ['7', 'Historique stock', 'GET /api/stock/historique/{id}'], ], col_widths=[2.5, 5, 9.5] ) doc.add_page_break() # ════════════════════════════════════════════════════════════════════════════ # ANNEXES # ════════════════════════════════════════════════════════════════════════════ styled_heading(doc, 'Annexes', 1) separator(doc) styled_heading(doc, 'A — Modèle de Données Relationnel', 2) body(doc, 'La base de données rayhan_erp_db contient les tables suivantes :') add_table(doc, ['Table', 'Description'], [ ['users / roles / user_roles', 'Authentification et droits d\'accès'], ['tiers', 'Table mère (héritage JOINED) — données communes clients/fournisseurs'], ['clients', 'Extension de tiers : plafond crédit, délai paiement'], ['fournisseurs', 'Extension de tiers : pays, catégorie produit, délai livraison'], ['articles', 'Catalogue : référence, type MP/PSF/PF, stock actuel/minimum'], ['bom_lines', 'Nomenclatures : lien produit fini ↔ composant + quantité'], ['production_orders', 'Ordres de fabrication : PLANIFIE → LANCE → TERMINE'], ['purchase_orders / purchase_order_lines', 'Commandes fournisseurs et leurs lignes'], ['goods_receipts / goods_receipt_lines', 'Bons de réception'], ['sales_orders / sales_order_lines', 'Commandes clients et leurs lignes'], ['delivery_notes / delivery_note_lines', 'Bons de livraison'], ['stock_movements', 'Historique complet IN/OUT avec stockAvant/stockAprès'], ], col_widths=[6, 11] ) styled_heading(doc, 'B — Initialisation Automatique', 2) body(doc, 'Au premier démarrage, la classe DataInitializer (CommandLineRunner Spring) crée automatiquement ' 'les 6 rôles dans la base de données ainsi que l\'utilisateur administrateur par défaut :') add_table(doc, ['Paramètre', 'Valeur'], [ ['Nom d\'utilisateur', 'admin'], ['Mot de passe', 'Rayhan2024!'], ['Rôle', 'ROLE_PDG (accès complet)'], ['Email', 'admin@rayhan.tn'], ], col_widths=[5, 12] ) styled_heading(doc, 'C — Commandes Docker Essentielles', 2) add_code_block(doc, '# Démarrer l\'application (première fois ou après modification)\n' 'docker compose up -d --build\n\n' '# Démarrer sans rebuild\n' 'docker compose up -d\n\n' '# Arrêter sans supprimer les données\n' 'docker compose down\n\n' '# Voir les logs de l\'API en temps réel\n' 'docker logs rayhan-backend -f\n\n' '# Accéder directement à MySQL\n' 'docker exec -it rayhan-mysql mysql -u root -prayhan_erp_2024 rayhan_erp_db' ) # ── Sauvegarde ─────────────────────────────────────────────────────────────── output_path = 'Livrables/Rapport-ERP-Rayhan-Ali-Guennari.docx' doc.save(output_path) print(f'Document généré : {output_path}')