""" C-Stone Invoice Check — Invoice Router Rechnungs-Verarbeitung und Speicherung """ import os import fitz # PyMuPDF import re from datetime import datetime from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from sqlalchemy.orm import Session from openpyxl import Workbook from openpyxl.styles import Font, Alignment, Border, Side from backend.database import get_db from backend.models import Invoice, InvoiceDetail, EmailLog router = APIRouter() # Ablagepfad (Evo-X2) INVOICE_STORAGE_PATH = os.getenv("INVOICE_STORAGE_PATH", "/Volumes/Evo-X2/Rechnungen") # Steuer-Schlüsselwörter TAX_KEYWORDS = { "mit_mwst": ["mwst", "mehrwertsteuer", "steuer", "incl. mwst", "zzgl. mwst"], "ohne_mwst": ["zzgl. steuer", "exkl. steuer", "steuerfrei", "steuerbefreit"], "umsatzsteuer": ["umsatzsteuer", "ust", "ust-id"], "vat": ["vat", "v.a.t.", "value added tax"], } def detect_tax_type(text: str) -> Dict[str, Any]: """Erkennt Steuer-Art aus PDF-Text""" text_lower = text.lower() # Prüfen auf "mit MwSt" if any(kw in text_lower for kw in TAX_KEYWORDS["mit_mwst"]): # Prüfen auf Satz: z.B. "inkl. 19 % MwSt" match = re.search(r'(?:inkl|zzgl)[\s\-]*(?:der)?[\s\-]*(\d+)[\s\-]*%[\s\-]*(?:mwst|steuer)', text_lower) if match: tax_rate = float(match.group(1)) / 100 return { "tax_type": "mit MwSt", "tax_rate": tax_rate, "confidence": 0.9, "found_keyword": match.group(0) } # Prüfen auf "ohne MwSt" if any(kw in text_lower for kw in TAX_KEYWORDS["ohne_mwst"]): return { "tax_type": "ohne MwSt", "tax_rate": 0.0, "confidence": 0.85, "found_keyword": next(kw for kw in TAX_KEYWORDS["ohne_mwst"] if kw in text_lower) } # Prüfen auf Umsatzsteuer if any(kw in text_lower for kw in TAX_KEYWORDS["umsatzsteuer"]): return { "tax_type": "Umsatzsteuer", "tax_rate": 0.19, # Standard "confidence": 0.7, "found_keyword": next(kw for kw in TAX_KEYWORDS["umsatzsteuer"] if kw in text_lower) } # Prüfen auf VAT (europäische Steuer) if any(kw in text_lower for kw in TAX_KEYWORDS["vat"]): return { "tax_type": "VAT", "tax_rate": 0.19, # Standard EU "confidence": 0.7, "found_keyword": next(kw for kw in TAX_KEYWORDS["vat"] if kw in text_lower) } # Standard: Prüfen auf Steuerangabe im Text tax_matches = re.findall(r'(\d+)[\s\-]*%[\s\-]*(?:steuer|mwst|ust|vat)', text_lower) if tax_matches: tax_rate = float(tax_matches[0]) / 100 return { "tax_type": "mit Steuer", "tax_rate": tax_rate, "confidence": 0.6, "found_keyword": f"{tax_matches[0]}%" } # Keine Steuer erkannt return { "tax_type": "unbekannt", "tax_rate": None, "confidence": 0.3, "found_keyword": None } def extract_amount(text: str) -> Optional[float]: """Extrahiert Betrag aus PDF-Text""" # Muster: "Summe: 123,45 €" oder "Total: 123.45 EUR" patterns = [ r'(?:summe|total|betrag)[\s:]*([\d\.]+[\s\,]\d{2})\s*€?', r'([\d\.]+[\s\,]\d{2})\s*(?:€|EUR|EUR\*)', r'(?:zu\s+bezahlen|gesamt)[\s:]*([\d\.]+[\s\,]\d{2})', ] for pattern in patterns: match = re.search(pattern, text.lower()) if match: amount_str = match.group(1) # Komma zu Punkt für float amount_str = amount_str.replace('.', '').replace(',', '.') try: return float(amount_str) except ValueError: continue return None def extract_invoice_number(text: str) -> Optional[str]: """Extrahiert Rechnungsnummer aus PDF-Text""" patterns = [ r'(?:rechnungs)?nr\.?\s*[:\s]*([A-Za-z0-9\-/]+)', r'(?:invoice|rechnung)?[\s_]*(?:no\.?|#)?\s*[:\s]*([A-Za-z0-9\-/]+)', r'(?:rechnung|invoice)\s*(\d{6,})', ] for pattern in patterns: match = re.search(pattern, text.lower()) if match: return match.group(1).strip() return None def extract_issuer(text: str) -> Optional[str]: """Extrahiert Rechnungsausteller aus PDF-Text""" patterns = [ r'(?:von|von\s+haus|ausgestellt\s+vom|issuer)[:\s]*\n?([A-Z][A-Za-z\s]+(?:GmbH|AG|UG|KG|e\.K\.|GmbH&Co\.KG))', r'(?:seit|seit\s+dem|seit\s+vom)[\s\S]{0,100}?(?:rechnung|invoice)', r'^(.+?)[\n\r]{1,2}(?:rechnung|invoice|rechnungsnummer)', ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: return match.group(1).strip() return None def extract_pdf_data(file_path: str) -> Dict[str, Any]: """Extrahiert alle relevanten Daten aus PDF-Datei""" try: doc = fitz.open(file_path) text = "" for page in doc: text += page.get_text() doc.close() return { "text": text, "invoice_number": extract_invoice_number(text), "amount": extract_amount(text), "issuer": extract_issuer(text), "tax_info": detect_tax_type(text), "pages": len(doc) if 'doc' in dir() else 0 } except Exception as e: return { "error": str(e), "pages": 0 } def get_storage_path(year: int, tax_type: str) -> str: """Erstellt Ablagepfad auf Evo-X2""" base_path = INVOICE_STORAGE_PATH # Steuer-Verzeichnis if tax_type and "mit" in tax_type.lower(): tax_dir = "mit MwSt" elif tax_type and "ohne" in tax_type.lower(): tax_dir = "ohne MwSt" else: tax_dir = "ohne MwSt" # Default return os.path.join(base_path, str(year), tax_dir) def save_to_excel(invoices: List[Invoice], excel_path: str): """Speichert Rechnungen in Excel-Datei""" wb = Workbook() ws = wb.active ws.title = "Rechnungen" # Header headers = ["Datum", "Rechnungsausteller", "Betrag", "Umsatzsteuer", "Steuerart", "Dateipfad"] for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.font = Font(bold=True) # Daten for row, inv in enumerate(invoices, 2): ws.cell(row=row, column=1, value=inv.invoice_date.strftime("%Y-%m-%d") if inv.invoice_date else "") ws.cell(row=row, column=2, value=inv.issuer_name or "") ws.cell(row=row, column=3, value=inv.invoice_amount or 0) ws.cell(row=row, column=4, value=inv.invoice_tax_amount or 0) ws.cell(row=row, column=5, value=inv.invoice_tax_type or "") ws.cell(row=row, column=6, value=inv.file_path or "") # Formatieren for col in range(1, 7): ws.column_dimensions[chr(64 + col)].width = 20 wb.save(excel_path) @router.get("/scan-pdf") async def scan_pdf_file(file_path: str, db: Session = Depends(get_db)): """Scannt einzelne PDF-Datei""" if not os.path.exists(file_path): raise HTTPException(status_code=404, detail="Datei nicht gefunden") data = extract_pdf_data(file_path) if "error" in data: return {"error": data["error"]} # Speichern in DB invoice = Invoice( file_path=file_path, file_name=os.path.basename(file_path), invoice_number=data.get("invoice_number"), invoice_amount=data.get("amount"), issuer_name=data.get("issuer"), invoice_tax_type=data.get("tax_info", {}).get("tax_type"), invoice_tax_rate=data.get("tax_info", {}).get("tax_rate"), confidence_score=data.get("tax_info", {}).get("confidence", 0), processed=True ) db.add(invoice) db.commit() # Details speichern detail = InvoiceDetail( invoice_id=invoice.id, raw_text=data.get("text", ""), parsed_data={ "invoice_number": data.get("invoice_number"), "amount": data.get("amount"), "issuer": data.get("issuer"), "tax_info": data.get("tax_info") } ) db.add(detail) db.commit() return { "id": invoice.id, "invoice_number": data.get("invoice_number"), "amount": data.get("amount"), "issuer": data.get("issuer"), "tax_type": data.get("tax_info", {}).get("tax_type"), "tax_rate": data.get("tax_info", {}).get("tax_rate"), "pages": data.get("pages", 0) } @router.get("/export-excel") async def export_to_excel(db: Session = Depends(get_db)): """Exportiert alle Rechnungen nach Excel""" invoices = db.query(Invoice).order_by(Invoice.invoice_date.desc()).all() excel_path = os.path.join(INVOICE_STORAGE_PATH, f"rechnungen_{datetime.utcnow().strftime('%Y%m%d')}.xlsx") save_to_excel(invoices, excel_path) return { "status": "success", "file_path": excel_path, "invoice_count": len(invoices) } @router.get("/list") async def list_invoices(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): """Listet alle Rechnungen""" invoices = db.query(Invoice).offset(skip).limit(limit).all() return { "invoices": [ { "id": inv.id, "invoice_number": inv.invoice_number, "invoice_date": inv.invoice_date.isoformat() if inv.invoice_date else None, "issuer_name": inv.issuer_name, "invoice_amount": inv.invoice_amount, "invoice_tax_type": inv.invoice_tax_type, "file_path": inv.file_path, "processed": inv.processed } for inv in invoices ], "total": db.query(Invoice).count() }