diff --git a/BACKUP/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx b/BACKUP/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx new file mode 100644 index 0000000..7eaff75 Binary files /dev/null and b/BACKUP/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx differ diff --git a/BoM/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx b/BoM/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx index 6de1e08..7eaff75 100644 Binary files a/BoM/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx and b/BoM/Axio4_Nexio_Conduent Subsystem Cost Analysis.xlsx differ diff --git a/octo_fill.py b/octo_fill.py index 4f82a08..3a4a753 100644 --- a/octo_fill.py +++ b/octo_fill.py @@ -46,7 +46,7 @@ COST_HEADER = "Unit Cost EUR @1000" SKIP_MPNS = { "", "0", "tbd", "n/a", "na", "-", "--", "---", "?", "none", "null", "nan", "xxx", "x", "dnf", "dnp", "do not fit", - "do not populate", + "do not populate", "total", } logging.basicConfig( @@ -199,7 +199,10 @@ def _find_tables(indexed_rows: list[tuple[int, tuple]]): continue empty_streak = 0 - if mfr.lower() == "manufacturer" and mpn.lower() == "mpn": + # Detect a new table header anywhere in the row (handles sub-tables + # at different column positions than the current table) + dr_str_lower = [_cell(v).lower() for v in dr] + if "manufacturer" in dr_str_lower and "mpn" in dr_str_lower: break if mpn and mpn.lower() not in SKIP_MPNS: @@ -237,7 +240,10 @@ def fill_boms( for f in files: log.info(f"Processing {f.name}") try: - wb = openpyxl.load_workbook(f) + # data_only gives us resolved cell values (not formula strings) for + # table/part detection; the writable wb is used for reading/writing prices. + wb_ro = openpyxl.load_workbook(f, data_only=True, read_only=True) + wb = openpyxl.load_workbook(f) except Exception as exc: log.error(f" Cannot open {f.name}: {exc}") continue @@ -246,14 +252,19 @@ def fill_boms( ws = wb[sheet_name] indexed = [ (i, tuple(row)) - for i, row in enumerate(ws.iter_rows(values_only=True), start=1) + for i, row in enumerate(wb_ro[sheet_name].iter_rows(values_only=True), start=1) ] for table in _find_tables(indexed): header_row = table["header_row"] + data_rows = [r for r, _, _ in table["data"]] + row_range = ( + f" (Excel rows {data_rows[0]}–{data_rows[-1]})" + if data_rows else " (no data rows detected)" + ) log.info( f" Sheet '{sheet_name}' row {header_row}: " - f"table at col {table['start_col']}, {len(table['data'])} parts" + f"table at col {table['start_col']}, {len(table['data'])} parts{row_range}" ) # Find or create the cost column. @@ -272,8 +283,11 @@ def fill_boms( for c in range(table["start_col"], max_col + 1): val = ws.cell(header_row, c).value if val is not None: - last_used = c - if str(val).strip().lower() in KNOWN_COST_HEADERS: + val_str = str(val).strip() + # Don't count formula placeholders as "used" columns + if not val_str.startswith("="): + last_used = c + if val_str.lower() in KNOWN_COST_HEADERS: cost_col = c break @@ -288,7 +302,14 @@ def fill_boms( if isinstance(cell, MergedCell): continue existing = cell.value - if existing is not None and str(existing).strip() not in ("", "0") and existing != 0: + is_formula = isinstance(existing, str) and existing.startswith("=") + is_empty = ( + existing is None + or (isinstance(existing, str) and existing.strip() in ("", "0")) + or (isinstance(existing, (int, float)) and existing == 0) + ) + if not is_empty and not is_formula: + log.info(f" Skip row {row_num} [{mpn}]: cell already has {repr(existing)}") total_skipped += 1 continue @@ -306,6 +327,7 @@ def fill_boms( total_missing += 1 log.info(f" No match in Octopart: [{mfr}] [{mpn}]") + wb_ro.close() try: wb.save(f) log.info(f" Saved {f.name}")