diff --git a/Livrables/Rapport-ERP-Rayhan-Ali-Guennari.docx b/Livrables/Rapport-ERP-Rayhan-Ali-Guennari.docx index 908ed17..b14c268 100644 Binary files a/Livrables/Rapport-ERP-Rayhan-Ali-Guennari.docx and b/Livrables/Rapport-ERP-Rayhan-Ali-Guennari.docx differ diff --git a/Livrables/generate_report.py b/Livrables/generate_report.py new file mode 100644 index 0000000..28ff388 --- /dev/null +++ b/Livrables/generate_report.py @@ -0,0 +1,685 @@ +""" +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}')