Przeglądaj źródła

Docuware Rechnung

gc-server3 2 dni temu
rodzic
commit
86d66f23c7

+ 95 - 317
app/docuware_search.py

@@ -1,335 +1,113 @@
-"""
-DocuWare: gueltige Suchfilter ermitteln und Dokumente abfragen.
-
-Voraussetzungen:
-  pip install requests
-
-Konfiguration per Umgebungsvariablen:
-  DOCUWARE_SERVER_URL     z.B. https://your-system.docuware.cloud
-  DOCUWARE_USERNAME       DocuWare Benutzername
-  DOCUWARE_PASSWORD       DocuWare Passwort
-  DOCUWARE_FILE_CABINET   optional: Name des File Cabinets
-  DOCUWARE_ORG_ID         optional: OrgId, falls bei On-Prem/Enterprise nötig
-
-Beispiele:
-  python docuware_search.py --list-filters
-  python docuware_search.py --list-documents --count 10 --fields DOCUMENT_TYPE,COMPANY_NAME
-  python docuware_search.py --search DOCUMENT_TYPE "Invoice" --count 10
-  python docuware_search.py --search DWSTOREDATETIME 2024-01-01 2024-12-31 --count 20
-"""
-
-import argparse
 import json
 import os
-import sys
-from typing import Any
 
-import requests
+import docuware
 from dotenv import load_dotenv
 
 load_dotenv()
 
 
