diff --git a/lib/drawdxf.py b/lib/drawdxf.py index 43550a3..314af8c 100644 --- a/lib/drawdxf.py +++ b/lib/drawdxf.py @@ -145,47 +145,18 @@ def draw_sensors(plines, doc): pos2sensors[(pt2.x, pt2.y)].append(pl) # Sensor Blöcke zeichnen - for (x,y), pls in pos2sensors.items(): - for i, pl in enumerate(pls): + for (x,y), pls in pos2sensors.items(): + for i, pl in enumerate(pls): sensor_name = pl.id.split('-')[-1] - pt1 = pl.coords[-2] - pt2 = pl.coords[-1] + pt1, pt2 = pl.coords[-2], pl.coords[-1] + + # Platzierungsinfo über Hilfsfunktion + placement_info = _calculate_text_placement(pt1, pt2, 'sensor', i, len(pls)) - dx = pt2.x - pt1.x - dy = pt2.y - pt1.y text = msp.add_text(sensor_name, dxfattribs=dxfattribs_sensors) - - # Offsets für Beschriftungen - offsetx = 0 - offsety = 0 - - # Kabel Horizontal - if abs(dx) > abs(dy): - n = len(pls) - center_offset = i - (n - 1) / 2 # Falls mehrere Sensoren -> Verschiebung so, dass Kabel immer in Mitte ankommt - offsety = -80 + center_offset * 110 # Wert -80 über Try-and-Error zur Mitte der Beschriftung angepasst - if dx > 0: - halign = 0 # LEFT - offsetx = 50 # Wert 50 durch Try-and-Error sodass etwas Abstand zu Kabelpritsche - else: - halign = 2 # RIGHT - offsetx = -50 # Wert 50 durch Try-and-Error sodass etwas Abstand zu Kabelpritsche - valign = 1 # BOTTOM - - # Kabel vertikal - else: - if dy > 0: - valign = 0 # BASELINE - offsety = 50 + i * 110 # nach oben anfügen - else: - valign = 3 # TOP - offsety = -50 - i * 110 # nach unten anfügen - halign = 1 # CENTER - - # Alignments setzen - text.dxf.halign = halign - text.dxf.valign = valign - text.set_placement((pt2.x + offsetx, pt2.y + offsety)) + text.dxf.halign = placement_info["halign"] + text.dxf.valign = placement_info["valign"] + text.set_placement(placement_info["placement"]) def draw_subdists(plines, doc): msp = doc.modelspace() @@ -211,36 +182,58 @@ def draw_subdists(plines, doc): subdist_name = pl.id.split('-')[0] pt2 = pl.coords[1] - dx = pt2.x - pt1.x - dy = pt2.y - pt1.y + placement_info = _calculate_text_placement(pt1, pt2, 'subdist') - # Offsets für Beschriftungen - offsetx = 0 - offsety = 0 - - if abs(dx) > abs(dy): # Horizontal - offsety = -80 # Wert -80 über Try-and-Error zur Mitte der Beschriftung angepasst - if dx < 0: - halign = 0 # LEFT - offsetx = 50 # Wert 50 durch Try-and-Error sodass etwas Abstand zu Kabelpritsche - else: - halign = 2 # RIGHT - offsetx = -50 # Wert 50 durch Try-and-Error sodass etwas Abstand zu Kabelpritsche - valign = 1 # BOTTOM - else: # Vertikal - if dy < 0: - valign = 0 # BASELINE - offsety = 50 - else: - valign = 3 # TOP - offsety = -50 - halign = 1 # CENTER - - # Text platzieren text = msp.add_text(subdist_name, dxfattribs=dxfattribs_subdists) - text.dxf.halign = halign - text.dxf.valign = valign - text.set_placement((pt1.x + offsetx, pt1.y + offsety)) + text.dxf.halign = placement_info["halign"] + text.dxf.valign = placement_info["valign"] + text.set_placement(placement_info["placement"]) + +def _calculate_text_placement(pt1, pt2, text_type='sensor', item_index=0, total_items=1): + """ + Berechnet die optimale Position und Ausrichtung für Beschriftungen. + Gibt ein Dictionary mit Platzierungskoordinaten und Ausrichtungs-Flags zurück. + """ + dx = pt2.x - pt1.x + dy = pt2.y - pt1.y + + # Lese Offsets aus der Konfiguration, mit Fallback-Werten + offset_h_dist = 50.0 + offset_v_dist = 50.0 + offset_y_center = -80.0 + offset_y_stack = 110.0 + + offsetx, offsety = 0, 0 + + if abs(dx) > abs(dy): # Kabel verläuft hauptsächlich horizontal + valign = 1 # BOTTOM + if text_type == 'sensor': + center_offset = item_index - (total_items - 1) / 2 + offsety = offset_y_center + center_offset * offset_y_stack + else: # subdist + offsety = offset_y_center + + if dx > 0: + halign = 0 # LEFT + offsetx = offset_h_dist + else: + halign = 2 # RIGHT + offsetx = -offset_h_dist + + else: # Kabel verläuft hauptsächlich vertikal + halign = 1 # CENTER + if dy > 0: + valign = 0 # BASELINE + offsety = offset_v_dist + item_index * offset_y_stack + else: + valign = 3 # TOP + offsety = -offset_v_dist - item_index * offset_y_stack + + return { + "placement": (pt2.x + offsetx, pt2.y + offsety), + "halign": halign, + "valign": valign, + } def model_from_json(json_file): with open(json_file, encoding='utf-8') as fh: @@ -266,12 +259,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,249 +281,237 @@ 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 + 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, + } + +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 + + 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]) + +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.""" + # Sheet: ERR-Equipment-Connection + if plines.errors_sensors or plines.errors_dists: + ws = wb.create_sheet("ERR-Equipment-Connection") + ws.append(["Type", "ID", "x", "y"]) + ws.column_dimensions['A'].width = 20 + for error in plines.errors_sensors: + ws.append(["Sensor / Actuator", error.name, int(error.coords.x), int(error.coords.y)]) + for error in plines.errors_dists: + ws.append(["Subistributor", error.name, int(error.coords.x), int(error.coords.y)]) + + # Sheet: ERR-Routing + if plines.errors_routing: + ws = wb.create_sheet("ERR-Routing") + ws.append(["Subdistributor", "Sensor / Actuator", "Details"]) + ws.column_dimensions['A'].width = 20 + ws.column_dimensions['B'].width = 20 + ws.column_dimensions['C'].width = 50 + nicht_angebunden = {e.name for e in plines.errors_sensors + plines.errors_dists} + for error in plines.errors_routing: + uv, uv_nicht_angebunden = error.unterverteiler, error.unterverteiler in nicht_angebunden if uv in plines.errors_dists_not_in_layout: - ws4.append([uv,"-", "Distributor not found in given layout."]) + ws.append([uv, "-", "Distributor not found in given layout."]) continue - - for sensor in routing_error.sensoren: + for sensor in error.sensoren: sensor_nicht_angebunden = sensor in nicht_angebunden - - 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)" - - ws4.append([uv, sensor, grund]) + 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)" + ws.append([uv, sensor, grund]) - 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 - + # Sheet: ERR-Attributes + if plines.errors_missing_attributes: + ws = wb.create_sheet("ERR-Attributes") + ws.append(["ID", "Error Detail"]) + ws.column_dimensions['B'].width = 35 for sname, err_msg in plines.errors_missing_attributes.items(): - ws5.append([sname, err_msg]) + ws.append([sname, err_msg]) - wb1.save(outpath) - print("Cable-Summary exported to Excel file") +def _create_bom_workbook(outpath, processed_data, bezeichner_cfg): + """Erstellt eine separate Excel-Arbeitsmappe für die Stückliste (BOM).""" + wb = Workbook() + + # 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"] - if with_bom: - wb2 = Workbook() - ws1 = wb2.active - ws1.title = "BOM" - ws1.append(["Art.-Number", "Amount (pcs)", "Length (m)", "Name (SIVAS)"]) + # 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 - ws1.column_dimensions['A'].width = 15 - ws1.column_dimensions['B'].width = 12 - ws1.column_dimensions['C'].width = 10 - ws1.column_dimensions['D'].width = 50 + # Sensoren auflisten + for artnr in sorted(sensor_counts.keys()): + ws1.append([artnr, sensor_counts[artnr], "", _get_sivas_name(artnr, bezeichner_cfg)]) - # 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." + # 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) + ]) - count = sensor_summary[artnr] - - ws1.append([ - artnr, - count, - "", # Sensoren haben keine Länge - name - ]) + # 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())) - # 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." + 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 = count_summary.get(artnr, "") - length = length_summary.get(artnr, "") - ws1.append([ - artnr, - count, - length, - name - ]) + 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]) - # 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") + 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