Init project

This commit is contained in:
seb
2026-02-21 23:26:50 +01:00
parent df61e93871
commit b7046b125c
29 changed files with 2553 additions and 0 deletions

162
routers/auth.py Normal file
View File

@@ -0,0 +1,162 @@
from template_helper import render
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import User
from auth import (
hasher_mot_de_passe, verifier_mot_de_passe,
creer_session, get_current_admin, COOKIE_NAME
)
from config import settings
router = APIRouter(tags=["auth"])
templates = Jinja2Templates(directory="templates")
# ── Login / Logout ─────────────────────────────────────────────────────────────
@router.get("/login", response_class=HTMLResponse)
def login_form(request: Request):
return render(templates, "auth/login.html", request, {
"erreur": None
})
@router.post("/login")
def login(
request: Request,
username: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
user = db.query(User).filter(
User.username == username, User.actif == True
).first()
if not user or not verifier_mot_de_passe(password, user.hashed_password):
return render(templates, "auth/login.html", request, {
"erreur": "Identifiants incorrects."
}, status_code=401)
token = creer_session(user.id)
response = RedirectResponse("/factures/", status_code=303)
response.set_cookie(
key=COOKIE_NAME,
value=token,
max_age=settings.session_max_age,
httponly=True,
samesite="lax",
secure=False # Passer à True si HTTPS (recommandé en prod)
)
return response
@router.post("/logout")
def logout():
response = RedirectResponse("/login", status_code=303)
response.delete_cookie(COOKIE_NAME)
return response
# ── Gestion utilisateurs (admin) ───────────────────────────────────────────────
@router.get("/admin/utilisateurs", response_class=HTMLResponse)
def liste_utilisateurs(
request: Request,
db: Session = Depends(get_db),
current_user=Depends(get_current_admin)
):
users = db.query(User).order_by(User.username).all()
return render(templates, "auth/utilisateurs.html", request, {
"users": users, "current_user": current_user
})
@router.get("/admin/utilisateurs/nouveau", response_class=HTMLResponse)
def nouveau_user_form(request: Request, current_user=Depends(get_current_admin)):
return render(templates, "auth/user_form.html", request, {
"user": None,
"titre": "Nouvel utilisateur", "current_user": current_user
})
@router.post("/admin/utilisateurs/nouveau")
def creer_user(
username: str = Form(...),
email: str = Form(""),
password: str = Form(...),
is_admin: bool = Form(False),
db: Session = Depends(get_db),
current_user=Depends(get_current_admin)
):
if db.query(User).filter(User.username == username).first():
raise HTTPException(status_code=400, detail="Ce nom d'utilisateur existe déjà.")
user = User(
username=username,
email=email,
hashed_password=hasher_mot_de_passe(password),
is_admin=is_admin
)
db.add(user)
db.commit()
return RedirectResponse("/admin/utilisateurs", status_code=303)
@router.get("/admin/utilisateurs/{user_id}/modifier", response_class=HTMLResponse)
def modifier_user_form(
request: Request,
user_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_admin)
):
user = db.query(User).get(user_id)
if not user:
raise HTTPException(status_code=404)
return render(templates, "auth/user_form.html", request, {
"user": user,
"titre": "Modifier l'utilisateur", "current_user": current_user
})
@router.post("/admin/utilisateurs/{user_id}/modifier")
def modifier_user(
user_id: int,
username: str = Form(...),
email: str = Form(""),
password: str = Form(""),
is_admin: bool = Form(False),
db: Session = Depends(get_db),
current_user=Depends(get_current_admin)
):
user = db.query(User).get(user_id)
if not user:
raise HTTPException(status_code=404)
# Empêcher de se retirer les droits admin à soi-même
if user.id == current_user.id and not is_admin:
raise HTTPException(status_code=400, detail="Vous ne pouvez pas retirer vos propres droits admin.")
user.username = username
user.email = email
user.is_admin = is_admin
if password:
user.hashed_password = hasher_mot_de_passe(password)
db.commit()
return RedirectResponse("/admin/utilisateurs", status_code=303)
@router.post("/admin/utilisateurs/{user_id}/desactiver")
def desactiver_user(
user_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_admin)
):
user = db.query(User).get(user_id)
if not user:
raise HTTPException(status_code=404)
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="Impossible de se désactiver soi-même.")
user.actif = not user.actif
db.commit()
return RedirectResponse("/admin/utilisateurs", status_code=303)

95
routers/clients.py Normal file
View File