-PLATFORM = "DocuWare/Platform"
-CLIENT_ID = "docuware.platform.net.client"
-SCOPE = "docuware.platform"
-
-
-class DocuWareClient:
-    def __init__(self, server_url: str, username: str, password: str, org_id: str | None = None) -> None:
-        self.server_url = server_url.rstrip("/")
-        self.username = username
-        self.password = password
-        self.org_id = org_id
-        self.session = requests.Session()
-        self.session.headers.update({"Accept": "application/json"})
-        self.access_token: str | None = None
-
-    def _url(self, path: str) -> str:
-        return f"{self.server_url}/{PLATFORM}/{path.lstrip('/')}"
-
-    def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
-        response = self.session.request(method, url, timeout=60, **kwargs)
-        try:
-            response.raise_for_status()
-        except requests.HTTPError as exc:
-            body = response.text[:2000]
-            raise RuntimeError(f"{method} {url} fehlgeschlagen: HTTP {response.status_code}\n{body}") from exc
-        return response
-
-    def authenticate_password_grant(self) -> None:
-        """
-        Entspricht in der Postman-Collection:
-        1. Home/IdentityServiceInfo
-        2. /.well-known/openid-configuration
-        3.a Request Token w/ Username & Password
-        """
-        identity_info = self._request(
-            "GET",
-            self._url("Home/IdentityServiceInfo"),
-        ).json()
-
-        identity_service_url = identity_info["IdentityServiceUrl"].rstrip("/")
-        openid_config = self._request(
-            "GET",
-            f"{identity_service_url}/.well-known/openid-configuration",
-        ).json()
-
-        token_endpoint = openid_config["token_endpoint"]
-        token_response = self._request(
-            "POST",
-            token_endpoint,
-            headers={"Accept": "application/json"},
-            data={
-                "grant_type": "password",
-                "scope": SCOPE,
-                "client_id": CLIENT_ID,
-                "username": self.username,
-                "password": self.password,
-            },
-        ).json()
-
-        self.access_token = token_response["access_token"]
-        self.session.headers.update({"Authorization": f"Bearer {self.access_token}"})
-
-    def get_file_cabinets(self) -> list[dict[str, Any]]:
-        params = {}
-        if self.org_id:
-            params["OrgId"] = self.org_id
-        data = self._request("GET", self._url("FileCabinets"), params=params).json()
-        return data.get("FileCabinet", [])
-
-    def select_file_cabinet(self, file_cabinet_name: str | None = None) -> dict[str, Any]:
-        cabinets = self.get_file_cabinets()
-        if not cabinets:
-            raise RuntimeError("Keine File Cabinets gefunden oder keine Berechtigung.")
-
-        if file_cabinet_name:
-            for cabinet in cabinets:
-                if cabinet.get("Name") == file_cabinet_name:
-                    return cabinet
-            names = ", ".join(c.get("Name", "<ohne Name>") for c in cabinets)
-            raise RuntimeError(f"File Cabinet '{file_cabinet_name}' nicht gefunden. Verfuegbar: {names}")
-
-        return cabinets[0]
-
-    def get_dialogs(self, file_cabinet_id: str, dialog_type: str | None = None) -> list[dict[str, Any]]:
-        params = {}
-        if dialog_type:
-            params["DialogType"] = dialog_type
-        data = self._request(
-            "GET",
-            self._url(f"FileCabinets/{file_cabinet_id}/Dialogs"),
-            params=params,
-        ).json()
-        return data.get("Dialog", [])
-
-    def select_search_dialog(self, file_cabinet_id: str) -> dict[str, Any]:
-        dialogs = self.get_dialogs(file_cabinet_id, dialog_type="Search")
-        if not dialogs:
-            # Fallback: alle Dialoge laden und nach Type suchen.
-            dialogs = [d for d in self.get_dialogs(file_cabinet_id) if d.get("Type") == "Search"]
-
-        if not dialogs:
-            raise RuntimeError("Kein Search-Dialog gefunden.")
-
-        for dialog in dialogs:
-            if dialog.get("IsDefault") is True:
-                return dialog
-
-        return dialogs[0]
-
-    def get_dialog_details(self, file_cabinet_id: str, search_dialog_id: str) -> dict[str, Any]:
-        return self._request(
-            "GET",
-            self._url(f"FileCabinets/{file_cabinet_id}/Dialogs/{search_dialog_id}"),
-        ).json()
-
-    def get_valid_search_filters(self, file_cabinet_id: str, search_dialog_id: str) -> list[dict[str, Any]]:
-        """
-        Die gueltigen Suchfilter sind die Felder des Search-Dialogs.
-        Fuer Abfragen wird typischerweise DBFieldName als Condition.DBName verwendet.
-        """
-        details = self.get_dialog_details(file_cabinet_id, search_dialog_id)
-        fields = details.get("Fields", [])
-
-        result = []
-        for field in fields:
-            if field.get("Visible", True):
-                result.append(
-                    {
-                        "db_name": field.get("DBFieldName"),
-                        "label": field.get("DlgLabel"),
-                        "type": field.get("DWFieldType"),
-                        "read_only": field.get("ReadOnly"),
-                        "not_empty": field.get("NotEmpty"),
-                        "allow_extended_search": field.get("AllowExtendedSearch"),
-                        "length": field.get("Length"),
-                        "precision": field.get("Precision"),
-                    }
-                )
-        return result
-
-    def list_documents(
-        self,
-        file_cabinet_id: str,
-        count: int = 10,
-        fields: list[str] | None = None,
-    ) -> dict[str, Any]:
-        """
-        Einfache Dokumentliste ohne Suchbedingung.
-        """
-        params: dict[str, Any] = {"Count": count}
-        if fields:
-            params["Fields"] = ",".join(fields)
-
-        return self._request(
-            "GET",
-            self._url(f"FileCabinets/{file_cabinet_id}/Documents"),
-            params=params,
-        ).json()
-
-    def search_documents(
-        self,
-        file_cabinet_id: str,
-        search_dialog_id: str,
-        conditions: list[dict[str, Any]],
-        operation: str = "And",
-        count: int = 10,
-        start: int = 0,
-        result_fields: list[str] | None = None,
-        sort_field: str | None = None,
-        sort_direction: str = "Asc",
-    ) -> dict[str, Any]:
-        """
-        Suche per DialogExpression.
-
-        conditions Beispiel:
-          [{"DBName": "DOCUMENT_TYPE", "Value": ["Invoice"]}]
-          [{"DBName": "DWSTOREDATETIME", "Value": ["2024-01-01", "2024-12-31"]}]
-
-        Mehrere Werte innerhalb einer Condition werden von DocuWare als OR interpretiert.
-        Mehrere Conditions werden ueber Operation ("And" / "Or") kombiniert.
-        """
-        body: dict[str, Any] = {
-            "Condition": conditions,
-            "Operation": operation,
-            "Start": start,
-            "Count": count,
-            "ForceRefresh": True,
-            "IncludeSuggestions": False,
+FILE_CABINETS = {
+    "1": "Belegpool Reisacher",
+    "2": "Belegpool Reisacher",
+    "3": "Belegpool Reisacher Electric Mobility",
+    "4": "Belegpool Reisacher SEK",
+}
+
+
+def main():
+    dw = docuware.connect()
+
+    for org in dw.organizations:
+        print(org)
+        for fc in org.file_cabinets:
+            print("   ", fc)
+        for b in org.baskets:
+            print("   ", b)
+        # dlg = fc.search_dialog()
+        # for field in dlg.fields.values():
+        #     print("Id    =", field.id)
+        #     print("Length=", field.length)
+        #     print("Name  =", field.name)
+        #     print("Type  =", field.type)
+        #     print("-------")
+
+
+def get_file_cabinet(client_db: str) -> docuware.FileCabinet:
+    dw = docuware.connect()
+    return dw.organization("1").file_cabinet(FILE_CABINETS[client_db])
+
+
+def search_files(
+    client_db: str, customer_no: str | None = None, vehicle_no: str | None = None, document_no: str | None = None
+) -> list[list[str]]:
+    search_condition = {}
+    if customer_no:
+        search_condition["KUNDENNUMMER"] = customer_no
+    if vehicle_no:
+        search_condition["FAHRGESTELLNUMMER"] = vehicle_no
+    if document_no:
+        search_condition["BELEGNUMMER"] = document_no
+
+    fc = get_file_cabinet(client_db)
+    dlg = fc.search_dialog()
+
+    res = []
+    for doc in dlg.search(search_condition):
+        # r = doc.document.__dict__
+        # r["file_cabinet"] = None
+        # r["attachments"] = r["attachments"][0].pages
+        # r["modified"] = str(r["modified"])
+        # r["created"] = str(r["created"])
+        # r["fields"] = {f.name: str(f.value) for f in doc.document.fields }
+        # r["endpoints"] = {k: str(v) for k, v in doc.document.endpoints.items() }
+        # res.append(r)
+        fields = {f.name: str(f.value) for f in doc.document.fields}
+        r = {
+            "ID": doc.document.id,
+            "Datei_Typ": fields["File type"],
+            "Datei_Link": doc.document.endpoints["self"],
+            "Beleg_Typ": fields["Dok.-/ Belegtyp"],
+            "Beleg_Nr": fields["Belegnummer"],
+            "Kunde_Nr": fields["Kundennummer"],
+            "Kunde_Name": fields["Name"],
+            "Fahrzeug_Nr": fields["Fahrgestellnummer"],
+            "Scan_Datum": str(doc.document.modified),
+            "Beleg_Datum": fields["Beleg-Datum"],
+            "Bearbeiter": fields["Bearbeitet von:"],
+            "Seiten": doc.document.attachments[0].pages,
+            "Betrag": fields["Rechnungsbetrag"],
         }
+        res.append(r)
 
-        if result_fields:
-            body["AdditionalResultFields"] = result_fields
-
-        if sort_field:
-            body["SortOrder"] = [{"Field": sort_field, "Direction": sort_direction}]
-
-        params = {"DialogId": search_dialog_id}
-
-        return self._request(
-            "POST",
-            self._url(f"FileCabinets/{file_cabinet_id}/Query/DialogExpression"),
-            params=params,
-            json=body,
-        ).json()
-
-
-def print_json(data: Any) -> None:
-    print(json.dumps(data, indent=2, ensure_ascii=False))
-
-
-def main() -> int:
-    parser = argparse.ArgumentParser(description="DocuWare Suchfilter und Dokumente per requests abfragen")
-    parser.add_argument(
-        "--file-cabinet", default=os.environ.get("DOCUWARE_FILE_CABINET"), help="Name des File Cabinets"
-    )
-    parser.add_argument("--count", type=int, default=10, help="Maximale Anzahl Dokumente")
-    parser.add_argument("--fields", default="", help="Kommagetrennte Ergebnisfelder, z.B. DOCUMENT_TYPE,COMPANY_NAME")
-    parser.add_argument("--list-filters", action="store_true", help="Gueltige Suchfelder des Search-Dialogs ausgeben")
-    parser.add_argument("--list-documents", action="store_true", help="Dokumente ohne Suchbedingung listen")
-    parser.add_argument(
-        "--search",
-        nargs="+",
-        metavar=("DB_FIELD", "VALUE"),
-        help="Suche: DB-Feld plus ein oder mehrere Werte, z.B. --search DOCUMENT_TYPE Invoice",
-    )
-    parser.add_argument("--operation", choices=["And", "Or"], default="And", help="Verknuepfung mehrerer Conditions")
-    parser.add_argument("--sort-field", help="DB-Feld zum Sortieren")
-    parser.add_argument("--sort-direction", choices=["Asc", "Desc"], default="Asc")
-    args = parser.parse_args()
-
-    server_url = os.environ.get("DOCUWARE_SERVER_URL")
-    username = os.environ.get("DOCUWARE_USERNAME")
-    password = os.environ.get("DOCUWARE_PASSWORD")
-    org_id = os.environ.get("DOCUWARE_ORG_ID")
-
-    missing = [
-        name
-        for name, value in {
-            "DOCUWARE_SERVER_URL": server_url,
-            "DOCUWARE_USERNAME": username,
-            "DOCUWARE_PASSWORD": password,
-        }.items()
-        if not value
-    ]
-    if missing:
-        print(f"Fehlende Umgebungsvariablen: {', '.join(missing)}", file=sys.stderr)
-        return 2
-
-    client = DocuWareClient(server_url=server_url, username=username, password=password, org_id=org_id)
-    client.authenticate_password_grant()
-
-    cabinet = client.select_file_cabinet(args.file_cabinet)
-    file_cabinet_id = cabinet["Id"]
-
-    search_dialog = client.select_search_dialog(file_cabinet_id)
-    search_dialog_id = search_dialog["Id"]
-
-    field_list = [f.strip() for f in args.fields.split(",") if f.strip()]
-
-    if args.list_filters:
-        filters = client.get_valid_search_filters(file_cabinet_id, search_dialog_id)
-        print_json(
-            {
-                "file_cabinet": {"id": file_cabinet_id, "name": cabinet.get("Name")},
-                "search_dialog": {"id": search_dialog_id, "name": search_dialog.get("DisplayName")},
-                "valid_filters": filters,
-            }
-        )
-        return 0
+    # Document ID 6809334
+    # File type .pdf
+    # Dok.-/ Belegtyp GA-KAUFVERTRAG MIT UNTERSCHRIFT
+    # Belegnummer 153968
+    # Kundennummer None
+    # Fahrgestellnummer WBA21FZ070FT19353
+    # Modified on 2025-09-29 07:32:23.893000
+    # Beleg-Datum 2025-09-29
+    # Bearbeitet von: SCANENGINE
+    # Rechnungsbetrag None
 
-    if args.list_documents:
-        print_json(client.list_documents(file_cabinet_id, count=args.count, fields=field_list or None))
-        return 0
+    with open("docuware.json", "w") as fwh:
+        json.dump(res, fwh, indent=2)
 
-    if args.search:
-        if len(args.search) < 2:
-            print("--search erwartet DB_FIELD und mindestens einen VALUE", file=sys.stderr)
-            return 2
 
-        db_field = args.search[0]
-        values = args.search[1:]
-        conditions = [{"DBName": db_field, "Value": values}]
+def get_invoice_pdf_by_document_no(client_db: str, document_no: str) -> str:
+    search_condition = {}
+    search_condition["BELEGNUMMER"] = document_no
 
-        print_json(
-            client.search_documents(
-                file_cabinet_id=file_cabinet_id,
-                search_dialog_id=search_dialog_id,
-                conditions=conditions,
-                operation=args.operation,
-                count=args.count,
-                result_fields=field_list or None,
-                sort_field=args.sort_field,
-                sort_direction=args.sort_direction,
-            )
-        )
-        return 0
+    fc = get_file_cabinet(client_db)
+    dlg = fc.search_dialog()
 
-    parser.print_help()
-    return 0
+    for doc in dlg.search(search_condition):
+        return os.getenv("DW_URL").strip("/") + doc.document.endpoints["content"]
+    raise ValueError(f"Document with document_no {document_no} not found in client_db {client_db}")
 
 
 if __name__ == "__main__":
-    raise SystemExit(main())
+    # main()
+    # search_files(client_db="1", vehicle_no="WBA21FZ070FT19353")
+    print(get_invoice_pdf_by_document_no("1", "WRG25117581"))

+ 14 - 0
app/routes.py

@@ -22,6 +22,7 @@ from app.file_storage import get_file, get_files
 from app.query_filter import filter_type_option, import_filter_config
 
 from .db import get_session
+from .docuware_search import get_invoice_pdf_by_document_no
 from .jinja_templates import templates
 from .ldap import ldap_authenticate
 from .models import Forderung
@@ -392,3 +393,16 @@ def export_docx2(request: Request, client_db: str, document_no: str, db: Session
 def get_files2(request: Request):
     filename = get_file(request.query_params.get("name"))
     return FileResponse(filename, media_type="application/octet-stream", filename=filename.name)
+
+
+@router.get("/app/forderungen/files/{primary_key}", response_class=HTMLResponse)
+def forderungen_files(request: Request, primary_key: str, db: Session = Depends(get_session)):
+    pkey = convert_primary_key("forderungen", primary_key)
+    try:
+        filename = get_invoice_pdf_by_document_no(client_db=pkey["Client_DB"], document_no=pkey["Beleg_Nr"])
+        return Response(
+            content=f'<iframe src="{filename}" style="width: 100%; height: 800px;" frameborder="0">Ihr Browser kann leider keine eingebetteten Frames anzeigen.</iframe>',
+            media_type="text/html",
+        )
+    except ValueError as e:
+        raise HTTPException(status_code=404, detail=str(e))

+ 1 - 0
pyproject.toml

@@ -23,4 +23,5 @@ dependencies = [
 	"passlib>=1.7.4",
 	"python-jose>=3.5.0",
 	"requests>=2.34.2",
+	"docuware-client>=0.9.0",
 ]

+ 2 - 2
templates/base/liste.html

@@ -66,10 +66,10 @@ Klassen:
   <div class="modal-dialog">
     <div class="modal-content">
       <div class="modal-header">
-        <h5 class="modal-title" id="exampleModalLabel">Invoice</h5>
+        <h5 class="modal-title" id="exampleModalLabel">Rechnung</h5>
         <button type="button" class="btn-close" data-coreui-dismiss="modal" aria-label="Close"></button>
       </div>
-      <div class="modal-body">
+      <div class="modal-body" id="exampleModalBody">
         <iframe src="/static/assets/pdf/invoice.pdf" style="width: 100%; height: 800px;" frameborder="0">
           Ihr Browser kann leider keine eingebetteten Frames anzeigen.
         </iframe>

+ 1 - 1
templates/forderungen/liste/liste_zeile.html

@@ -16,7 +16,7 @@
 
   <td class="text-end align-top">
     <div class="gc-money">{{ row.Beleg_Betrag|number_format }} €</div>
-    <a href="#" data-coreui-toggle="modal" data-coreui-target="#exampleModal">
+    <a href="#" data-coreui-toggle="modal" data-coreui-target="#exampleModal" hx-get="/app/forderungen/files/{{ row.Client_DB }}_{{ row.Beleg_Nr }}" hx-target="#exampleModalBody" hx-swap="innerHTML">
       <span class="gc-badge gc-badge-info">{{ row.Beleg_Nr }}</span>
     </a>
   </td>

+ 97 - 47
uv.lock

@@ -159,6 +159,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
 ]
 
+[[package]]
+name = "docuware-client"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "httpx", extra = ["http2"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/91/7195d6fbd2b359c6a16c1545eb458f8631b300943ed6ef47fc1ef37d297b/docuware_client-0.9.0.tar.gz", hash = "sha256:b103ef8d5a9e03726670dc0ff0588aee60f1189e976d156fd365c0e8132af76f", size = 49421, upload-time = "2026-07-04T20:16:45.134Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/11/a8/2d044356d2d392eb79c657f10469001da15440fc2bcde149da80fee0652c/docuware_client-0.9.0-py3-none-any.whl", hash = "sha256:02dc71ea53057971c91789363e9483f37ef2e2c4eff1f26310db65155c0c0664", size = 59884, upload-time = "2026-07-04T20:16:43.611Z" },
+]
+
 [[package]]
 name = "docxtpl"
 version = "0.20.2"
@@ -330,6 +342,55 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/4f/36/a7af08d233624515d9a0f5d41b7a01a51fd825b8c795e41800215a3200e7/fastar-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:34f646ac4f5bed3661a106ca56c1744e7146a02aacf517d47b24fd3f25dc1ff6", size = 460604, upload-time = "2026-03-20T14:26:40.771Z" },
 ]
 
