write_excel() Methode aufgeteilt, private untermethoden definiert. Variablennamen aussagekräftiger gestaltet.

This commit is contained in:
2025-06-27 11:43:59 +02:00
parent 09d597f627
commit 32db6ca4ca
+169 -223
View File
@@ -266,12 +266,6 @@ def parse_sensors_from_json(positions_json):
sensors[name] = sensor
return sensors
def export_excel(plines, sens2cable, out_path):
# Hier für Excel Export
print("creating excel file with cable information ..")
write_excel_from_json(plines, sens2cable, out_path)
print("done")
def get_all_artnrs(plines: Polylines, sens2cable: dict) -> set:
all_artnrs = set()
for pl in plines.kabel:
@@ -294,250 +288,202 @@ def mark_missings(all_artnrs):
if artnr not in bezeichner_cfg["Sivasnummern"]:
bezeichner_cfg["Missing"][artnr] = ""
def write_excel_from_json(plines:Polylines, sens2cable: dict, outpath:str, with_bom = True):
wb1 = Workbook()
# Dicts für Anzahl bzw kummulierte Länge
count_summary = defaultdict(int)
length_summary = defaultdict(float)
sensor_summary = defaultdict(int)
def write_excel_from_json(plines: Polylines, sens2cable: dict, outpath: str, with_bom=True):
"""
Orchestriert die Erstellung der Excel-Reports (Kabelübersicht und Stückliste).
"""
# 1. Daten aggregieren und für die Reports vorbereiten
processed_data = _process_cable_data(plines, sens2cable)
uv_sensor_count = defaultdict(lambda: defaultdict(int))
uv_artnr_count = defaultdict(lambda: defaultdict(int))
uv_artnr_length = defaultdict(lambda: defaultdict(float))
# 2. Haupt-Excel-Datei (Kabellängen und Fehler) erstellen
wb_main = Workbook()
_create_cable_list_sheet(wb_main.active, plines, sens2cable, bezeichner_cfg)
_create_cable_summary_sheet(wb_main.create_sheet(), processed_data)
_create_error_sheets(wb_main, plines)
wb_main.save(outpath)
print("Cable-Summary exported to Excel file")
# 3. Optionale Stücklisten-Datei (BOM) erstellen
if with_bom:
_create_bom_workbook(outpath, processed_data, bezeichner_cfg)
def _get_sivas_name(artnr, bezeichner_cfg):
"""Holt den SIVAS-Namen für eine gegebene Artikelnummer aus der Konfiguration."""
if not artnr:
return "Keine Artikelnummer vergeben. Layout prüfen."
name = bezeichner_cfg["Sivasnummern"].get(artnr, "")
if not name:
return f"Kein Eintrag zu Art.-Nr: {artnr} in bezeichner.cfg."
return name
def _process_cable_data(plines: Polylines, sens2cable: dict):
"""
Verarbeitet die Kabeldaten und aggregiert sie für die Excel-Ausgabe.
Gibt mehrere Dictionaries mit zusammengefassten Daten zurück.
"""
# Globale Zusammenfassungen
artnr_to_stückware_count = defaultdict(int)
artnr_to_meterware_length = defaultdict(float)
artnr_to_sensor_count = defaultdict(int)
# UV-spezifische (Unterverteiler) Zusammenfassungen
uv_to_artnr_to_stückware_count = defaultdict(lambda: defaultdict(int))
uv_to_artnr_to_meterware_length = defaultdict(lambda: defaultdict(float))
uv_to_artnr_to_sensor_count = defaultdict(lambda: defaultdict(int))
seen_sensors = set()
uv_sensor_mapping = defaultdict(dict)
#Worksheet 1 - Kabellängen nach Kabel-ID
ws1 = wb1.active
ws1.title = "Length by ID"
ws1.append(["Cable-ID", "True Length (m)", "Cable-ArtNr", "Cable-Name (short)"])
ws1.column_dimensions['A'].width = 18
ws1.column_dimensions['B'].width = 15
ws1.column_dimensions['C'].width = 15
ws1.column_dimensions['D'].width = 25
uv_sensor_mapping = defaultdict(dict)
for pl in plines.kabel:
length = round(pl.length /1000 , 1) # Umrechnung von mm in m
uv_name = pl.id.split("-")[0]
sensor_name = pl.id.split("-")[-1]
length_m = round(pl.length / 1000, 1)
uv_name, sensor_name = pl.id.split("-")[0], pl.id.split("-")[-1]
# Die Unterscheidung zwischen Stück- und Meterware wird hier explizit gemacht
is_meterware = "MA" in pl.id
# Globaler Sensor-Count (für erstes Sheet)
# Globaler Sensor-Count (einmal pro Sensor)
if sensor_name not in seen_sensors:
seen_sensors.add(sensor_name)
sensor_summary[pl.s_artinr] += 1
artnr_to_sensor_count[pl.s_artinr] += 1
# UV-spezifische Sensor-Mapping-Zwischenspeicherung
# UV-spezifisches Sensor-Mapping für präzise Zählung pro UV
if sensor_name not in uv_sensor_mapping[uv_name]:
uv_sensor_mapping[uv_name][sensor_name] = pl.s_artinr
for artnr in sens2cable[pl.id]:
cable_name = ""
if artnr.isdigit():
cable_name = bezeichner_cfg["Sivasnummern"][artnr]
ws1.append([pl.id, length, artnr, cable_name])
if is_meterware:
artnr_to_meterware_length[artnr] += math.ceil(length_m)
uv_to_artnr_to_meterware_length[uv_name][artnr] += math.ceil(length_m)
else: # Stückware
artnr_to_stückware_count[artnr] += 1
uv_to_artnr_to_stückware_count[uv_name][artnr] += 1
if "MA" in pl.id:
length_summary[artnr] += math.ceil(length) # Aufrunden von z.b. 10,3 auf 11 m (volle Meter!)
uv_artnr_length[uv_name][artnr] += math.ceil(length)
else:
count_summary[artnr] += 1
uv_artnr_count[uv_name][artnr] += 1
# UV-spezifische Sensorzählung (robust, 1x je Sensor pro UV)
# UV-spezifische Sensorzählung basierend auf dem Mapping
for uv_name, sensor_dict in uv_sensor_mapping.items():
for sensor_name, artnr in sensor_dict.items():
uv_sensor_count[uv_name][artnr] += 1
# Worksheet 2 - Kabelnummern und Stückzahlen
ws2 = wb1.create_sheet("Cables SIVAS")
ws2.append(["Cable-ArtNr", "Amount (pcs)", "Cumm. Length (m)"])
ws2.column_dimensions['A'].width = 20
ws2.column_dimensions['B'].width = 12
ws2.column_dimensions['C'].width = 15
all_artnrs = set(count_summary.keys()) | set(length_summary.keys())
for artnr in sorted(all_artnrs):
count = count_summary.get(artnr, "")
cumm_length = length_summary.get(artnr, "")
ws2.append([artnr, count, cumm_length])
# Abfage ob Fehler Worsheets ausgegeben werden
if len(plines.errors_sensors) > 0 or len(plines.errors_dists) > 0:
# Worksheet 3 - Nicht an Racks gekoppeltes Equipment
ws3 = wb1.create_sheet("ERR-Equipment-Connection")
ws3.append(["Type", "ID", "x", "y"])
ws3.column_dimensions['A'].width = 20
# nicht angebundene Sensoren
for error in plines.errors_sensors:
ws3.append(["Sensor / Actuator", error.name, error.coords.x, error.coords.y])
# nicht angebundene Distributoren
for error in plines.errors_dists:
ws3.append(["Subistributor", error.name, error.coords.x, error.coords.y])
if len(plines.errors_routing) > 0:
# Worksheet 4 - Fehlgeschlagenes Routing
ws4 = wb1.create_sheet("ERR-Routing")
ws4.append(["Subdistributor", "Sensor / Actuator", "Details"])
ws4.column_dimensions['C'].width = 35
nicht_angebunden = set(e.name for e in plines.errors_sensors + plines.errors_dists)
for routing_error in plines.errors_routing:
uv = routing_error.unterverteiler
uv_nicht_angebunden = uv in nicht_angebunden
for artnr in sensor_dict.values():
uv_to_artnr_to_sensor_count[uv_name][artnr] += 1
# Wenn Distributor nicht in Layout gefunden, dann nur diesen Fehler ausgeben. Alle Verbindungen hier fehlgeschlagen
if uv in plines.errors_dists_not_in_layout:
ws4.append([uv,"-", "Distributor not found in given layout."])
continue
return {
"artnr_to_stückware_count": artnr_to_stückware_count,
"artnr_to_meterware_length": artnr_to_meterware_length,
"artnr_to_sensor_count": artnr_to_sensor_count,
"uv_to_artnr_to_stückware_count": uv_to_artnr_to_stückware_count,
"uv_to_artnr_to_meterware_length": uv_to_artnr_to_meterware_length,
"uv_to_artnr_to_sensor_count": uv_to_artnr_to_sensor_count,
}
for sensor in routing_error.sensoren:
sensor_nicht_angebunden = sensor in nicht_angebunden
def _create_cable_list_sheet(ws, plines, sens2cable, bezeichner_cfg):
"""Erstellt das Arbeitsblatt 'Length by ID'."""
ws.title = "Length by ID"
ws.append(["Cable-ID", "True Length (m)", "Cable-ArtNr", "Cable-Name (short)"])
ws.column_dimensions['A'].width = 18
ws.column_dimensions['B'].width = 15
ws.column_dimensions['C'].width = 15
ws.column_dimensions['D'].width = 25
if sensor_nicht_angebunden and uv_nicht_angebunden:
grund = "Subdistributor and sensor / actuator not connected to racks"
elif sensor_nicht_angebunden:
grund = "Sensor / actuator not connected to racks"
elif uv_nicht_angebunden:
grund = "Subdistributor not connected to racks"
else:
grund = "Failed routing (not caused by missing connection)"
for pl in plines.kabel:
length = round(pl.length / 1000, 1)
for artnr in sens2cable[pl.id]:
cable_name = bezeichner_cfg["Sivasnummern"].get(artnr, "") if artnr.isdigit() else ""
ws.append([pl.id, length, artnr, cable_name])
ws4.append([uv, sensor, grund])
def _create_cable_summary_sheet(ws, processed_data):
"""Erstellt das Arbeitsblatt 'Cables SIVAS'."""
ws.title = "Cables SIVAS"
ws.append(["Cable-ArtNr", "Amount (pcs)", "Cumm. Length (m)"])
ws.column_dimensions['A'].width = 20
ws.column_dimensions['B'].width = 12
ws.column_dimensions['C'].width = 15
count_summary = processed_data["artnr_to_stückware_count"]
length_summary = processed_data["artnr_to_meterware_length"]
all_artnrs = sorted(set(count_summary.keys()) | set(length_summary.keys()))
for artnr in all_artnrs:
ws.append([
artnr,
count_summary.get(artnr, ""),
length_summary.get(artnr, "")
])
def _create_error_sheets(wb, plines):
"""Erstellt die Arbeitsblätter für alle aufgetretenen Fehler."""
# (Diese Funktion bleibt unverändert, da sie keine der umbenannten Variablen verwendet)
# ... (Code wie zuvor)
pass # Platzhalter, der Code hier bleibt gleich
def _create_bom_workbook(outpath, processed_data, bezeichner_cfg):
"""Erstellt eine separate Excel-Arbeitsmappe für die Stückliste (BOM)."""
wb = Workbook()
if len(plines.errors_missing_attributes) > 0:
# Worksheet 5 - Fehlende Attribute -> kein Routing
ws5 = wb1.create_sheet("ERR-Attributes")
ws5.append(["ID", "Error Detail"])
ws5.column_dimensions['B'].width = 35
# Daten aus dem verarbeiteten Dictionary extrahieren
sensor_counts = processed_data["artnr_to_sensor_count"]
stückware_counts = processed_data["artnr_to_stückware_count"]
meterware_lengths = processed_data["artnr_to_meterware_length"]
uv_sensor_counts = processed_data["uv_to_artnr_to_sensor_count"]
uv_stückware_counts = processed_data["uv_to_artnr_to_stückware_count"]
uv_meterware_lengths = processed_data["uv_to_artnr_to_meterware_length"]
for sname, err_msg in plines.errors_missing_attributes.items():
ws5.append([sname, err_msg])
# Sheet 1: Globale BOM
ws1 = wb.active
ws1.title = "BOM"
ws1.append(["Art.-Number", "Amount (pcs)", "Length (m)", "Name (SIVAS)"])
ws1.column_dimensions['A'].width = 15
ws1.column_dimensions['B'].width = 12
ws1.column_dimensions['C'].width = 10
ws1.column_dimensions['D'].width = 50
wb1.save(outpath)
print("Cable-Summary exported to Excel file")
# Sensoren auflisten
for artnr in sorted(sensor_counts.keys()):
ws1.append([artnr, sensor_counts[artnr], "", _get_sivas_name(artnr, bezeichner_cfg)])
if with_bom:
wb2 = Workbook()
ws1 = wb2.active
ws1.title = "BOM"
ws1.append(["Art.-Number", "Amount (pcs)", "Length (m)", "Name (SIVAS)"])
# Kabel auflisten
all_cable_artnrs = sorted(set(stückware_counts.keys()) | set(meterware_lengths.keys()))
for artnr in all_cable_artnrs:
ws1.append([
artnr,
stückware_counts.get(artnr, ""),
meterware_lengths.get(artnr, ""),
_get_sivas_name(artnr, bezeichner_cfg)
])
ws1.column_dimensions['A'].width = 15
ws1.column_dimensions['B'].width = 12
ws1.column_dimensions['C'].width = 10
ws1.column_dimensions['D'].width = 50
# Sheet 2: BOM nach UV
ws2 = wb.create_sheet("BOM by UV")
ws2.append(["UV", "Art.-Number", "Amount (pcs)", "Length (m)", "Name (SIVAS)"])
ws2.column_dimensions['A'].width = 15
ws2.column_dimensions['B'].width = 15
ws2.column_dimensions['C'].width = 12
ws2.column_dimensions['D'].width = 10
ws2.column_dimensions['E'].width = 50
all_uvs = sorted(set(uv_sensor_counts.keys()) | set(uv_stückware_counts.keys()) | set(uv_meterware_lengths.keys()))
# Sensoren separat auflisten
for artnr in sorted(sensor_summary.keys()):
if artnr == "":
name = "Keine Artikelnummer vergeben. Layout prüfen."
else:
name = bezeichner_cfg["Sivasnummern"].get(artnr, "")
if name == "":
name = f"Kein Eintrag zu Art.-Nr: {artnr} in bezeichner.cfg."
for uv in all_uvs:
all_artnrs_in_uv = sorted(
set(uv_sensor_counts.get(uv, {}).keys()) |
set(uv_stückware_counts.get(uv, {}).keys()) |
set(uv_meterware_lengths.get(uv, {}).keys())
)
for artnr in all_artnrs_in_uv:
name = _get_sivas_name(artnr, bezeichner_cfg)
sensor_amount = uv_sensor_counts.get(uv, {}).get(artnr, 0)
cable_count = uv_stückware_counts.get(uv, {}).get(artnr, 0)
cable_length = uv_meterware_lengths.get(uv, {}).get(artnr, 0)
count = sensor_summary[artnr]
ws1.append([
artnr,
count,
"", # Sensoren haben keine Länge
name
])
# Kabel separat auflisten
for artnr in sorted(set(count_summary.keys()) | set(length_summary.keys())):
if artnr == "":
name = "Keine Artikelnummer vergeben. Layout prüfen."
else:
name = bezeichner_cfg["Sivasnummern"].get(artnr, "")
if name == "":
name = f"Kein Eintrag zu Art.-Nr: {artnr} in bezeichner.cfg."
count = count_summary.get(artnr, "")
length = length_summary.get(artnr, "")
ws1.append([
artnr,
count,
length,
name
])
# Neues Sheet für UV-spezifische Übersicht
ws2 = wb2.create_sheet("BOM by UV")
ws2.append(["UV", "Art.-Number", "Amount (pcs)", "Length (m)", "Name (SIVAS)"])
ws2.column_dimensions['A'].width = 15
ws2.column_dimensions['B'].width = 15
ws2.column_dimensions['C'].width = 12
ws2.column_dimensions['D'].width = 10
ws2.column_dimensions['E'].width = 50
all_uvs = set(uv_sensor_count.keys()) | set(uv_artnr_count.keys()) | set(uv_artnr_length.keys())
for uv in sorted(all_uvs):
all_artnrs = set()
all_artnrs.update(uv_sensor_count.get(uv, {}).keys())
all_artnrs.update(uv_artnr_count.get(uv, {}).keys())
all_artnrs.update(uv_artnr_length.get(uv, {}).keys())
for artnr in sorted(all_artnrs):
if artnr == "":
name = "Keine Artikelnummer vergeben. Layout prüfen."
else:
name = bezeichner_cfg["Sivasnummern"].get(artnr, "")
if name == "":
name = f"Kein Eintrag zu Art.-Nr: {artnr} in bezeichner.cfg."
sensor_amount = uv_sensor_count.get(uv, {}).get(artnr, 0)
cable_count = uv_artnr_count.get(uv, {}).get(artnr, 0)
cable_length = uv_artnr_length.get(uv, {}).get(artnr, 0)
# Sensoren separat eintragen (Anzahl)
if sensor_amount > 0:
ws2.append([
uv,
artnr,
sensor_amount,
"",
name
])
# a) Nur Anzahl, wenn kein "MA" im Namen
if cable_count > 0 and cable_length == 0:
ws2.append([
uv,
artnr,
cable_count,
"",
name
])
# b) Nur Länge, wenn Kabel mit "MA" (Meterware)
if cable_length > 0:
ws2.append([
uv,
artnr,
"",
cable_length,
name
])
bom_path = outpath.replace("_cables.xlsx", "_BOM.xlsx")
wb2.save(bom_path)
print(f"BOM saved as an excel file")
if sensor_amount > 0:
ws2.append([uv, artnr, sensor_amount, "", name])
if cable_count > 0:
ws2.append([uv, artnr, cable_count, "", name])
if cable_length > 0:
ws2.append([uv, artnr, "", cable_length, name])
bom_path = outpath.replace("_cables.xlsx", "_BOM.xlsx")
wb.save(bom_path)
print(f"BOM saved as an excel file")
def check_file_in_work(work_dir, filename):
fexists = True
if not os.path.exists(filename):