forked from seb_vallee/BillManager
Compare commits
11 Commits
master
...
87e78314c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 87e78314c6 | |||
| dbb5a29863 | |||
| f25cf02660 | |||
| bb12b3777f | |||
| b403efd056 | |||
| 40c05f7860 | |||
| bc82603f96 | |||
| b1029d10d3 | |||
| 8e79796781 | |||
| 8ed1df83ec | |||
| 2838a87413 |
@@ -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
|
||||||
|
|||||||
225
generate_facturx_jinja2.py
Normal file
225
generate_facturx_jinja2.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
"""
|
||||||
|
Générateur Factur-X BASIC via template Jinja2
|
||||||
|
"""
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import date
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Structures de données (identiques au générateur lxml)
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
@dataclass
|
||||||
|
class Address:
|
||||||
|
line1: str
|
||||||
|
city: str
|
||||||
|
postcode: str
|
||||||
|
country_code: str
|
||||||
|
line2: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Party:
|
||||||
|
name: str
|
||||||
|
vat_id: Optional[str] = None
|
||||||
|
siret: Optional[str] = None
|
||||||
|
address: Optional[Address] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InvoiceLine:
|
||||||
|
line_id: str
|
||||||
|
description: str
|
||||||
|
quantity: float
|
||||||
|
unit_code: str
|
||||||
|
unit_price: float
|
||||||
|
vat_rate: float
|
||||||
|
vat_category: str = "S"
|
||||||
|
|
||||||
|
# Raison d'exonération par catégorie TVA
|
||||||
|
EXEMPTION_REASONS = {
|
||||||
|
"E": "TVA non applicable, art. 293 B du CGI",
|
||||||
|
"Z": "Taux zéro",
|
||||||
|
"AE": "Autoliquidation",
|
||||||
|
"K": "Exonération intracommunautaire",
|
||||||
|
"G": "Exportation hors UE",
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaxLine:
|
||||||
|
rate: float
|
||||||
|
category: str
|
||||||
|
base_amount: float
|
||||||
|
amount: float
|
||||||
|
exemption_reason: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Invoice:
|
||||||
|
number: str
|
||||||
|
issue_date: date
|
||||||
|
currency: str
|
||||||
|
seller: Party
|
||||||
|
buyer: Party
|
||||||
|
lines: List[InvoiceLine] = field(default_factory=list)
|
||||||
|
due_date: Optional[date] = None
|
||||||
|
type_code: str = "380"
|
||||||
|
payment_means_code: str = "30"
|
||||||
|
iban: Optional[str] = None
|
||||||
|
payment_reference: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
# Calculés automatiquement (voir compute())
|
||||||
|
tax_lines: List[TaxLine] = field(default_factory=list)
|
||||||
|
total_ht: float = 0.0
|
||||||
|
total_tva: float = 0.0
|
||||||
|
total_ttc: float = 0.0
|
||||||
|
|
||||||
|
def compute(self):
|
||||||
|
"""Calcule les totaux et les lignes de TVA depuis les lignes de facture."""
|
||||||
|
tax_groups = defaultdict(float)
|
||||||
|
self.total_ht = 0.0
|
||||||
|
|
||||||
|
for ln in self.lines:
|
||||||
|
net = round(ln.quantity * ln.unit_price, 2)
|
||||||
|
self.total_ht += net
|
||||||
|
tax_groups[(ln.vat_rate, ln.vat_category)] += net
|
||||||
|
|
||||||
|
self.total_ht = round(self.total_ht, 2)
|
||||||
|
self.tax_lines = []
|
||||||
|
self.total_tva = 0.0
|
||||||
|
|
||||||
|
for (rate, category), base in tax_groups.items():
|
||||||
|
base = round(base, 2)
|
||||||
|
amount = round(base * rate / 100, 2)
|
||||||
|
self.total_tva += amount
|
||||||
|
self.tax_lines.append(TaxLine(
|
||||||
|
rate=rate,
|
||||||
|
category=category,
|
||||||
|
base_amount=base,
|
||||||
|
amount=amount,
|
||||||
|
# Raison d'exonération automatique si catégorie != "S" (standard)
|
||||||
|
exemption_reason=EXEMPTION_REASONS.get(category) if category != "S" else None,
|
||||||
|
))
|
||||||
|
|
||||||
|
self.total_tva = round(self.total_tva, 2)
|
||||||
|
self.total_ttc = round(self.total_ht + self.total_tva, 2)
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Filtres Jinja2 personnalisés
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def filter_amount(value: float) -> str:
|
||||||
|
return f"{float(value):.2f}"
|
||||||
|
|
||||||
|
|
||||||
|
def filter_datefmt(value: date) -> str:
|
||||||
|
return value.strftime("%Y%m%d")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Générateur
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
def generate_facturx_xml(invoice: Invoice, template_dir: str = ".", output_file: str = "factur-x.xml") -> str:
|
||||||
|
"""
|
||||||
|
Génère le XML Factur-X BASIC depuis le template Jinja2.
|
||||||
|
Retourne le XML sous forme de chaîne et l'écrit dans output_file.
|
||||||
|
"""
|
||||||
|
# Calcul des totaux
|
||||||
|
invoice.compute()
|
||||||
|
|
||||||
|
# Environnement Jinja2
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(template_dir),
|
||||||
|
autoescape=False, # XML géré manuellement
|
||||||
|
trim_blocks=True, # supprime le \n après les blocs {% %}
|
||||||
|
lstrip_blocks=True, # supprime les espaces avant {% %}
|
||||||
|
)
|
||||||
|
env.filters["amount"] = filter_amount
|
||||||
|
env.filters["datefmt"] = filter_datefmt
|
||||||
|
|
||||||
|
template = env.get_template("templates/xml/factur-x-basic.xml.j2")
|
||||||
|
xml_str = template.render(
|
||||||
|
invoice=invoice,
|
||||||
|
seller=invoice.seller,
|
||||||
|
buyer=invoice.buyer,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(xml_str)
|
||||||
|
|
||||||
|
return xml_str
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 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 absent : pas de numéro TVA en franchise (art. 293 B CGI)
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
payment_means_code="30",
|
||||||
|
iban="FR7612345678901234567890189",
|
||||||
|
payment_reference="CTR-2024-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ajout des lignes — franchise de TVA (art. 293 B du CGI)
|
||||||
|
# Utiliser vat_category="E" et vat_rate=0.0
|
||||||
|
articles = [
|
||||||
|
{"desc": "Développement logiciel - Sprint 1", "qty": 10, "unit": "HUR", "prix": 150.0, "tva": 0.0, "cat": "E"},
|
||||||
|
{"desc": "Licence logicielle annuelle", "qty": 1, "unit": "C62", "prix": 500.0, "tva": 0.0, "cat": "E"},
|
||||||
|
{"desc": "Formation utilisateurs", "qty": 2, "unit": "HUR", "prix": 200.0, "tva": 0.0, "cat": "E"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, art in enumerate(articles, start=1):
|
||||||
|
invoice.lines.append(InvoiceLine(
|
||||||
|
line_id=str(i),
|
||||||
|
description=art["desc"],
|
||||||
|
quantity=art["qty"],
|
||||||
|
unit_code=art["unit"],
|
||||||
|
unit_price=art["prix"],
|
||||||
|
vat_rate=art["tva"],
|
||||||
|
vat_category=art["cat"], # "E" = exonéré (franchise TVA)
|
||||||
|
))
|
||||||
|
|
||||||
|
xml = generate_facturx_xml(invoice, template_dir=".", output_file="factur-x.xml")
|
||||||
|
|
||||||
|
print(f"✅ Fichier généré : factur-x.xml")
|
||||||
|
print(f" Total HT : {invoice.total_ht:.2f} €")
|
||||||
|
print(f" TVA 20% : {invoice.total_tva:.2f} €")
|
||||||
|
print(f" Total TTC : {invoice.total_ttc:.2f} €")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from fastapi import APIRouter, Depends, Request, Form, HTTPException
|
from fastapi import APIRouter, Depends, Request, Form, HTTPException
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ from models import Devis, LigneDevis, Client, StatutDevis
|
|||||||
from numerotation import generer_numero_devis
|
from numerotation import generer_numero_devis
|
||||||
from auth import get_current_user
|
from auth import get_current_user
|
||||||
from template_helper import render
|
from template_helper import render
|
||||||
|
from config import settings
|
||||||
|
|
||||||
router = APIRouter(prefix="/devis", tags=["devis"], dependencies=[Depends(get_current_user)])
|
router = APIRouter(prefix="/devis", tags=["devis"], dependencies=[Depends(get_current_user)])
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
@@ -150,6 +151,27 @@ def changer_statut_devis(
|
|||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse(f"/devis/{devis_id}", status_code=303)
|
return RedirectResponse(f"/devis/{devis_id}", status_code=303)
|
||||||
|
|
||||||
|
@router.get("/{devis_id}/pdf")
|
||||||
|
def telecharger_devis__pdf(devis_id: int, db: Session = Depends(get_db)):
|
||||||
|
from weasyprint import HTML
|
||||||
|
devis = db.query(Devis).get(devis_id)
|
||||||
|
if not devis:
|
||||||
|
raise HTTPException(status_code=404)
|
||||||
|
|
||||||
|
html_content = templates.get_template("pdf/devis.html").render({
|
||||||
|
"devis": devis,
|
||||||
|
"settings": settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
pdf_bytes = HTML(string=html_content, base_url=".").write_pdf()
|
||||||
|
|
||||||
|
filename = f"devis-{devis.numero}.pdf"
|
||||||
|
return Response(
|
||||||
|
content=pdf_bytes,
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{devis_id}/convertir")
|
@router.post("/{devis_id}/convertir")
|
||||||
def convertir_en_facture(devis_id: int, db: Session = Depends(get_db)):
|
def convertir_en_facture(devis_id: int, db: Session = Depends(get_db)):
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ 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
|
||||||
|
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")
|
||||||
@@ -155,20 +157,64 @@ 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}/pdf")
|
||||||
def telecharger_pdf(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(
|
||||||
@@ -187,3 +233,55 @@ def apercu_pdf(request: Request, facture_id: int, db: Session = Depends(get_db))
|
|||||||
"facture": facture,
|
"facture": facture,
|
||||||
"settings": settings,
|
"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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<h1>Devis {{ devis.numero }}</h1>
|
<h1>Devis {{ devis.numero }}</h1>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="/devis/" class="btn">← Retour</a>
|
<a href="/devis/" class="btn">← Retour</a>
|
||||||
|
<a href="/devis/{{ devis.id }}/pdf" class="btn btn-primary">⬇ Télécharger PDF</a>
|
||||||
{% if devis.statut.value == 'brouillon' %}
|
{% if devis.statut.value == 'brouillon' %}
|
||||||
<a href="/devis/{{ devis.id }}/modifier" class="btn">Modifier</a>
|
<a href="/devis/{{ devis.id }}/modifier" class="btn">Modifier</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<td><span class="badge badge-{{ d.statut.value }}">{{ d.statut.value }}</span></td>
|
<td><span class="badge badge-{{ d.statut.value }}">{{ d.statut.value }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/devis/{{ d.id }}" class="btn btn-sm">Voir</a>
|
<a href="/devis/{{ d.id }}" class="btn btn-sm">Voir</a>
|
||||||
|
<a href="/devis/{{ d.id }}/pdf" class="btn btn-sm btn-primary">PDF</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
277
templates/pdf/devis.html
Normal file
277
templates/pdf/devis.html
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1.5cm 1.8cm;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'DejaVu Sans', Arial, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
border-bottom: 2px solid #2c3e50;
|
||||||
|
}
|
||||||
|
.emetteur h1 {
|
||||||
|
font-size: 16pt;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.emetteur p { font-size: 9pt; color: #555; line-height: 1.6; }
|
||||||
|
|
||||||
|
.facture-titre {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.facture-titre h2 {
|
||||||
|
font-size: 22pt;
|
||||||
|
color: #2c3e50;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.facture-titre .numero {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
.facture-titre .dates {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #555;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parties {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
gap: 2em;
|
||||||
|
}
|
||||||
|
.partie {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.partie h3 {
|
||||||
|
font-size: 8pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.7em;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
.partie p { font-size: 9.5pt; line-height: 1.7; }
|
||||||
|
.partie .nom { font-weight: bold; font-size: 11pt; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
thead tr {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
thead th {
|
||||||
|
padding: 0.6em 0.8em;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: normal;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
thead th:last-child { text-align: right; }
|
||||||
|
tbody tr:nth-child(even) { background: #f8f9fa; }
|
||||||
|
tbody td {
|
||||||
|
padding: 0.6em 0.8em;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
tbody td:last-child { text-align: right; font-weight: 500; }
|
||||||
|
td.qte { text-align: center; }
|
||||||
|
td.pu { text-align: right; }
|
||||||
|
|
||||||
|
.totaux {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
.totaux table {
|
||||||
|
width: auto;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
.totaux td {
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
font-size: 10pt;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.totaux tr.total-ht td {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
border-top: 2px solid #2c3e50;
|
||||||
|
padding-top: 0.6em;
|
||||||
|
}
|
||||||
|
.totaux td:last-child { text-align: right; }
|
||||||
|
|
||||||
|
.tva-mention {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding-top: 1em;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.footer-info .conditions {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.footer-info .penalites {
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.footer-info .iban {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devis-ref {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 3px solid #2c3e50;
|
||||||
|
padding: 0.7em 1em;
|
||||||
|
font-size: 9pt;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- EN-TÊTE -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="emetteur">
|
||||||
|
<h1>{{ settings.asso_nom }}</h1>
|
||||||
|
<p>
|
||||||
|
{{ settings.asso_adresse }}<br>
|
||||||
|
{{ settings.asso_code_postal }} {{ settings.asso_ville }}<br>
|
||||||
|
{% if settings.asso_email %}{{ settings.asso_email }}<br>{% endif %}
|
||||||
|
{% if settings.asso_telephone %}{{ settings.asso_telephone }}<br>{% endif %}
|
||||||
|
{% if settings.asso_rna %}RNA : {{ settings.asso_rna }}<br>{% endif %}
|
||||||
|
{% if settings.asso_siret %}SIRET : {{ settings.asso_siret }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="facture-titre">
|
||||||
|
<h2>Devis</h2>
|
||||||
|
<div class="numero">N° {{ devis.numero }}</div>
|
||||||
|
<div class="dates">
|
||||||
|
Date d'émission : {{ devis.date_emission.strftime('%d/%m/%Y') }}<br>
|
||||||
|
Date d'échéance : {{ devis.date_validite.strftime('%d/%m/%Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PARTIES -->
|
||||||
|
<div class="parties">
|
||||||
|
<div class="partie">
|
||||||
|
<h3>Émetteur</h3>
|
||||||
|
<p>
|
||||||
|
<span class="nom">{{ settings.asso_nom }}</span><br>
|
||||||
|
{{ settings.asso_adresse }}<br>
|
||||||
|
{{ settings.asso_code_postal }} {{ settings.asso_ville }}<br>
|
||||||
|
{% if settings.asso_rna %}RNA : {{ settings.asso_rna }}<br>{% endif %}
|
||||||
|
{% if settings.asso_siret %}SIRET : {{ settings.asso_siret }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="partie">
|
||||||
|
<h3>Destinataire</h3>
|
||||||
|
<p>
|
||||||
|
<span class="nom">{{ devis.client.nom }}</span><br>
|
||||||
|
{{ devis.client.adresse }}<br>
|
||||||
|
{{ devis.client.code_postal }} {{ devis.client.ville }}
|
||||||
|
{% if devis.client.siret %}<br>SIRET : {{ devis.client.siret }}{% endif %}
|
||||||
|
{% if devis.client.email %}<br>{{ devis.client.email }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LIGNES -->
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:55%">Désignation</th>
|
||||||
|
<th style="width:10%" class="qte">Qté</th>
|
||||||
|
<th style="width:17%">Prix unitaire HT</th>
|
||||||
|
<th style="width:18%">Total HT</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for l in devis.lignes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ l.description }}</td>
|
||||||
|
<td class="qte">
|
||||||
|
{% if l.quantite == l.quantite | int %}{{ l.quantite | int }}{% else %}{{ l.quantite }}{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="pu">
|
||||||
|
{{ "%.2f"|format(l.prix_unitaire_ht) | replace('.', ',') }} €
|
||||||
|
</td>
|
||||||
|
<td>{{ "%.2f"|format(l.total_ht) | replace('.', ',') }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- TOTAUX -->
|
||||||
|
<div class="totaux">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Montant HT</td>
|
||||||
|
<td>{{ "%.2f"|format(devis.total_ht) | replace('.', ',') }} €</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>TVA</td>
|
||||||
|
<td>0,00 €</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="total-ht">
|
||||||
|
<td>Total TTC</td>
|
||||||
|
<td>{{ "%.2f"|format(devis.total_ht) | replace('.', ',') }} €</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="tva-mention">TVA non applicable — art. 293B du CGI</p>
|
||||||
|
|
||||||
|
<!-- PIED DE PAGE LÉGAL -->
|
||||||
|
<div class="footer-info">
|
||||||
|
{% if settings.asso_iban %}
|
||||||
|
<div class="iban">
|
||||||
|
Coordonnées bancaires — IBAN : {{ settings.asso_iban }}
|
||||||
|
{% if settings.asso_bic %} — BIC : {{ settings.asso_bic }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
|
|||||||
176
templates/xml/factur-x-basic.xml.j2
Normal file
176
templates/xml/factur-x-basic.xml.j2
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rsm:CrossIndustryInvoice
|
||||||
|
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
||||||
|
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
|
||||||
|
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
|
||||||
|
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
|
||||||
|
<!-- 1. CONTEXTE -->
|
||||||
|
<rsm:ExchangedDocumentContext>
|
||||||
|
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||||
|
<ram:ID>urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic</ram:ID>
|
||||||
|
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||||
|
</rsm:ExchangedDocumentContext>
|
||||||
|
|
||||||
|
<!-- 2. EN-TÊTE FACTURE -->
|
||||||
|
<rsm:ExchangedDocument>
|
||||||
|
<ram:ID>{{ invoice.number }}</ram:ID>
|
||||||
|
<ram:TypeCode>{{ invoice.type_code }}</ram:TypeCode>
|
||||||
|
<ram:IssueDateTime>
|
||||||
|
<udt:DateTimeString format="102">{{ invoice.issue_date | datefmt }}</udt:DateTimeString>
|
||||||
|
</ram:IssueDateTime>
|
||||||
|
{% if invoice.note %}
|
||||||
|
<ram:IncludedNote>
|
||||||
|
<ram:Content>{{ invoice.note }}</ram:Content>
|
||||||
|
</ram:IncludedNote>
|
||||||
|
{% endif %}
|
||||||
|
</rsm:ExchangedDocument>
|
||||||
|
|
||||||
|
<!-- 3. TRANSACTION COMMERCIALE -->
|
||||||
|
<rsm:SupplyChainTradeTransaction>
|
||||||
|
|
||||||
|
<!-- 3a. Lignes de facture -->
|
||||||
|
{% for line in invoice.lines %}
|
||||||
|
<ram:IncludedSupplyChainTradeLineItem>
|
||||||
|
<ram:AssociatedDocumentLineDocument>
|
||||||
|
<ram:LineID>{{ line.line_id }}</ram:LineID>
|
||||||
|
</ram:AssociatedDocumentLineDocument>
|
||||||
|
<ram:SpecifiedTradeProduct>
|
||||||
|
<ram:Name>{{ line.description }}</ram:Name>
|
||||||
|
</ram:SpecifiedTradeProduct>
|
||||||
|
<ram:SpecifiedLineTradeAgreement>
|
||||||
|
<ram:NetPriceProductTradePrice>
|
||||||
|
<ram:ChargeAmount>{{ line.unit_price | amount }}</ram:ChargeAmount>
|
||||||
|
</ram:NetPriceProductTradePrice>
|
||||||
|
</ram:SpecifiedLineTradeAgreement>
|
||||||
|
<ram:SpecifiedLineTradeDelivery>
|
||||||
|
<ram:BilledQuantity unitCode="{{ line.unit_code }}">{{ line.quantity | amount }}</ram:BilledQuantity>
|
||||||
|
</ram:SpecifiedLineTradeDelivery>
|
||||||
|
<ram:SpecifiedLineTradeSettlement>
|
||||||
|
<ram:ApplicableTradeTax>
|
||||||
|
<ram:TypeCode>VAT</ram:TypeCode>
|
||||||
|
<ram:CategoryCode>{{ line.vat_category }}</ram:CategoryCode>
|
||||||
|
<ram:RateApplicablePercent>{{ line.vat_rate | amount }}</ram:RateApplicablePercent>
|
||||||
|
</ram:ApplicableTradeTax>
|
||||||
|
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||||
|
<ram:LineTotalAmount>{{ (line.quantity * line.unit_price) | amount }}</ram:LineTotalAmount>
|
||||||
|
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||||
|
</ram:SpecifiedLineTradeSettlement>
|
||||||
|
</ram:IncludedSupplyChainTradeLineItem>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- 3b. Accord commercial -->
|
||||||
|
<ram:ApplicableHeaderTradeAgreement>
|
||||||
|
|
||||||
|
<ram:SellerTradeParty>
|
||||||
|
<ram:Name>{{ seller.name }}</ram:Name>
|
||||||
|
{% if seller.siret %}
|
||||||
|
<ram:SpecifiedLegalOrganization>
|
||||||
|
<ram:ID schemeID="0002">{{ seller.siret }}</ram:ID>
|
||||||
|
</ram:SpecifiedLegalOrganization>
|
||||||
|
{% endif %}
|
||||||
|
{% if seller.address %}
|
||||||
|
<ram:PostalTradeAddress>
|
||||||
|
<ram:PostcodeCode>{{ seller.address.postcode }}</ram:PostcodeCode>
|
||||||
|
<ram:LineOne>{{ seller.address.line1 }}</ram:LineOne>
|
||||||
|
{% if seller.address.line2 %}
|
||||||
|
<ram:LineTwo>{{ seller.address.line2 }}</ram:LineTwo>
|
||||||
|
{% endif %}
|
||||||
|
<ram:CityName>{{ seller.address.city }}</ram:CityName>
|
||||||
|
<ram:CountryID>{{ seller.address.country_code }}</ram:CountryID>
|
||||||
|
</ram:PostalTradeAddress>
|
||||||
|
{% endif %}
|
||||||
|
{% if seller.vat_id %}
|
||||||
|
<ram:SpecifiedTaxRegistration>
|
||||||
|
<ram:ID schemeID="VA">{{ seller.vat_id }}</ram:ID>
|
||||||
|
</ram:SpecifiedTaxRegistration>
|
||||||
|
{% elif seller.siret %}
|
||||||
|
{# Franchise TVA (art. 293 B CGI) : pas de n° TVA, on utilise le SIRET avec schemeID="FC" #}
|
||||||
|
<ram:SpecifiedTaxRegistration>
|
||||||
|
<ram:ID schemeID="FC">{{ seller.siret }}</ram:ID>
|
||||||
|
</ram:SpecifiedTaxRegistration>
|
||||||
|
{% endif %}
|
||||||
|
</ram:SellerTradeParty>
|
||||||
|
|
||||||
|
<ram:BuyerTradeParty>
|
||||||
|
<ram:Name>{{ buyer.name }}</ram:Name>
|
||||||
|
{% if buyer.siret %}
|
||||||
|
<ram:SpecifiedLegalOrganization>
|
||||||
|
<ram:ID schemeID="0002">{{ buyer.siret }}</ram:ID>
|
||||||
|
</ram:SpecifiedLegalOrganization>
|
||||||
|
{% endif %}
|
||||||
|
{% if buyer.address %}
|
||||||
|
<ram:PostalTradeAddress>
|
||||||
|
<ram:PostcodeCode>{{ buyer.address.postcode }}</ram:PostcodeCode>
|
||||||
|
<ram:LineOne>{{ buyer.address.line1 }}</ram:LineOne>
|
||||||
|
{% if buyer.address.line2 %}
|
||||||
|
<ram:LineTwo>{{ buyer.address.line2 }}</ram:LineTwo>
|
||||||
|
{% endif %}
|
||||||
|
<ram:CityName>{{ buyer.address.city }}</ram:CityName>
|
||||||
|
<ram:CountryID>{{ buyer.address.country_code }}</ram:CountryID>
|
||||||
|
</ram:PostalTradeAddress>
|
||||||
|
{% endif %}
|
||||||
|
</ram:BuyerTradeParty>
|
||||||
|
|
||||||
|
{% if invoice.payment_reference %}
|
||||||
|
<ram:BuyerOrderReferencedDocument>
|
||||||
|
<ram:IssuerAssignedID>{{ invoice.payment_reference }}</ram:IssuerAssignedID>
|
||||||
|
</ram:BuyerOrderReferencedDocument>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</ram:ApplicableHeaderTradeAgreement>
|
||||||
|
|
||||||
|
<!-- 3c. Livraison -->
|
||||||
|
<ram:ApplicableHeaderTradeDelivery>
|
||||||
|
<ram:ShipToTradeParty>
|
||||||
|
<ram:Name>{{ buyer.name }}</ram:Name>
|
||||||
|
</ram:ShipToTradeParty>
|
||||||
|
</ram:ApplicableHeaderTradeDelivery>
|
||||||
|
|
||||||
|
<!-- 3d. Règlement -->
|
||||||
|
<ram:ApplicableHeaderTradeSettlement>
|
||||||
|
<ram:InvoiceCurrencyCode>{{ invoice.currency }}</ram:InvoiceCurrencyCode>
|
||||||
|
|
||||||
|
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||||
|
<ram:TypeCode>{{ invoice.payment_means_code }}</ram:TypeCode>
|
||||||
|
{% if invoice.iban %}
|
||||||
|
<ram:PayeePartyCreditorFinancialAccount>
|
||||||
|
<ram:IBANID>{{ invoice.iban }}</ram:IBANID>
|
||||||
|
</ram:PayeePartyCreditorFinancialAccount>
|
||||||
|
{% endif %}
|
||||||
|
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||||
|
|
||||||
|
{% for tax in invoice.tax_lines %}
|
||||||
|
<ram:ApplicableTradeTax>
|
||||||
|
<ram:CalculatedAmount>{{ tax.amount | amount }}</ram:CalculatedAmount>
|
||||||
|
<ram:TypeCode>VAT</ram:TypeCode>
|
||||||
|
{% if tax.exemption_reason %}
|
||||||
|
<ram:ExemptionReason>{{ tax.exemption_reason }}</ram:ExemptionReason>
|
||||||
|
{% endif %}
|
||||||
|
<ram:BasisAmount>{{ tax.base_amount | amount }}</ram:BasisAmount>
|
||||||
|
<ram:CategoryCode>{{ tax.category }}</ram:CategoryCode>
|
||||||
|
<ram:RateApplicablePercent>{{ tax.rate | amount }}</ram:RateApplicablePercent>
|
||||||
|
</ram:ApplicableTradeTax>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if invoice.due_date %}
|
||||||
|
<ram:SpecifiedTradePaymentTerms>
|
||||||
|
<ram:DueDateDateTime>
|
||||||
|
<udt:DateTimeString format="102">{{ invoice.due_date | datefmt }}</udt:DateTimeString>
|
||||||
|
</ram:DueDateDateTime>
|
||||||
|
</ram:SpecifiedTradePaymentTerms>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||||
|
<ram:LineTotalAmount>{{ invoice.total_ht | amount }}</ram:LineTotalAmount>
|
||||||
|
<ram:TaxBasisTotalAmount>{{ invoice.total_ht | amount }}</ram:TaxBasisTotalAmount>
|
||||||
|
<ram:TaxTotalAmount currencyID="{{ invoice.currency }}">{{ invoice.total_tva | amount }}</ram:TaxTotalAmount>
|
||||||
|
<ram:GrandTotalAmount>{{ invoice.total_ttc | amount }}</ram:GrandTotalAmount>
|
||||||
|
<ram:DuePayableAmount>{{ invoice.total_ttc | amount }}</ram:DuePayableAmount>
|
||||||
|
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||||
|
|
||||||
|
</ram:ApplicableHeaderTradeSettlement>
|
||||||
|
|
||||||
|
</rsm:SupplyChainTradeTransaction>
|
||||||
|
</rsm:CrossIndustryInvoice>
|
||||||
Reference in New Issue
Block a user