"""Contract PDF generation using WeasyPrint.
Generates a Verkaufvertrag (sales contract) PDF from an HTML template
with vehicle data, contact data, GwG-Klausel, and USt-IdNr. field.
"""
import asyncio
import os
from datetime import date
from decimal import Decimal
from typing import Any
from app.config import settings
# HTML template for the sales contract
_CONTRACT_HTML_TEMPLATE = """
Kaufvertrag über ein Nutzfahrzeug
1. Vertragsparteien
| Verkäufer: | {seller_name} {seller_address} {seller_country} |
| USt-IdNr. (Verkäufer): | {seller_vat_id} |
| Käufer: | {buyer_name} {buyer_address} {buyer_country} |
| USt-IdNr. (Käufer): | {buyer_vat_id} |
2. Vertragsgegenstand
| Fahrzeug: | {vehicle_make} {vehicle_model} |
| Fahrgestellnummer (FIN): | {vehicle_fin} |
| Fahrzeugtyp: | {vehicle_type} |
| Erstzulassung: | {first_registration} |
| Kilometerstand: | {mileage_km} km |
| Leistung: | {power_kw} kW ({power_hp} PS) |
| Kraftstoff: | {fuel_type} |
3. Kaufpreis
| Kaufpreis: | {sale_price} EUR |
| Verkaufsdatum: | {sale_date} |
{gwg_section}
4. Zahlungsbedingungen
Der Kaufpreis ist bei Übergabe des Fahrzeugs fällig. Die Zahlung erfolgt per
Überweisung auf das Konto des Verkäufers.
5. Gewährleistung
Das Fahrzeug wird unter Ausschluss der Sachmängelhaftung verkauft, soweit
nicht ausdrücklich etwas anderes vereinbart wurde.
6. Übergabe
Das Fahrzeug wird am {sale_date} übergeben. Mit der Übergabe geht die
Gefahr auf den Käufer über.
"""
_GWG_CLAUSE_HTML = """
Geringwertige Wirtschaftsgüter (GwG) – § 6 Abs. 2 EStG
Der Kaufpreis beträgt maximal 800 EUR. Nach § 6 Abs. 2 EStG kann der
Verkäufer den vollständigen Betrag im Jahr der Anschaffung als
Sofortabzug im Betriebsvermögen geltend machen. Die
Abschreibung erfolgt somit sofort und vollständig in der
Anschaffungsperiode.
"""
def _format_contact_name(contact: Any) -> str:
"""Format contact name for the contract."""
if contact is None:
return "N/A"
return getattr(contact, "company_name", "N/A") or "N/A"
def _format_contact_address(contact: Any) -> str:
"""Format contact address for the contract."""
if contact is None:
return "N/A"
parts = []
street = getattr(contact, "address_street", None)
zip_code = getattr(contact, "address_zip", None)
city = getattr(contact, "address_city", None)
if street:
parts.append(street)
if zip_code and city:
parts.append(f"{zip_code} {city}")
elif city:
parts.append(city)
elif zip_code:
parts.append(zip_code)
return "
".join(parts) if parts else "N/A"
def _format_contact_country(contact: Any) -> str:
"""Format contact country for the contract."""
if contact is None:
return "N/A"
return getattr(contact, "address_country", "DE") or "DE"
def _format_vat_id(contact: Any) -> str:
"""Format VAT ID for the contract."""
if contact is None:
return "Nicht angegeben"
vat_id = getattr(contact, "vat_id", None)
return vat_id if vat_id else "Nicht angegeben"
def _format_price(price: Decimal) -> str:
"""Format price for the contract."""
if price is None:
return "0,00"
formatted = f"{price:,.2f}"
return formatted.replace(",", "X").replace(".", ",").replace("X", ".")
def _format_date(d: date) -> str:
"""Format date in German convention DD.MM.YYYY."""
if d is None:
return "N/A"
return d.strftime("%d.%m.%Y")
def build_contract_html(sale: Any) -> str:
"""Build the HTML contract from a Sale model instance.
Args:
sale: Sale model instance with nested vehicle, buyer, seller.
Returns:
HTML string for the contract.
"""
vehicle = getattr(sale, "vehicle", None)
buyer = getattr(sale, "buyer", None)
seller = getattr(sale, "seller", None)
# Determine GwG clause
gwg_section = ""
if sale.is_gwg and sale.sale_price is not None and sale.sale_price <= Decimal("800"):
gwg_section = _GWG_CLAUSE_HTML
html = _CONTRACT_HTML_TEMPLATE.format(
seller_name=_format_contact_name(seller) if seller else "N/A",
seller_address=_format_contact_address(seller) if seller else "N/A",
seller_country=_format_contact_country(seller) if seller else "DE",
seller_vat_id=_format_vat_id(seller) if seller else "Nicht angegeben",
buyer_name=_format_contact_name(buyer),
buyer_address=_format_contact_address(buyer),
buyer_country=_format_contact_country(buyer),
buyer_vat_id=_format_vat_id(buyer),
vehicle_make=getattr(vehicle, "make", "N/A") if vehicle else "N/A",
vehicle_model=getattr(vehicle, "model", "N/A") if vehicle else "N/A",
vehicle_fin=getattr(vehicle, "fin", "N/A") if vehicle else "N/A",
vehicle_type=getattr(vehicle, "vehicle_type", "N/A") if vehicle else "N/A",
first_registration=_format_date(getattr(vehicle, "first_registration", None)) if vehicle else "N/A",
mileage_km=getattr(vehicle, "mileage_km", "N/A") if vehicle else "N/A",
power_kw=getattr(vehicle, "power_kw", "N/A") if vehicle else "N/A",
power_hp=getattr(vehicle, "power_hp", "N/A") if vehicle else "N/A",
fuel_type=getattr(vehicle, "fuel_type", "N/A") if vehicle else "N/A",
sale_price=_format_price(sale.sale_price),
sale_date=_format_date(sale.sale_date),
gwg_section=gwg_section,
created_date=_format_date(date.today()),
contract_number=str(sale.id)[:8].upper(),
)
return html
def generate_contract_pdf_sync(sale: Any, output_dir: str = "/tmp/contracts") -> str:
"""Generate contract PDF synchronously using WeasyPrint.
Args:
sale: Sale model instance with nested vehicle, buyer, seller.
output_dir: Directory to save the PDF.
Returns:
Path to the generated PDF file.
"""
from weasyprint import HTML
os.makedirs(output_dir, exist_ok=True)
html_content = build_contract_html(sale)
pdf_path = os.path.join(output_dir, f"contract_{sale.id}.pdf")
HTML(string=html_content).write_pdf(pdf_path)
return pdf_path
async def generate_contract_pdf(sale: Any, output_dir: str = "/tmp/contracts") -> str:
"""Generate contract PDF asynchronously (runs WeasyPrint in a thread).
WeasyPrint is synchronous, so we offload it to a thread to avoid
blocking the event loop.
Args:
sale: Sale model instance with nested vehicle, buyer, seller.
output_dir: Directory to save the PDF.
Returns:
Path to the generated PDF file.
"""
return await asyncio.to_thread(generate_contract_pdf_sync, sale, output_dir)