@@ -0,0 +1,95 @@
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Client
from auth import get_current_user
from template_helper import render
router = APIRouter(prefix="/clients", tags=["clients"],
dependencies=[Depends(get_current_user)])
templates = Jinja2Templates(directory="templates")
@router.get("/", response_class=HTMLResponse)
def liste_clients(request: Request, db: Session = Depends(get_db)):
clients = db.query(Client).filter(Client.actif == True).order_by(Client.nom).all()
return render(templates, "clients/liste.html", request, {
"clients": clients
})
@router.get("/nouveau", response_class=HTMLResponse)
def nouveau_client_form(request: Request):
return render(templates, "clients/form.html", request, {
"client": None, "titre": "Nouveau client"
})
@router.post("/nouveau")
def creer_client(
request: Request,
nom: str = Form(...),
adresse: str = Form(...),
code_postal: str = Form(...),
ville: str = Form(...),
email: str = Form(""),
telephone: str = Form(""),
siret: str = Form(""),
db: Session = Depends(get_db)
):
client = Client(
nom=nom, adresse=adresse, code_postal=code_postal,
ville=ville, email=email, telephone=telephone, siret=siret
)
db.add(client)
db.commit()
return RedirectResponse("/clients/", status_code=303)
@router.get("/{client_id}/modifier", response_class=HTMLResponse)
def modifier_client_form(request: Request, client_id: int, db: Session = Depends(get_db)):
client = db.query(Client).get(client_id)
if not client:
raise HTTPException(status_code=404)
return render(templates, "clients/form.html", request, {
"client": client, "titre": "Modifier le client"
})
@router.post("/{client_id}/modifier")
def modifier_client(
client_id: int,
nom: str = Form(...),
adresse: str = Form(...),
code_postal: str = Form(...),
ville: str = Form(...),
email: str = Form(""),
telephone: str = Form(""),
siret: str = Form(""),
db: Session = Depends(get_db)
):
client = db.query(Client).get(client_id)
if not client:
raise HTTPException(status_code=404)
client.nom = nom
client.adresse = adresse
client.code_postal = code_postal
client.ville = ville
client.email = email
client.telephone = telephone
client.siret = siret
db.commit()
return RedirectResponse("/clients/", status_code=303)
@router.post("/{client_id}/supprimer")
def supprimer_client(client_id: int, db: Session = Depends(get_db)):
client = db.query(Client).get(client_id)
if not client:
raise HTTPException(status_code=404)
client.actif = False
db.commit()
return RedirectResponse("/clients/", status_code=303)

186
routers/devis.py Normal file
View File

