docs: generate professional DOCX with python-docx (no letterhead)
This commit is contained in:
parent
30f64f01de
commit
3816f8b8f1
Binary file not shown.
|
|
@ -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 <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 / 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}')
|
||||
Loading…
Reference in New Issue