rayhan-erp/Livrables/generate_report.py

700 lines
31 KiB
Python
Raw Permalink 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.

"""
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 <token> 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 6.x', 'Gestion de l\'état global (token, données chargées)'],
['HTTP client', 'Dio 5.x', 'Appels API avec intercepteur JWT automatique'],
['Stockage local', 'shared_preferences', 'Persistance du token JWT entre sessions'],
['Navigation', 'go_router 13.x', 'Routing déclaratif avec garde d\'authentification'],
['Graphiques', 'fl_chart', 'Graphiques pour le tableau de bord KPI'],
['Formatage', 'intl', 'Formatage des devises (TND) et des dates en français'],
],
col_widths=[4, 4, 9]
)
styled_heading(doc, '6.3 Écrans Réalisés', 2)
add_table(doc,
['Écran', 'Statut', 'Endpoints utilisés'],
[
['Login', '✅ Terminé', 'POST /api/auth/signin'],
['Tableau de bord', '✅ Terminé', 'GET /api/dashboard'],
['Articles', '✅ Terminé', 'GET/POST/PUT/DELETE /api/articles'],
['Commandes vente', '✅ Terminé', 'GET/POST /api/sales-orders + /deliver'],
['Commandes achat', '✅ Terminé', 'GET/POST /api/purchase-orders + /receive'],
['Production', '✅ Terminé', 'GET/POST /api/production/bom + /orders'],
['Stock & Historique', '✅ Terminé', 'GET /api/stock/historique + POST /adjust'],
],
col_widths=[4, 2.5, 10.5]
)
styled_heading(doc, '6.4 Fonctionnalités Clés du Frontend', 2)
for item in [
'Intercepteur JWT automatique : le token est injecté dans chaque requête HTTP sans code répétitif',
'Routing sécurisé : GoRouter redirige automatiquement vers /login si non authentifié',
'Calcul en temps réel HT/TVA/TTC dans les formulaires de commande',
'Vérification du stock disponible avant planification d\'un ordre de fabrication',
'Affichage dynamique de la nomenclature BOM lors de la sélection du produit',
'Ajustement manuel du stock avec traçabilité complète (motif, date, opérateur)',
'Barre de progression visuelle dans l\'écran stock (vert → rouge selon le niveau)',
'Pull-to-refresh sur toutes les listes pour actualiser les données',
]:
bullet(doc, item)
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}')