forked from seb_vallee/BillManager
Compare commits
5 Commits
40c05f7860
...
87e78314c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 87e78314c6 | |||
| dbb5a29863 | |||
| f25cf02660 | |||
| bb12b3777f | |||
| b403efd056 |
@@ -1,6 +1,6 @@
|
||||
# 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).
|
||||
|
||||
## Fonctionnalités
|
||||
@@ -9,7 +9,7 @@ non assujetties à la TVA (art. 293B du CGI).
|
||||
- Devis avec numérotation automatique (DEV-AAAA-XXXX)
|
||||
- Factures avec numérotation chronologique (AAAA-XXXX)
|
||||
- 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)
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -8,3 +8,5 @@ pydantic==2.9.2
|
||||
pydantic-settings==2.5.2
|
||||
bcrypt==4.2.0
|
||||
itsdangerous==2.2.0
|
||||
factur-x==3.16
|
||||
pikepdf==10.5.0
|
||||
|
||||
@@ -10,8 +10,9 @@ 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 template_helper import render
|
||||
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)])
|
||||
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
|
||||
|
||||
@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)
|
||||
@router.get("/{facture_id}/pdf")
|
||||
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)
|
||||
if not facture:
|
||||
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({
|
||||
"facture": facture,
|
||||
"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"
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
|
||||
@@ -17,20 +17,3 @@ def render(templates: Jinja2Templates, template_name: str,
|
||||
response = templates.TemplateResponse(template_name, ctx)
|
||||
response.status_code = status_code
|
||||
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}"'}
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<td>
|
||||
<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 }}/facturx" class="btn btn-sm btn-primary">facturx</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -4,10 +4,22 @@
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {
|
||||
|
||||
size: A4;
|
||||
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 {
|
||||
font-family: 'DejaVu Sans', Arial, sans-serif;
|
||||
font-size: 10pt;
|
||||
@@ -26,8 +38,11 @@
|
||||
color: #2c3e50;
|
||||
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 {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -40,12 +55,12 @@
|
||||
.facture-titre .numero {
|
||||
font-size: 13pt;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
color: #c0392b; /* #e74c3c remplacé par équivalent moins saturé → moins de gamut issues */
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.facture-titre .dates {
|
||||
font-size: 9pt;
|
||||
color: #555;
|
||||
color: #555555;
|
||||
margin-top: 0.5em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
@@ -66,7 +81,7 @@
|
||||
font-size: 8pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #888;
|
||||
color: #888888;
|
||||
margin-bottom: 0.7em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.4em;
|
||||
@@ -128,7 +143,7 @@
|
||||
|
||||
.tva-mention {
|
||||
font-size: 8.5pt;
|
||||
color: #666;
|
||||
color: #666666;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
margin-bottom: 1.5em;
|
||||
@@ -138,7 +153,7 @@
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 1em;
|
||||
font-size: 8.5pt;
|
||||
color: #555;
|
||||
color: #555555;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.footer-info .conditions {
|
||||
@@ -146,7 +161,7 @@
|
||||
}
|
||||
.footer-info .penalites {
|
||||
font-size: 8pt;
|
||||
color: #888;
|
||||
color: #888888;
|
||||
}
|
||||
.footer-info .iban {
|
||||
margin-top: 0.5em;
|
||||
@@ -155,7 +170,7 @@
|
||||
|
||||
.devis-ref {
|
||||
font-size: 8.5pt;
|
||||
color: #888;
|
||||
color: #888888;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
@@ -165,7 +180,7 @@
|
||||
padding: 0.7em 1em;
|
||||
font-size: 9pt;
|
||||
margin-bottom: 1.5em;
|
||||
color: #444;
|
||||
color: #444444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user