Files
erp-nutzfahrzeuge/backend/app/utils/contract_pdf.py
T

262 lines
8.7 KiB
Python
Raw Normal View History

"""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 = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; font-size: 11pt; margin: 2cm; }}
h1 {{ text-align: center; font-size: 16pt; margin-bottom: 20px; }}
h2 {{ font-size: 13pt; border-bottom: 1px solid #333; padding-bottom: 3px; }}
table {{ width: 100%; border-collapse: collapse; margin: 10px 0; }}
td {{ padding: 4px 8px; vertical-align: top; }}
td.label {{ font-weight: bold; width: 35%; }}
.section {{ margin-bottom: 20px; }}
.gwg-clause {{
background-color: #fff3cd;
border: 1px solid #ffc107;
padding: 10px;
margin: 15px 0;
border-radius: 4px;
}}
.gwg-clause h3 {{ margin-top: 0; color: #856404; }}
.signature-block {{ margin-top: 40px; }}
.signature-line {{ border-top: 1px solid #333; width: 45%; margin-top: 50px; display: inline-block; text-align: center; font-size: 9pt; }}
.signature-spacer {{ width: 10%; display: inline-block; }}
.footer {{ margin-top: 30px; font-size: 9pt; color: #666; border-top: 1px solid #ccc; padding-top: 10px; }}
</style>
</head>
<body>
<h1>Kaufvertrag über ein Nutzfahrzeug</h1>
<div class="section">
<h2>1. Vertragsparteien</h2>
<table>
<tr><td class="label">Verkäufer:</td><td>{seller_name}<br>{seller_address}<br>{seller_country}</td></tr>
<tr><td class="label">USt-IdNr. (Verkäufer):</td><td>{seller_vat_id}</td></tr>
<tr><td class="label">Käufer:</td><td>{buyer_name}<br>{buyer_address}<br>{buyer_country}</td></tr>
<tr><td class="label">USt-IdNr. (Käufer):</td><td>{buyer_vat_id}</td></tr>
</table>
</div>
<div class="section">
<h2>2. Vertragsgegenstand</h2>
<table>
<tr><td class="label">Fahrzeug:</td><td>{vehicle_make} {vehicle_model}</td></tr>
<tr><td class="label">Fahrgestellnummer (FIN):</td><td>{vehicle_fin}</td></tr>
<tr><td class="label">Fahrzeugtyp:</td><td>{vehicle_type}</td></tr>
<tr><td class="label">Erstzulassung:</td><td>{first_registration}</td></tr>
<tr><td class="label">Kilometerstand:</td><td>{mileage_km} km</td></tr>
<tr><td class="label">Leistung:</td><td>{power_kw} kW ({power_hp} PS)</td></tr>
<tr><td class="label">Kraftstoff:</td><td>{fuel_type}</td></tr>
</table>
</div>
<div class="section">
<h2>3. Kaufpreis</h2>
<table>
<tr><td class="label">Kaufpreis:</td><td>{sale_price} EUR</td></tr>
<tr><td class="label">Verkaufsdatum:</td><td>{sale_date}</td></tr>
</table>
</div>
{gwg_section}
<div class="section">
<h2>4. Zahlungsbedingungen</h2>
<p>Der Kaufpreis ist bei Übergabe des Fahrzeugs fällig. Die Zahlung erfolgt per
Überweisung auf das Konto des Verkäufers.</p>
</div>
<div class="section">
<h2>5. Gewährleistung</h2>
<p>Das Fahrzeug wird unter Ausschluss der Sachmängelhaftung verkauft, soweit
nicht ausdrücklich etwas anderes vereinbart wurde.</p>
</div>
<div class="section">
<h2>6. Übergabe</h2>
<p>Das Fahrzeug wird am {sale_date} übergeben. Mit der Übergabe geht die
Gefahr auf den Käufer über.</p>
</div>
<div class="signature-block">
<div class="signature-line">Verkäufer</div>
<div class="signature-spacer"></div>
<div class="signature-line">Käufer</div>
</div>
<div class="footer">
Vertrag erstellt am {created_date} | Vertragsnummer: {contract_number}
</div>
</body>
</html>"""
_GWG_CLAUSE_HTML = """
<div class="gwg-clause">
<h3>Geringwertige Wirtschaftsgüter (GwG) § 6 Abs. 2 EStG</h3>
<p>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.</p>
</div>
"""
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 "<br>".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)