Files
BillManager/routers/factures.py
2026-03-16 17:47:16 +01:00

263 lines
8.7 KiB
Python

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
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