@@ -0,0 +1,186 @@
import json
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Devis, LigneDevis, Client, StatutDevis
from numerotation import generer_numero_devis
from auth import get_current_user
from template_helper import render
router = APIRouter(prefix="/devis", tags=["devis"], dependencies=[Depends(get_current_user)])
templates = Jinja2Templates(directory="templates")
@router.get("/", response_class=HTMLResponse)
def liste_devis(request: Request, db: Session = Depends(get_db)):
devis = db.query(Devis).order_by(Devis.date_emission.desc()).all()
return render(templates, "devis/liste.html", request, {
"devis": devis
})
@router.get("/nouveau", response_class=HTMLResponse)
def nouveau_devis_form(request: Request, db: Session = Depends(get_db)):
clients = db.query(Client).filter(Client.actif == True).order_by(Client.nom).all()
return render(templates, "devis/form.html", request, {
"devis": None,
"clients": clients,
"titre": "Nouveau devis",
"date_aujourd_hui": date.today().isoformat(),
"date_validite_defaut": (date.today() + timedelta(days=30)).isoformat(),
})
@router.post("/nouveau")
def creer_devis(
request: Request,
client_id: int = Form(...),
date_emission: str = Form(...),
date_validite: str = Form(...),
notes: str = Form(""),
conditions: str = Form(""),
lignes_json: str = Form(...),
db: Session = Depends(get_db)
):
numero = generer_numero_devis(db)
devis = Devis(
numero=numero,
client_id=client_id,
date_emission=date.fromisoformat(date_emission),
date_validite=date.fromisoformat(date_validite),
notes=notes,
conditions=conditions,
)
db.add(devis)
db.flush()
lignes = json.loads(lignes_json)
for i, l in enumerate(lignes):
ligne = LigneDevis(
devis_id=devis.id,
description=l["description"],
quantite=float(l["quantite"]),
prix_unitaire_ht=float(l["prix_unitaire_ht"]),
ordre=i
)
db.add(ligne)
db.commit()
return RedirectResponse(f"/devis/{devis.id}", status_code=303)
@router.get("/{devis_id}", response_class=HTMLResponse)
def voir_devis(request: Request, devis_id: int, db: Session = Depends(get_db)):
devis = db.query(Devis).get(devis_id)
if not devis:
raise HTTPException(status_code=404)
return render(templates, "devis/detail.html", request, {
"devis": devis, "StatutDevis": StatutDevis
})
@router.get("/{devis_id}/modifier", response_class=HTMLResponse)
def modifier_devis_form(request: Request, devis_id: int, db: Session = Depends(get_db)):
devis = db.query(Devis).get(devis_id)
if not devis:
raise HTTPException(status_code=404)
clients = db.query(Client).filter(Client.actif == True).order_by(Client.nom).all()
return render(templates, "devis/form.html", request, {
"devis": devis,
"clients": clients,
"titre": "Modifier le devis",
"date_aujourd_hui": date.today().isoformat(),
"date_validite_defaut": devis.date_validite.isoformat(),
})
@router.post("/{devis_id}/modifier")
def modifier_devis(
devis_id: int,
client_id: int = Form(...),
date_emission: str = Form(...),
date_validite: str = Form(...),
notes: str = Form(""),
conditions: str = Form(""),
lignes_json: str = Form(...),
db: Session = Depends(get_db)
):
devis = db.query(Devis).get(devis_id)
if not devis:
raise HTTPException(status_code=404)
devis.client_id = client_id
devis.date_emission = date.fromisoformat(date_emission)
devis.date_validite = date.fromisoformat(date_validite)
devis.notes = notes
devis.conditions = conditions
for ligne in devis.lignes:
db.delete(ligne)
db.flush()
lignes = json.loads(lignes_json)
for i, l in enumerate(lignes):
ligne = LigneDevis(
devis_id=devis.id,
description=l["description"],
quantite=float(l["quantite"]),
prix_unitaire_ht=float(l["prix_unitaire_ht"]),
ordre=i
)
db.add(ligne)
db.commit()
return RedirectResponse(f"/devis/{devis_id}", status_code=303)
@router.post("/{devis_id}/statut")
def changer_statut_devis(
devis_id: int,
statut: str = Form(...),
db: Session = Depends(get_db)
):
devis = db.query(Devis).get(devis_id)
if not devis:
raise HTTPException(status_code=404)
devis.statut = StatutDevis(statut)
db.commit()
return RedirectResponse(f"/devis/{devis_id}", status_code=303)
@router.post("/{devis_id}/convertir")
def convertir_en_facture(devis_id: int, db: Session = Depends(get_db)):
from models import Facture, LigneFacture
from numerotation import generer_numero_facture
devis = db.query(Devis).get(devis_id)
if not devis:
raise HTTPException(status_code=404)
numero = generer_numero_facture(db)
facture = Facture(
numero=numero,
client_id=devis.client_id,
devis_id=devis.id,
date_emission=date.today(),
date_echeance=date.today() + timedelta(days=30),
notes=devis.notes,
)
db.add(facture)
db.flush()
for i, l in enumerate(devis.lignes):
ligne = LigneFacture(
facture_id=facture.id,
description=l.description,
quantite=l.quantite,
prix_unitaire_ht=l.prix_unitaire_ht,
ordre=i
)
db.add(ligne)
devis.statut = StatutDevis.accepte
db.commit()
return RedirectResponse(f"/factures/{facture.id}", status_code=303)

189
routers/factures.py Normal file
View File

