"""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 # 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.

Verkäufer
Käufer
""" _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)