import re import time import logging from urllib.parse import quote, urlencode import requests logger = logging.getLogger(__name__) EV_HOST = "https://evergabe.telekom.de" USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0" class EVergabeError(Exception): pass class EVergabeClient: """Python-Port der AutoIt EVvergabeWebobj.au3 Funktionen.""" def __init__(self, username, password, name=""): self.username = username self.password = password self.name = name self.session = requests.Session() self.session.headers.update({ "User-Agent": USER_AGENT, }) self.session.verify = True def _url_encode(self, s): return quote(str(s), safe="") def _extract_csrf(self, html): meta = re.search( r'(?i)]+name="csrf-token"[^>]+content="([^"]+)"', html ) if meta: return "_csrf", meta.group(1) inp = re.search( r'(?i)name="([^"]*csrf[^"]*)"[^>]*value="([^"]+)"', html ) if inp: return inp.group(1), inp.group(2) raise EVergabeError("CSRF-Token nicht gefunden") def login(self): html = self._get(f"{EV_HOST}/public/login") csrf_field, csrf_value = self._extract_csrf(html) body = ( f"LoginForm[username]={self._url_encode(self.username)}" f"&LoginForm[password]={self._url_encode(self.password)}" f"&{csrf_field}={self._url_encode(csrf_value)}" ) resp = self.session.post( f"{EV_HOST}/public/login", data=body, headers={"Content-Type": "application/x-www-form-urlencoded"}, allow_redirects=False, ) if resp.status_code not in (301, 302, 303) and "Logout" not in resp.text: raise EVergabeError(f"Login fehlgeschlagen, HTTP {resp.status_code}") if resp.status_code in (301, 302, 303): loc = resp.headers.get('Location', '/') if loc.startswith('http'): follow = self.session.get(loc, allow_redirects=True) else: follow = self.session.get(f"{EV_HOST}{loc}", allow_redirects=True) logger.info("EVergabe Login erfolgreich") return True def _get(self, url, referer=""): headers = {} if referer: headers["Referer"] = referer resp = self.session.get(url, headers=headers, timeout=30) text = resp.text.replace("amp;", "") with open(r'E:\_Coding\__Deepseek_coding\AufmassCreater v2.35 (20260309)\_aufmass_web\ev_debug.log', 'a', encoding='utf-8') as f: f.write(f"\n{'='*80}\nGET {url}\nReferer: {referer}\nStatus: {resp.status_code}\n{'='*80}\n{text}\n\n") return text def _post(self, url, body, referer=""): headers = {"Content-Type": "application/x-www-form-urlencoded"} if referer: headers["Referer"] = referer resp = self.session.post(url, data=body, headers=headers, timeout=30) return resp.text.replace("amp;", "") # === _EV_Search_SM_obj === def search_sm(self, sm_nr): url = ( f"{EV_HOST}/framework-agreement-call" f"?OrderRecallSearch%5Bhead_line%5D={self._url_encode(sm_nr)}" f"&OrderRecallSearch%5Bdocument_no%5D=" f"&OrderRecallSearch%5Bincoming_date%5D=" f"&OrderRecallSearch%5Border_date%5D=" f"&OrderRecallSearch%5Bgeneral_agreement_id%5D=" f"&OrderRecallSearch%5Bdocument_state%5D=-1" ) referer = f"{EV_HOST}/framework-agreement-call" html = self._get(url, referer) if sm_nr not in html: return [] results = [] titles = re.findall(r'(?m)no-wrap" title="([^"]+)', html) bedarfnr = re.findall(r'Bedarfsnr.: 0([^<]+)<', html) belegnr = re.findall(r'(42[^\/]+)\/0001', html) detail_ids = re.findall(r'call/details\?id=([^&]+)&c=1', html) rv = re.findall(r'/framework-agreement/details\?id=[^"]+">([^<]+)', html) dates_all = re.findall(r'(\d{2}\.\d{2}\.\d{4})', html) ausfuehrungsfrist = re.findall(r'(\d{2}\.\d{2}\.\d{4})\s*\s*\s*[^<]+', html) beleg_eingang = dates_all if dates_all else [] max_len = max(len(titles), len(bedarfnr), len(belegnr), len(detail_ids)) logger.info(f"search_sm: titles={len(titles)}, bedarfnr={len(bedarfnr)}, belegnr={len(belegnr)}, detail_ids={len(detail_ids)}") logger.info(f"search_sm: detail_ids={detail_ids}") logger.info(f"search_sm: titles_sample={titles[0][:100] if titles else 'none'}") for i in range(max_len): results.append({ "title": (titles[i].replace("SM Auftragsnummer: 000", " SM ").replace("\r", " ").replace("\n", " ") if i < len(titles) else ""), "bedarf_nr": bedarfnr[i] if i < len(bedarfnr) else "", "beleg_nr": belegnr[i] if i < len(belegnr) else "", "detail_id": detail_ids[i] if i < len(detail_ids) else "", "rv": rv[i] if i < len(rv) else "", "beleg_eingang": beleg_eingang[i] if i < len(beleg_eingang) else "", "ausfuehrungsfrist": ausfuehrungsfrist[i] if i < len(ausfuehrungsfrist) else "", "status": status[i] if i < len(status) else "", }) return results # === _EV_Aspa_obj === def get_aspa(self, details_id): url = f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1" referer = f"{EV_HOST}/framework-agreement-call/" html = self._get(url, referer) name = re.search(r'Name\s*\s*\s*([^<]+?)\s*', html) emails = re.findall(r'mailto:([^"?\s&]+)', html) tel = re.search(r'th>Telefon([^<]+)', html) name_val = name.group(1).strip() if name else "" email_val = emails[1].strip() if len(emails) > 1 else (emails[0].strip() if emails else "") tel_val = tel.group(1).strip() if tel else "" return { "name": name_val, "email": email_val, "tel": tel_val, } # === _EV_Hole_Kopfdaten_obj (kombiniert) === def hole_kopfdaten(self, sm_nr): self.login() results = self.search_sm(sm_nr) if not results: raise EVergabeError(f"Kein Treffer für SM-Nr: {sm_nr}") first = results[0] logger.info(f"SM-Suche: title={first['title'][:50]}, detail_id={first['detail_id']}, beleg_eingang={first['beleg_eingang']}, ausfuehrungsfrist={first['ausfuehrungsfrist']}") aspa = self.get_aspa(first["detail_id"]) return { "bezeichnung": first["title"].strip(), "sm_nr": first["bedarf_nr"], "abruf_nr": first["beleg_nr"], "detail_id": first["detail_id"], "rv": first["rv"], "beleg_eingang": first["beleg_eingang"], "ausfuehrungsfrist": first["ausfuehrungsfrist"], "status": first["status"], "ansprechpartner_name": aspa["name"], "ansprechpartner_email": aspa["email"], "ansprechpartner_tel": aspa["tel"], } # === _EV_Pos_eintragen_obj (Aufmaß übertragen) === def aufmass_uebertragen(self, sm_nr, positionen, bauabschnitt="", leist_zeitv="", leist_zeitb="", leistungsort="", teilaufmass=False, schlussaufmass=False): self.login() results = self.search_sm(sm_nr) if not results: raise EVergabeError(f"Kein Treffer für SM-Nr: {sm_nr}") if len(results) > 1: details_id = results[0]["detail_id"] else: details_id = results[0]["detail_id"] logger.info(f"DetailsID: {details_id}") html = self._get( f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1", f"{EV_HOST}/framework-agreement-call" ) if "LERF nicht möglich" in html: raise EVergabeError("LERF nicht möglich") html = self._get( f"{EV_HOST}/sheet/index?c=1&importId={details_id}", f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1" ) html = self._get( f"{EV_HOST}/sheet/create-sheet?c=1&id={details_id}", f"{EV_HOST}/sheet/index?c=1&importId={details_id}" ) csrf_field, csrf_value = self._extract_csrf(html) leistungsort1 = leistungsort[:25] if len(leistungsort) >= 25 else leistungsort leistungsort_so = leistungsort sachbearbeiter = self.name[:12] if len(self.name) >= 12 else self.name bauabschnitt_clean = bauabschnitt[:39] if len(bauabschnitt) >= 40 else bauabschnitt kurztext = f"SM {sm_nr}"[:39] langtext = f"{leistungsort} {bauabschnitt} SM {sm_nr}" final = "X" if schlussaufmass else "" body = ( f"_csrf={self._url_encode(csrf_value)}%3D%3D" f"&BapiEssr%5Bfinal%5D={final}" f"&BapiEssr%5Bfinal%5D={final}" f"&BapiEssr%5Blzvon%5D={self._url_encode(leist_zeitv)}" f"&BapiEssr%5Blzbis%5D={self._url_encode(leist_zeitb)}" f"&BapiEssr%5Bdlort%5D={self._url_encode(leistungsort1)}" f"&BapiEssr%5Bsbnaman%5D={self._url_encode(sachbearbeiter)}" f"&BapiEssr%5Btxz01%5D={self._url_encode(bauabschnitt_clean)}" f"&BapiEssr%5Bdescription%5D={self._url_encode(langtext)}" f"&save+sheets=" ) html = self._post( f"{EV_HOST}/sheet/create-sheet?c=1&iid={details_id}", body, f"{EV_HOST}/sheet/create-sheet?c=1&iid={details_id}" ) sheet_id_match = re.search(r'sheetId=(.*)">Kopfdaten', html) if not sheet_id_match: raise EVergabeError(f"sheetID nicht gefunden. Response: {html[:200]}") sheet_id = sheet_id_match.group(1) logger.info(f"SheetID: {sheet_id}") ergebnisse = [] for idx, pos in enumerate(positionen): pos_html = self._get( f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}", f"{EV_HOST}/sheet/index?c=1&importId={details_id}" ) csrf_field2, csrf_value2 = self._extract_csrf(pos_html) pos_nr = pos.get("pos_nr", "") pos_body = f"_csrf={self._url_encode(csrf_value2)}%3D%3D&ServicePosition%5Bnumber%5D={self._url_encode(pos_nr)}&insertPosition=0" pos_html2 = self._post( f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}", pos_body, f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}" ) pos_id_match = re.search(r'positionId=([^&]+)', pos_html2) if not pos_id_match: ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": "positionId nicht gefunden"}) continue pos_id = pos_id_match.group(1) create_url = f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}" create_html = self._get( f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}", f"{EV_HOST}/sheet/index?c=1&importId={details_id}" ) csrf_field3, csrf_value3 = self._extract_csrf(create_html) einheit = pos.get("einheit", "ST") abschnitt = pos.get("bauabschnitt", bauabschnitt_clean)[:25] langtext_pos = pos.get("langtext", "") pos_kurztext = f"{pos_nr}|{pos.get('bezeichnung', '')}" if einheit == "ST": menge = str(pos.get("menge", "")).replace(",", "%2C") post_body = ( f"_csrf={self._url_encode(csrf_value3)}%3D%3D" f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}" f"&ServicePosition%5B0%5D%5Bquantity%5D={menge}" f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}" f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}" f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save=" ) elif einheit == "M": faktor = str(pos.get("faktor", "1")).replace(",", "%2C") meter = str(pos.get("menge", "")).replace(",", "%2C") post_body = ( f"_csrf={self._url_encode(csrf_value3)}%3D%3D" f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}" f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=ME" f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}" f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={meter}" f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}" f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}" f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save=" ) elif einheit == "M2": faktor = str(pos.get("faktor", "1")).replace(",", "%2C") laenge = str(pos.get("laenge", "")).replace(",", "%2C") breite = str(pos.get("breite", "")).replace(",", "%2C") post_body = ( f"_csrf={self._url_encode(csrf_value3)}%3D%3D" f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}" f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=MF" f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}" f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={laenge}" f"&ServicePosition%5B0%5D%5BformulaValueWidth%5D={breite}" f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}" f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}" f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save=" ) elif einheit == "M3": faktor = str(pos.get("faktor", "1")).replace(",", "%2C") laenge = str(pos.get("laenge", "")).replace(",", "%2C") breite = str(pos.get("breite", "")).replace(",", "%2C") tiefe = str(pos.get("tiefe", "")).replace(",", "%2C") post_body = ( f"_csrf={self._url_encode(csrf_value3)}%3D%3D" f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}" f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=MV" f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}" f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={laenge}" f"&ServicePosition%5B0%5D%5BformulaValueWidth%5D={breite}" f"&ServicePosition%5B0%5D%5BformulaValueDepth%5D={tiefe}" f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}" f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}" f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save=" ) elif einheit in ("STD", "LE"): stueck = str(pos.get("menge", "")).replace(",", "%2C") post_body = ( f"_csrf={self._url_encode(csrf_value3)}%3D%3D" f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}" f"&ServicePosition%5B0%5D%5Bquantity%5D={stueck}" f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}" f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}" f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save=" ) else: ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": f"Unbekannte Einheit: {einheit}"}) continue result_html = self._post( f"{EV_HOST}/sheet-position/create?insertPosition={idx}&positionId={pos_id}&c=1&sheetId={sheet_id}", post_body, f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}" ) if "erfolgreich gespeichert" in result_html: ergebnisse.append({"pos_nr": pos_nr, "status": "ok", "msg": "eingetragen"}) else: ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": "Fehlgeschlagen"}) time.sleep(0.5) return ergebnisse