@@ -0,0 +1,189 @@
import json
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Facture, LigneFacture, Client, StatutFacture
from numerotation import generer_numero_facture
from config import settings
from auth import get_current_user
from template_helper import render
router = APIRouter(prefix="/factures", tags=["factures"], dependencies=[Depends(get_current_user)])
templates = Jinja2Templates(directory="templates")
@router.get("/", response_class=HTMLResponse)
def liste_factures(request: Request, db: Session = Depends(get_db)):
factures = db.query(Facture).order_by(Facture.date_emission.desc()).all()
return render(templates, "factures/liste.html", request, {
"factures": factures
})
@router.get("/nouvelle", response_class=HTMLResponse)
def nouvelle_facture_form(request: Request, db: Session = Depends(get_db)):
clients = db.query(Client).filter(Client.actif == True).order_by(Client.nom).all()
return render(templates, "factures/form.html", request, {
"facture": None,
"clients": clients,
"titre": "Nouvelle facture",
"date_aujourd_hui": date.today().isoformat(),
"date_echeance_defaut": (date.today() + timedelta(days=30)).isoformat(),
})
@router.post("/nouvelle")
def creer_facture(
client_id: int = Form(...),
date_emission: str = Form(...),
date_echeance: str = Form(...),
notes: str = Form(""),
conditions_reglement: str = Form("Paiement à réception de facture."),
lignes_json: str = Form(...),
db: Session = Depends(get_db)
):
numero = generer_numero_facture(db)
facture = Facture(
numero=numero,
client_id=client_id,
date_emission=date.fromisoformat(date_emission),
date_echeance=date.fromisoformat(date_echeance),
notes=notes,
conditions_reglement=conditions_reglement,
)
db.add(facture)
db.flush()
lignes = json.loads(lignes_json)
for i, l in enumerate(lignes):
ligne = LigneFacture(
facture_id=facture.id,
description=l["description"],
quantite=float(l["quantite"]),
prix_unitaire_ht=float(l["prix_unitaire_ht"]),
ordre=i
)
db.add(ligne)
db.commit()
return RedirectResponse(f"/factures/{facture.id}", status_code=303)
@router.get("/{facture_id}", response_class=HTMLResponse)
def voir_facture(request: Request, facture_id: int, db: Session = Depends(get_db)):
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
return render(templates, "factures/detail.html", request, {
"facture": facture, "StatutFacture": StatutFacture
})
@router.get("/{facture_id}/modifier", response_class=HTMLResponse)
def modifier_facture_form(request: Request, facture_id: int, db: Session = Depends(get_db)):
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
if facture.statut != StatutFacture.emise:
raise HTTPException(status_code=400, detail="Seules les factures émises peuvent être modifiées.")
clients = db.query(Client).filter(Client.actif == True).order_by(Client.nom).all()
return render(templates, "factures/form.html", request, {
"facture": facture,
"clients": clients,
"titre": "Modifier la facture",
"date_aujourd_hui": facture.date_emission.isoformat(),
"date_echeance_defaut": facture.date_echeance.isoformat(),
})
@router.post("/{facture_id}/modifier")
def modifier_facture(
facture_id: int,
client_id: int = Form(...),
date_emission: str = Form(...),
date_echeance: str = Form(...),
notes: str = Form(""),
conditions_reglement: str = Form(""),
lignes_json: str = Form(...),
db: Session = Depends(get_db)
):
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
facture.client_id = client_id
facture.date_emission = date.fromisoformat(date_emission)
facture.date_echeance = date.fromisoformat(date_echeance)
facture.notes = notes
facture.conditions_reglement = conditions_reglement
for ligne in facture.lignes:
db.delete(ligne)
db.flush()
lignes = json.loads(lignes_json)
for i, l in enumerate(lignes):
ligne = LigneFacture(
facture_id=facture.id,
description=l["description"],
quantite=float(l["quantite"]),
prix_unitaire_ht=float(l["prix_unitaire_ht"]),
ordre=i
)
db.add(ligne)
db.commit()
return RedirectResponse(f"/factures/{facture_id}", status_code=303)
@router.post("/{facture_id}/statut")
def changer_statut_facture(
facture_id: int,
statut: str = Form(...),
date_paiement: str = Form(""),
db: Session = Depends(get_db)
):
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
facture.statut = StatutFacture(statut)
if statut == "payee" and date_paiement:
facture.date_paiement = date.fromisoformat(date_paiement)
db.commit()
return RedirectResponse(f"/factures/{facture_id}", status_code=303)
@router.get("/{facture_id}/pdf")
def telecharger_pdf(facture_id: int, db: Session = Depends(get_db)):
from weasyprint import HTML
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
html_content = templates.get_template("pdf/facture.html").render({
"facture": facture,
"settings": settings,
})
pdf_bytes = HTML(string=html_content, base_url=".").write_pdf()
filename = f"facture-{facture.numero}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
)
@router.get("/{facture_id}/apercu-pdf", response_class=HTMLResponse)
def apercu_pdf(request: Request, facture_id: int, db: Session = Depends(get_db)):
facture = db.query(Facture).get(facture_id)
if not facture:
raise HTTPException(status_code=404)
return render(templates, "pdf/facture.html", request, {
"facture": facture,
"settings": settings,
})