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, render_xml from generate_facturx_jinja2 import Address, Party, Invoice, InvoiceLine, generate_facturx_xml 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) from generate_facturx_jinja2 import Invoice, Party, Address, InvoiceLine, filter_amount, filter_datefmt @router.get("/{facture_id}/facturx") def telecharger_facturx(request: Request, facture_id: int, db: Session = Depends(get_db)): facture = get_invoice_data(facture_id, db) # votre appel BDD # facture = db.query(Facture).get(facture_id) if not facture: raise HTTPException(status_code=404) xml_bytes = generate_facturx_xml(facture) filename=f"factur-x-{facture_id}.xml" return Response( content= xml_bytes, media_type="application/xml", headers={"Content-Disposition": f'attachment; filename="{filename}"'} ) @router.get("/{facture_id}/pdf", response_class=HTMLResponse) def telecharger_pdf(request: Request, 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, }) def get_invoice_data(facture_id: int, db: Session): facture = db.query(Facture).get(facture_id) invoice = Invoice( number=facture.numero, issue_date=facture.date_emission, due_date=facture.date_echeance, currency="EUR", type_code="380", note=("Facture établie suite au devis " + facture.devis_origine.numero), seller=Party( name=settings.asso_nom, #vat_id="FR12345678901", siret=settings.asso_siret, address=Address( line1=settings.asso_adresse, city=settings.asso_ville, postcode=settings.asso_code_postal, country_code="FR", ), ), buyer=Party( name=facture.client.nom, #vat_id="FR98765432100", siret=facture.client.siret, address=Address( line1=facture.client.adresse, city=facture.client.ville, postcode=facture.client.code_postal, country_code="FR", ), ), payment_means_code="30", # 30 = virement bancaire iban=settings.asso_iban, payment_reference=facture.devis_origine.numero, ) for l in facture.lignes: invoice.lines.append(InvoiceLine( line_id=l.id, description=l.description, quantity=l.quantite, unit_code="C62", unit_price=l.prix_unitaire_ht, vat_category="E", vat_rate=0.0, )) return invoice