+[[package]]
+name = "gcepic"
+version = "0.2.0"
+source = { virtual = "." }
+dependencies = [
+    { name = "aiofiles" },
+    { name = "alembic" },
+    { name = "docuware-client" },
+    { name = "docxtpl" },
+    { name = "fastapi", extra = ["standard"] },
+    { name = "imap-tools" },
+    { name = "jinja2" },
+    { name = "ldap3" },
+    { name = "openpyxl" },
+    { name = "pandas" },
+    { name = "passlib" },
+    { name = "pyodbc" },
+    { name = "python-docx" },
+    { name = "python-dotenv" },
+    { name = "python-jose" },
+    { name = "python-multipart" },
+    { name = "requests" },
+    { name = "sqlalchemy" },
+    { name = "uvicorn", extra = ["standard"] },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "aiofiles", specifier = ">=23.1" },
+    { name = "alembic", specifier = ">=1.11" },
+    { name = "docuware-client", specifier = ">=0.9.0" },
+    { name = "docxtpl", specifier = ">=0.20.2" },
+    { name = "fastapi", extras = ["standard"], specifier = ">=0.95" },
+    { name = "imap-tools", specifier = ">=1.12.1" },
+    { name = "jinja2", specifier = ">=3.1" },
+    { name = "ldap3", specifier = ">=2.9" },
+    { name = "openpyxl", specifier = ">=3.1" },
+    { name = "pandas", specifier = ">=2.0" },
+    { name = "passlib", specifier = ">=1.7.4" },
+    { name = "pyodbc", specifier = ">=4.0" },
+    { name = "python-docx", specifier = ">=0.8" },
+    { name = "python-dotenv", specifier = ">=1.2.1" },
+    { name = "python-jose", specifier = ">=3.5.0" },
+    { name = "python-multipart", specifier = ">=0.0.6" },
+    { name = "requests", specifier = ">=2.34.2" },
+    { name = "sqlalchemy", specifier = ">=2.0" },
+    { name = "uvicorn", extras = ["standard"], specifier = ">=0.22" },
+]
+
 [[package]]
 name = "greenlet"
 version = "3.3.2"
