generayion du XML a partir d'un template
This commit is contained in:
@@ -1,345 +0,0 @@
|
|||||||
"""
|
|
||||||
Usage
|
|
||||||
pip install lxml
|
|
||||||
python generate_facturx_basic.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
Générateur de XML Factur-X - Profil BASIC
|
|
||||||
Conforme à la norme EN 16931, syntaxe CII (Cross Industry Invoice)
|
|
||||||
Factur-X 1.0 / ZUGFeRD 2.x
|
|
||||||
"""
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
from datetime import date
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Namespaces CII obligatoires
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
NSMAP = {
|
|
||||||
"rsm": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
|
|
||||||
"ram": "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
|
|
||||||
"udt": "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100",
|
|
||||||
"qdt": "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
|
|
||||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
||||||
}
|
|
||||||
RSM = NSMAP["rsm"]
|
|
||||||
RAM = NSMAP["ram"]
|
|
||||||
UDT = NSMAP["udt"]
|
|
||||||
|
|
||||||
PROFILE_BASIC = "urn:factur-x.eu:1p0:basic"
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Structures de données
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
@dataclass
|
|
||||||
class Address:
|
|
||||||
line1: str
|
|
||||||
city: str
|
|
||||||
postcode: str
|
|
||||||
country_code: str # ISO 3166-1 alpha-2, ex: "FR"
|
|
||||||
line2: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Party:
|
|
||||||
name: str
|
|
||||||
vat_id: Optional[str] = None # ex: "FR12345678901"
|
|
||||||
siret: Optional[str] = None # schemeID="0002"
|
|
||||||
address: Optional[Address] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TaxLine:
|
|
||||||
base_amount: float
|
|
||||||
rate: float # ex: 20.0 pour 20%
|
|
||||||
amount: float
|
|
||||||
category: str = "E" # S=standard, Z=zéro, E=exonéré, AE=autoliquidation
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InvoiceLine:
|
|
||||||
line_id: str
|
|
||||||
description: str
|
|
||||||
quantity: float
|
|
||||||
unit_code: str # ex: "C62"=unité, "HUR"=heure, "KGM"=kg
|
|
||||||
unit_price: float # prix HT unitaire
|
|
||||||
vat_rate: float # ex: 20.0
|
|
||||||
vat_category: str = "S"
|
|
||||||
# Le montant HT de la ligne est calculé automatiquement
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Invoice:
|
|
||||||
# En-tête
|
|
||||||
number: str
|
|
||||||
issue_date: date
|
|
||||||
due_date: Optional[date]
|
|
||||||
currency: str # ex: "EUR"
|
|
||||||
type_code: str = "380" # 380=facture, 381=avoir, 384=facture rectificative
|
|
||||||
|
|
||||||
# Parties
|
|
||||||
seller: Party = None
|
|
||||||
buyer: Party = None
|
|
||||||
|
|
||||||
# Lignes
|
|
||||||
lines: List[InvoiceLine] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Paiement
|
|
||||||
payment_means_code: str = "30" # 30=virement
|
|
||||||
iban: Optional[str] = None
|
|
||||||
bic: Optional[str] = None
|
|
||||||
payment_reference: Optional[str] = None
|
|
||||||
|
|
||||||
# Note (champ libre)
|
|
||||||
note: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Helpers XML
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
def sub(parent, ns, tag, text=None, **attribs):
|
|
||||||
el = etree.SubElement(parent, f"{{{ns}}}{tag}", **attribs)
|
|
||||||
if text is not None:
|
|
||||||
el.text = str(text)
|
|
||||||
return el
|
|
||||||
|
|
||||||
|
|
||||||
def date_str(d: date) -> str:
|
|
||||||
return d.strftime("%Y%m%d")
|
|
||||||
|
|
||||||
|
|
||||||
def amount_str(v: float) -> str:
|
|
||||||
return f"{v:.2f}"
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Construction du XML
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
def build_facturx(inv: Invoice) -> etree._ElementTree:
|
|
||||||
|
|
||||||
root = etree.Element(f"{{{RSM}}}CrossIndustryInvoice", nsmap=NSMAP)
|
|
||||||
|
|
||||||
# ── 1. ExchangedDocumentContext ──────────────
|
|
||||||
ctx = sub(root, RSM, "ExchangedDocumentContext")
|
|
||||||
gp = sub(ctx, RAM, "GuidelineSpecifiedDocumentContextParameter")
|
|
||||||
sub(gp, RAM, "ID", PROFILE_BASIC)
|
|
||||||
|
|
||||||
# ── 2. ExchangedDocument ────────────────────
|
|
||||||
doc = sub(root, RSM, "ExchangedDocument")
|
|
||||||
sub(doc, RAM, "ID", inv.number)
|
|
||||||
sub(doc, RAM, "TypeCode", inv.type_code)
|
|
||||||
idt = sub(doc, RAM, "IssueDateTime")
|
|
||||||
sub(idt, UDT, "DateTimeString", date_str(inv.issue_date), format="102")
|
|
||||||
if inv.note:
|
|
||||||
sub(doc, RAM, "IncludedNote").append(
|
|
||||||
etree.fromstring(f'<ram:Content xmlns:ram="{RAM}">{inv.note}</ram:Content>')
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── 3. SupplyChainTradeTransaction ──────────
|
|
||||||
tx = sub(root, RSM, "SupplyChainTradeTransaction")
|
|
||||||
|
|
||||||
# ── 3a. Lignes de facture (BASIC = lignes obligatoires) ──
|
|
||||||
for ln in inv.lines:
|
|
||||||
net = round(ln.quantity * ln.unit_price, 2)
|
|
||||||
ili = sub(tx, RAM, "IncludedSupplyChainTradeLineItem")
|
|
||||||
assoc = sub(ili, RAM, "AssociatedDocumentLineDocument")
|
|
||||||
sub(assoc, RAM, "LineID", ln.line_id)
|
|
||||||
|
|
||||||
prod = sub(ili, RAM, "SpecifiedTradeProduct")
|
|
||||||
sub(prod, RAM, "Name", ln.description)
|
|
||||||
|
|
||||||
la = sub(ili, RAM, "SpecifiedLineTradeAgreement")
|
|
||||||
gpp = sub(la, RAM, "GrossPriceProductTradePrice")
|
|
||||||
sub(gpp, RAM, "ChargeAmount", amount_str(ln.unit_price), currencyID=inv.currency)
|
|
||||||
npp = sub(la, RAM, "NetPriceProductTradePrice")
|
|
||||||
sub(npp, RAM, "ChargeAmount", amount_str(ln.unit_price), currencyID=inv.currency)
|
|
||||||
|
|
||||||
ld = sub(ili, RAM, "SpecifiedLineTradeDelivery")
|
|
||||||
sub(ld, RAM, "BilledQuantity", amount_str(ln.quantity), unitCode=ln.unit_code)
|
|
||||||
|
|
||||||
ls = sub(ili, RAM, "SpecifiedLineTradeSettlement")
|
|
||||||
tax = sub(ls, RAM, "ApplicableTradeTax")
|
|
||||||
sub(tax, RAM, "TypeCode", "VAT")
|
|
||||||
sub(tax, RAM, "CategoryCode", ln.vat_category)
|
|
||||||
sub(tax, RAM, "RateApplicablePercent", amount_str(ln.vat_rate))
|
|
||||||
|
|
||||||
smls = sub(ls, RAM, "SpecifiedTradeSettlementLineMonetarySummation")
|
|
||||||
sub(smls, RAM, "LineTotalAmount", amount_str(net), currencyID=inv.currency)
|
|
||||||
|
|
||||||
# ── 3b. ApplicableHeaderTradeAgreement ──────
|
|
||||||
agr = sub(tx, RAM, "ApplicableHeaderTradeAgreement")
|
|
||||||
|
|
||||||
def _party(parent_el, tag, party: Party):
|
|
||||||
p = sub(parent_el, RAM, tag)
|
|
||||||
sub(p, RAM, "Name", party.name)
|
|
||||||
if party.siret:
|
|
||||||
sr = sub(p, RAM, "SpecifiedLegalOrganization")
|
|
||||||
sub(sr, RAM, "ID", party.siret, schemeID="0002")
|
|
||||||
if party.vat_id:
|
|
||||||
tr = sub(p, RAM, "SpecifiedTaxRegistration")
|
|
||||||
sub(tr, RAM, "ID", party.vat_id, schemeID="VA")
|
|
||||||
if party.address:
|
|
||||||
addr = sub(p, RAM, "PostalTradeAddress")
|
|
||||||
sub(addr, RAM, "PostcodeCode", party.address.postcode)
|
|
||||||
sub(addr, RAM, "LineOne", party.address.line1)
|
|
||||||
if party.address.line2:
|
|
||||||
sub(addr, RAM, "LineTwo", party.address.line2)
|
|
||||||
sub(addr, RAM, "CityName", party.address.city)
|
|
||||||
sub(addr, RAM, "CountryID", party.address.country_code)
|
|
||||||
return p
|
|
||||||
|
|
||||||
_party(agr, "SellerTradeParty", inv.seller)
|
|
||||||
_party(agr, "BuyerTradeParty", inv.buyer)
|
|
||||||
|
|
||||||
if inv.payment_reference:
|
|
||||||
bor = sub(agr, RAM, "BuyerOrderReferencedDocument")
|
|
||||||
sub(bor, RAM, "IssuerAssignedID", inv.payment_reference)
|
|
||||||
|
|
||||||
# ── 3c. ApplicableHeaderTradeDelivery ───────
|
|
||||||
sub(tx, RAM, "ApplicableHeaderTradeDelivery")
|
|
||||||
|
|
||||||
# ── 3d. ApplicableHeaderTradeSettlement ─────
|
|
||||||
stl = sub(tx, RAM, "ApplicableHeaderTradeSettlement")
|
|
||||||
sub(stl, RAM, "InvoiceCurrencyCode", inv.currency)
|
|
||||||
|
|
||||||
# Moyens de paiement
|
|
||||||
pm = sub(stl, RAM, "SpecifiedTradeSettlementPaymentMeans")
|
|
||||||
sub(pm, RAM, "TypeCode", inv.payment_means_code)
|
|
||||||
if inv.iban:
|
|
||||||
acc = sub(pm, RAM, "PayeePartyCreditorFinancialAccount")
|
|
||||||
sub(acc, RAM, "IBANID", inv.iban)
|
|
||||||
if inv.bic:
|
|
||||||
fi = sub(pm, RAM, "PayeeSpecifiedCreditorFinancialInstitution")
|
|
||||||
sub(fi, RAM, "BICID", inv.bic)
|
|
||||||
|
|
||||||
# TVA par taux (calcul à partir des lignes)
|
|
||||||
tax_totals: dict[tuple, list] = {}
|
|
||||||
for ln in inv.lines:
|
|
||||||
key = (ln.vat_rate, ln.vat_category)
|
|
||||||
tax_totals.setdefault(key, []).append(round(ln.quantity * ln.unit_price, 2))
|
|
||||||
|
|
||||||
total_ht = 0.0
|
|
||||||
total_tva = 0.0
|
|
||||||
for (rate, cat), bases in tax_totals.items():
|
|
||||||
base = round(sum(bases), 2)
|
|
||||||
tva = round(base * rate / 100, 2)
|
|
||||||
total_ht += base
|
|
||||||
total_tva += tva
|
|
||||||
tax = sub(stl, RAM, "ApplicableTradeTax")
|
|
||||||
sub(tax, RAM, "CalculatedAmount", amount_str(tva), currencyID=inv.currency)
|
|
||||||
sub(tax, RAM, "TypeCode", "VAT")
|
|
||||||
sub(tax, RAM, "BasisAmount", amount_str(base), currencyID=inv.currency)
|
|
||||||
sub(tax, RAM, "CategoryCode", cat)
|
|
||||||
sub(tax, RAM, "RateApplicablePercent", amount_str(rate))
|
|
||||||
|
|
||||||
total_ht = round(total_ht, 2)
|
|
||||||
total_tva = round(total_tva, 2)
|
|
||||||
total_ttc = round(total_ht + total_tva, 2)
|
|
||||||
|
|
||||||
# Échéance
|
|
||||||
if inv.due_date:
|
|
||||||
stp = sub(stl, RAM, "SpecifiedTradePaymentTerms")
|
|
||||||
ddt = sub(stp, RAM, "DueDateDateTime")
|
|
||||||
sub(ddt, UDT, "DateTimeString", date_str(inv.due_date), format="102")
|
|
||||||
|
|
||||||
# Totaux
|
|
||||||
sums = sub(stl, RAM, "SpecifiedTradeSettlementHeaderMonetarySummation")
|
|
||||||
sub(sums, RAM, "LineTotalAmount", amount_str(total_ht), currencyID=inv.currency)
|
|
||||||
sub(sums, RAM, "TaxBasisTotalAmount",amount_str(total_ht), currencyID=inv.currency)
|
|
||||||
sub(sums, RAM, "TaxTotalAmount", amount_str(total_tva), currencyID=inv.currency)
|
|
||||||
sub(sums, RAM, "GrandTotalAmount", amount_str(total_ttc), currencyID=inv.currency)
|
|
||||||
sub(sums, RAM, "DuePayableAmount", amount_str(total_ttc), currencyID=inv.currency)
|
|
||||||
|
|
||||||
return etree.ElementTree(root)
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Exemple d'utilisation
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
invoice = Invoice(
|
|
||||||
number="FA-2024-00123",
|
|
||||||
issue_date=date(2024, 3, 15),
|
|
||||||
due_date=date(2024, 4, 14),
|
|
||||||
currency="EUR",
|
|
||||||
type_code="380",
|
|
||||||
note="Facture relative au contrat n°CTR-2024-01",
|
|
||||||
|
|
||||||
seller=Party(
|
|
||||||
name="Ma Société SAS",
|
|
||||||
vat_id="FR12345678901",
|
|
||||||
siret="12345678900012",
|
|
||||||
address=Address(
|
|
||||||
line1="10 rue de la Paix",
|
|
||||||
city="Paris",
|
|
||||||
postcode="75001",
|
|
||||||
country_code="FR",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
buyer=Party(
|
|
||||||
name="Client SARL",
|
|
||||||
vat_id="FR98765432100",
|
|
||||||
siret="98765432100011",
|
|
||||||
address=Address(
|
|
||||||
line1="5 avenue des Fleurs",
|
|
||||||
city="Lyon",
|
|
||||||
postcode="69001",
|
|
||||||
country_code="FR",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
lines=[
|
|
||||||
InvoiceLine(
|
|
||||||
line_id="1",
|
|
||||||
description="Développement logiciel - Sprint 1",
|
|
||||||
quantity=10,
|
|
||||||
unit_code="HUR", # heures
|
|
||||||
unit_price=150.00,
|
|
||||||
vat_rate=0.0,
|
|
||||||
),
|
|
||||||
InvoiceLine(
|
|
||||||
line_id="2",
|
|
||||||
description="Licence logicielle annuelle",
|
|
||||||
quantity=1,
|
|
||||||
unit_code="C62", # unité
|
|
||||||
unit_price=500.00,
|
|
||||||
vat_rate=0.0,
|
|
||||||
),
|
|
||||||
InvoiceLine(
|
|
||||||
line_id="3",
|
|
||||||
description="Formation utilisateurs",
|
|
||||||
quantity=2,
|
|
||||||
unit_code="HUR",
|
|
||||||
unit_price=200.00,
|
|
||||||
vat_rate=0.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
payment_means_code="30", # 30 = virement bancaire
|
|
||||||
iban="FR7612345678901234567890189",
|
|
||||||
bic="BNPAFRPPXXX",
|
|
||||||
payment_reference="CTR-2024-01",
|
|
||||||
)
|
|
||||||
|
|
||||||
tree = build_facturx(invoice)
|
|
||||||
output_file = "factur-x.xml"
|
|
||||||
tree.write(output_file, xml_declaration=True, encoding="UTF-8", pretty_print=True)
|
|
||||||
|
|
||||||
print(f"✅ Fichier généré : {output_file}")
|
|
||||||
print(f" Facture : {invoice.number}")
|
|
||||||
print(f" Émetteur : {invoice.seller.name}")
|
|
||||||
print(f" Destinataire : {invoice.buyer.name}")
|
|
||||||
|
|
||||||
# Calcul récapitulatif
|
|
||||||
total_ht = sum(round(l.quantity * l.unit_price, 2) for l in invoice.lines)
|
|
||||||
total_tva = round(total_ht * 0.20, 2)
|
|
||||||
print(f" Total HT : {total_ht:.2f} €")
|
|
||||||
print(f" TVA 20% : {total_tva:.2f} €")
|
|
||||||
print(f" Total TTC : {total_ht + total_tva:.2f} €")
|
|
||||||
@@ -10,8 +10,8 @@ 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
|
from template_helper import render, render_xml
|
||||||
from generate_facturx_basic import Address, Party, Invoice, InvoiceLine, build_facturx
|
from generate_facturx_jinja2 import Address, Party, Invoice, InvoiceLine, generate_facturx_xml
|
||||||
|
|
||||||
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")
|
||||||
@@ -156,9 +156,29 @@ def changer_statut_facture(
|
|||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse(f"/factures/{facture_id}", status_code=303)
|
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}/pdf")
|
@router.get("/{facture_id}/facturx")
|
||||||
def telecharger_pdf(facture_id: int, db: Session = Depends(get_db)):
|
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
|
from weasyprint import HTML
|
||||||
facture = db.query(Facture).get(facture_id)
|
facture = db.query(Facture).get(facture_id)
|
||||||
if not facture:
|
if not facture:
|
||||||
@@ -173,10 +193,6 @@ def telecharger_pdf(facture_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
filename = f"facture-{facture.numero}.pdf"
|
filename = f"facture-{facture.numero}.pdf"
|
||||||
|
|
||||||
tree = build_facturx(generate_xml (facture_id, db))
|
|
||||||
output_file = "factur-x.xml"
|
|
||||||
tree.write(output_file, xml_declaration=True, encoding="UTF-8", pretty_print=True)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
content=pdf_bytes,
|
content=pdf_bytes,
|
||||||
media_type="application/pdf",
|
media_type="application/pdf",
|
||||||
@@ -194,7 +210,7 @@ def apercu_pdf(request: Request, facture_id: int, db: Session = Depends(get_db))
|
|||||||
"settings": settings,
|
"settings": settings,
|
||||||
})
|
})
|
||||||
|
|
||||||
def generate_xml(facture_id: int, db: Session):
|
def get_invoice_data(facture_id: int, db: Session):
|
||||||
facture = db.query(Facture).get(facture_id)
|
facture = db.query(Facture).get(facture_id)
|
||||||
invoice = Invoice(
|
invoice = Invoice(
|
||||||
number=facture.numero,
|
number=facture.numero,
|
||||||
@@ -228,11 +244,8 @@ def generate_xml(facture_id: int, db: Session):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
payment_means_code="30", # 30 = virement bancaire
|
payment_means_code="30", # 30 = virement bancaire
|
||||||
iban=settings.asso_iban,
|
iban=settings.asso_iban,
|
||||||
bic=settings.asso_bic,
|
|
||||||
payment_reference=facture.devis_origine.numero,
|
payment_reference=facture.devis_origine.numero,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,6 +256,7 @@ def generate_xml(facture_id: int, db: Session):
|
|||||||
quantity=l.quantite,
|
quantity=l.quantite,
|
||||||
unit_code="C62",
|
unit_code="C62",
|
||||||
unit_price=l.prix_unitaire_ht,
|
unit_price=l.prix_unitaire_ht,
|
||||||
|
vat_category="E",
|
||||||
vat_rate=0.0,
|
vat_rate=0.0,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Helper pour injecter automatiquement current_user dans le contexte Jinja2.
|
|||||||
"""
|
"""
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse, Response
|
||||||
|
|
||||||
|
|
||||||
def render(templates: Jinja2Templates, template_name: str,
|
def render(templates: Jinja2Templates, template_name: str,
|
||||||
@@ -17,3 +17,20 @@ 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}"'}
|
||||||
|
)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user