From db25260c9c5651372b20e3e55cbaa0fae239945b Mon Sep 17 00:00:00 2001 From: David Rice Date: Wed, 29 Apr 2026 21:06:39 +0100 Subject: [PATCH] Changes --- .vscode/settings.json | 3 + bom_price_checker.py | 33 +- bom_prices.xlsx | Bin 0 -> 10301 bytes price_cache.json | 1340 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1374 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 bom_prices.xlsx create mode 100644 price_cache.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c9ebf2d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/bom_price_checker.py b/bom_price_checker.py index 01c98a1..7991143 100644 --- a/bom_price_checker.py +++ b/bom_price_checker.py @@ -42,6 +42,7 @@ from typing import Optional import requests import openpyxl +from openpyxl.cell.cell import MergedCell import pandas as pd from dotenv import load_dotenv @@ -595,6 +596,8 @@ def write_back( skipped = 0 for row_num, mfr, mpn in table.data: cell = ws.cell(row_num, cost_col) + if isinstance(cell, MergedCell): + continue existing = cell.value if existing is not None and str(existing).strip() != "": skipped += 1 @@ -620,6 +623,32 @@ def write_back( log.error(f" Save failed for {file_path.name}: {exc}") +# ── Summary output ──────────────────────────────────────────────────────────── + +OUTPUT_FILE = Path("bom_prices.xlsx") + + +def write_summary(parts: set[tuple[str, str]], price_lookup: dict[str, Optional[float]]) -> None: + rows = [] + for mfr, mpn in sorted(parts): + price = price_lookup.get(_part_key(mfr, mpn)) + rows.append({ + "Manufacturer": mfr, + "MPN": mpn, + f"Unit Cost EUR @{QUANTITY}": price, + }) + + df = pd.DataFrame(rows) + with pd.ExcelWriter(OUTPUT_FILE, engine="openpyxl") as writer: + df.to_excel(writer, index=False, sheet_name="Prices") + ws = writer.sheets["Prices"] + for col in ws.columns: + width = max(len(str(cell.value or "")) for cell in col) + ws.column_dimensions[col[0].column_letter].width = min(width + 3, 50) + + log.info(f"Summary written → {OUTPUT_FILE}") + + # ── Main ─────────────────────────────────────────────────────────────────────── def main() -> None: @@ -638,7 +667,7 @@ def main() -> None: else: log.warning("No API keys found. Copy .env.example → .env and fill in your keys.") - file_map, parts = extract_parts(BOM_DIR) + _, parts = extract_parts(BOM_DIR) if not parts: log.error("No valid (Manufacturer, MPN) pairs found in BoM files.") @@ -655,7 +684,7 @@ def main() -> None: log.info(f" avg {tag} ({r.sources_found()}/3 sources)") price_lookup[_part_key(mfr, mpn)] = r.average_eur - write_back(file_map, price_lookup) + write_summary(parts, price_lookup) found = sum(1 for v in price_lookup.values() if v is not None) missing = [mpn for (_, mpn), v in diff --git a/bom_prices.xlsx b/bom_prices.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..41e9e4640cb861c47cefd897b624e9e6714594a2 GIT binary patch literal 10301 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