@@ -370,6 +431,28 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
 ]
 
+[[package]]
+name = "h2"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "hpack" },
+    { name = "hyperframe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/d4/a7d6fb3f58be99d65cbf2d3f766896217a2921d0f3ab10711c45dc1519ee/h2-4.4.0.tar.gz", hash = "sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580", size = 2156691, upload-time = "2026-07-23T19:14:19.442Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f6/df/5b14a118322d6097cb9bb30ec6bacad268e546a8ecfcb1f6d0de618dac2f/h2-4.4.0-py3-none-any.whl", hash = "sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c", size = 62368, upload-time = "2026-07-23T19:14:16.143Z" },
+]
+
+[[package]]
+name = "hpack"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" },
+]
+
 [[package]]
 name = "httpcore"
 version = "1.0.9"
@@ -420,6 +503,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
 ]
 
+[package.optional-dependencies]
+http2 = [
+    { name = "h2" },
+]
+
+[[package]]
+name = "hyperframe"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
+]
+
 [[package]]
 name = "idna"
 version = "3.11"
@@ -970,53 +1067,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
 ]
 
-[[package]]
-name = "reisacher-forderung"
-version = "0.1.0"
-source = { virtual = "." }
-dependencies = [
-    { name = "aiofiles" },
-    { name = "alembic" },
-    { name = "docxtpl" },
-    { name = "fastapi", extra = ["standard"] },
-    { name = "imap-tools" },
-    { name = "jinja2" },
-    { name = "ldap3" },
-    { name = "openpyxl" },
-    { name = "pandas" },
-    { name = "passlib" },
-    { name = "pyodbc" },
-    { name = "python-docx" },
-    { name = "python-dotenv" },
-    { name = "python-jose" },
-    { name = "python-multipart" },
-    { name = "requests" },
-    { name = "sqlalchemy" },
-    { name = "uvicorn", extra = ["standard"] },
-]
-
-[package.metadata]
-requires-dist = [
-    { name = "aiofiles", specifier = ">=23.1" },
-    { name = "alembic", specifier = ">=1.11" },
-    { name = "docxtpl", specifier = ">=0.20.2" },
-    { name = "fastapi", extras = ["standard"], specifier = ">=0.95" },
-    { name = "imap-tools", specifier = ">=1.12.1" },
-    { name = "jinja2", specifier = ">=3.1" },
-    { name = "ldap3", specifier = ">=2.9" },
-    { name = "openpyxl", specifier = ">=3.1" },
-    { name = "pandas", specifier = ">=2.0" },
-    { name = "passlib", specifier = ">=1.7.4" },
-    { name = "pyodbc", specifier = ">=4.0" },
-    { name = "python-docx", specifier = ">=0.8" },
-    { name = "python-dotenv", specifier = ">=1.2.1" },
-    { name = "python-jose", specifier = ">=3.5.0" },
-    { name = "python-multipart", specifier = ">=0.0.6" },
-    { name = "requests", specifier = ">=2.34.2" },
-    { name = "sqlalchemy", specifier = ">=2.0" },
-    { name = "uvicorn", extras = ["standard"], specifier = ">=0.22" },
-]
-
 [[package]]
 name = "requests"
 version = "2.34.2"