Compare commits

...

8 Commits

Author SHA1 Message Date
baa3827a20 Merge branch 'master' into devel 2026-03-16 23:51:35 +01:00
87e78314c6 update README 2026-03-16 23:47:27 +01:00
dbb5a29863 ajout des libs utilisées 2026-03-16 23:18:09 +01:00
f25cf02660 correction pour conformité complete PDF/A-3
codé avec l'aide de claude IA
2026-03-16 23:16:23 +01:00
bb12b3777f retrait des fonctions d'aide et finalisation de la génération de fichier pdf
conforme
2026-03-16 21:21:07 +01:00
b403efd056 Code cleanup 2026-03-16 17:47:16 +01:00
ab3bf493d2 Merge pull request 'devel' (#1) from jblb/BillManager:devel into master
Reviewed-on: seb_vallee/BillManager#1
2026-03-13 11:23:35 +01:00
31cf8054cd generate pdf file for devis 2026-03-13 11:23:05 +01:00
6 changed files with 78 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
# Facturation Association # Facturation Association
Application de facturation légale française pour associations (loi 1901), Application de facturation légale française pour associations (loi 1901) ou micro-entrepreneur,
non assujetties à la TVA (art. 293B du CGI). non assujetties à la TVA (art. 293B du CGI).
## Fonctionnalités ## Fonctionnalités
@@ -9,7 +9,7 @@ non assujetties à la TVA (art. 293B du CGI).
- Devis avec numérotation automatique (DEV-AAAA-XXXX) - Devis avec numérotation automatique (DEV-AAAA-XXXX)
- Factures avec numérotation chronologique (AAAA-XXXX) - Factures avec numérotation chronologique (AAAA-XXXX)
- Conversion devis → facture - Conversion devis → facture
- Génération PDF avec toutes les mentions légales françaises - Génération PDF avec toutes les mentions légales françaises et conforme aux normes de facturation electronique Factur-X
- Suivi des statuts (émise / payée / annulée) - Suivi des statuts (émise / payée / annulée)
## Stack ## Stack

View File

@@ -8,3 +8,5 @@ pydantic==2.9.2
pydantic-settings==2.5.2 pydantic-settings==2.5.2
bcrypt==4.2.0 bcrypt==4.2.0
itsdangerous==2.2.0 itsdangerous==2.2.0
factur-x==3.16
pikepdf==10.5.0

View File

@@ -10,8 +10,9 @@ from models import Facture, LigneFacture, Client, StatutFacture
from numerotation import generer_numero_facture from numerotation import generer_numero_facture
from config import settings from config import settings
from auth import get_current_user from auth import get_current_user
from template_helper import render, render_xml from template_helper import render
from generate_facturx_jinja2 import Address, Party, Invoice, InvoiceLine, generate_facturx_xml from generate_facturx_jinja2 import Address, Party, Invoice, InvoiceLine, generate_facturx_xml
from facturx import generate_from_binary
router = APIRouter(prefix="/factures", tags=["factures"], dependencies=[Depends(get_current_user)]) router = APIRouter(prefix="/factures", tags=["factures"], dependencies=[Depends(get_current_user)])
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@@ -158,41 +159,64 @@ def changer_statut_facture(
from generate_facturx_jinja2 import Invoice, Party, Address, InvoiceLine, filter_amount, filter_datefmt from generate_facturx_jinja2 import Invoice, Party, Address, InvoiceLine, filter_amount, filter_datefmt
@router.get("/{facture_id}/facturx") @router.get("/{facture_id}/pdf")
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)): def telecharger_pdf(request: Request, facture_id: int, db: Session = Depends(get_db)):
from weasyprint import HTML from weasyprint import HTML, Attachment
from facturx import generate_from_binary
import hashlib
import pikepdf
from pikepdf import Name
import io
facture = db.query(Facture).get(facture_id) facture = db.query(Facture).get(facture_id)
if not facture: if not facture:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
# 1. XML Factur-X
invoice = get_invoice_data(facture_id, db)
xml_str = generate_facturx_xml(invoice)
xml_bytes = xml_str.encode("utf-8")
# 2. Rendu HTML → PDF simple (pas PDF/A-3 ici)
html_content = templates.get_template("pdf/facture.html").render({ html_content = templates.get_template("pdf/facture.html").render({
"facture": facture, "facture": facture,
"settings": settings, "settings": settings,
}) })
pdf_bytes = HTML(string=html_content, base_url=".").write_pdf(
pdf_variant="pdf/a-3b",
)
pdf_bytes = HTML(string=html_content, base_url=".").write_pdf() # 3. generate_from_binary gère :
# - l'intégration du XML
# - les métadonnées XMP Factur-X (DocumentType, DocumentFileName, etc.)
# - la conformité PDF/A-3b
pdf_bytes = generate_from_binary(
pdf_bytes,
xml_bytes,
flavor="factur-x",
level="BASIC",
)
# 4. Corriger uniquement /EF/F/Subtype → /text/xml avec pikepdf
pdf_io = io.BytesIO(pdf_bytes)
with pikepdf.open(pdf_io) as pdf:
names = pdf.Root.Names.EmbeddedFiles.Names
i = 0
while i < len(names):
i += 1 # sauter la clé (string)
if i < len(names):
obj = names[i]
try:
obj.EF.F.Subtype = Name("/text/xml")
except Exception:
pass
i += 1
output = io.BytesIO()
pdf.save(output)
pdf_bytes = output.getvalue()
filename = f"facture-{facture.numero}.pdf" filename = f"facture-{facture.numero}.pdf"
return Response( return Response(
content=pdf_bytes, content=pdf_bytes,
media_type="application/pdf", media_type="application/pdf",

View File

@@ -17,20 +17,3 @@ def render(templates: Jinja2Templates, template_name: str,
response = templates.TemplateResponse(template_name, ctx) response = templates.TemplateResponse(template_name, ctx)
response.status_code = status_code response.status_code = status_code
return response return response
def render_xml(templates: Jinja2Templates, template_name: str,
request: Request, invoice: "Invoice",
filename: str = "factur-x.xml") -> Response:
xml_content = templates.env.get_template(template_name).render(
request=request,
invoice=invoice,
seller=invoice.seller,
buyer=invoice.buyer,
)
return Response(
content=xml_content,
media_type="application/xml",
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
)

View File

@@ -29,7 +29,6 @@
<td> <td>
<a href="/factures/{{ f.id }}" class="btn btn-sm">Voir</a> <a href="/factures/{{ f.id }}" class="btn btn-sm">Voir</a>
<a href="/factures/{{ f.id }}/pdf" class="btn btn-sm btn-primary">PDF</a> <a href="/factures/{{ f.id }}/pdf" class="btn btn-sm btn-primary">PDF</a>
<a href="/factures/{{ f.id }}/facturx" class="btn btn-sm btn-primary">facturx</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -4,10 +4,22 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
@page { @page {
size: A4; size: A4;
margin: 1.5cm 1.8cm; margin: 1.5cm 1.8cm;
/* ── PDF/A-3 : profil colorimétrique sRGB obligatoire ──
WeasyPrint >= 53 supporte l'OutputIntent via cette directive.
Téléchargez sRGB_v4_ICC_preference.icc depuis ICC et placez-le
dans static/icc/ ou utilisez le chemin absolu. */
@pdf-output-intent url('/static/icc/sRGB_v4_ICC_preference.icc');
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
} }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { body {
font-family: 'DejaVu Sans', Arial, sans-serif; font-family: 'DejaVu Sans', Arial, sans-serif;
font-size: 10pt; font-size: 10pt;
@@ -26,8 +38,11 @@
color: #2c3e50; color: #2c3e50;
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
.emetteur p { font-size: 9pt; color: #555; line-height: 1.6; } .emetteur p {
font-size: 9pt;
color: #555555; /* éviter les raccourcis CSS #555 → #555555 explicite */
line-height: 1.6;
}
.facture-titre { .facture-titre {
text-align: right; text-align: right;
} }
@@ -40,12 +55,12 @@
.facture-titre .numero { .facture-titre .numero {
font-size: 13pt; font-size: 13pt;
font-weight: bold; font-weight: bold;
color: #e74c3c; color: #c0392b; /* #e74c3c remplacé par équivalent moins saturé → moins de gamut issues */
margin-top: 0.2em; margin-top: 0.2em;
} }
.facture-titre .dates { .facture-titre .dates {
font-size: 9pt; font-size: 9pt;
color: #555; color: #555555;
margin-top: 0.5em; margin-top: 0.5em;
line-height: 1.8; line-height: 1.8;
} }
@@ -66,7 +81,7 @@
font-size: 8pt; font-size: 8pt;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
color: #888; color: #888888;
margin-bottom: 0.7em; margin-bottom: 0.7em;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
padding-bottom: 0.4em; padding-bottom: 0.4em;
@@ -128,7 +143,7 @@
.tva-mention { .tva-mention {
font-size: 8.5pt; font-size: 8.5pt;
color: #666; color: #666666;
font-style: italic; font-style: italic;
text-align: right; text-align: right;
margin-bottom: 1.5em; margin-bottom: 1.5em;
@@ -138,7 +153,7 @@
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
padding-top: 1em; padding-top: 1em;
font-size: 8.5pt; font-size: 8.5pt;
color: #555; color: #555555;
line-height: 1.7; line-height: 1.7;
} }
.footer-info .conditions { .footer-info .conditions {
@@ -146,7 +161,7 @@
} }
.footer-info .penalites { .footer-info .penalites {
font-size: 8pt; font-size: 8pt;
color: #888; color: #888888;
} }
.footer-info .iban { .footer-info .iban {
margin-top: 0.5em; margin-top: 0.5em;
@@ -155,7 +170,7 @@
.devis-ref { .devis-ref {
font-size: 8.5pt; font-size: 8.5pt;
color: #888; color: #888888;
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
@@ -165,7 +180,7 @@
padding: 0.7em 1em; padding: 0.7em 1em;
font-size: 9pt; font-size: 9pt;
margin-bottom: 1.5em; margin-bottom: 1.5em;
color: #444; color: #444444;
} }
</style> </style>
</head> </head>