Files
kicad-manager/export_bom.py
brentperteet 3f0aff923d initial commit
2026-02-22 08:16:48 -06:00

90 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""
BOM CSV Exporter
================
Reads extract_symbols.json and writes bom.csv with columns:
Reference, MPN, MFG
Power symbols (#PWR, #FLG) are excluded.
Usage:
python3 export_bom.py [project_dir]
"""
import csv
import json
import sys
from pathlib import Path
def main(project_dir: Path):
json_path = project_dir / 'extract_symbols.json'
if not json_path.exists():
print(f"Error: {json_path} not found. Run extract_symbols.py first.", file=sys.stderr)
sys.exit(1)
data = json.loads(json_path.read_text(encoding='utf-8'))
symbols = data['symbols']
# Filter out power/flag symbols, DNP parts, and parts excluded from BOM
parts = [
s for s in symbols
if not (s.get('reference') or '').startswith(('#', '~'))
and not (s.get('lib_id') or '').startswith('power:')
and s.get('in_bom') is not False
and not s.get('dnp')
]
# Collapse multi-unit / duplicate records to one row per reference.
# If multiple records exist for the same ref, pick the one with the
# most complete MPN/MFG data (longest non-placeholder string).
def data_score(s):
props = s.get('properties', {})
mpn = props.get('MPN', '')
mfg = props.get('MFG') or props.get('MANUFACTURER', '')
placeholder = mpn.strip().lower() in ('', 'x', 'tbd', 'n/a', 'na')
return (0 if placeholder else len(mpn) + len(mfg))
from collections import defaultdict
by_ref: dict[str, list] = defaultdict(list)
for s in parts:
by_ref[s.get('reference', '')].append(s)
best: list[dict] = []
for ref, recs in by_ref.items():
best.append(max(recs, key=data_score))
# Sort by reference
def ref_sort_key(r):
ref = r.get('reference') or ''
letters = ''.join(c for c in ref if c.isalpha())
digits = ''.join(c for c in ref if c.isdigit())
return (letters, int(digits) if digits else 0)
best.sort(key=ref_sort_key)
out_path = project_dir / 'bom.csv'
with out_path.open('w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Reference', 'MPN', 'MFG'])
for s in best:
props = s.get('properties', {})
writer.writerow([
s.get('reference', ''),
props.get('MPN', ''),
props.get('MFG') or props.get('MANUFACTURER', ''),
])
excluded_bom = sum(1 for s in symbols if s.get('in_bom') is False
and not (s.get('reference') or '').startswith(('#', '~')))
excluded_dnp = sum(1 for s in symbols if s.get('dnp')
and not (s.get('reference') or '').startswith(('#', '~'))
and s.get('in_bom') is not False)
print(f"Excluded: {excluded_bom} 'exclude from BOM', {excluded_dnp} DNP")
print(f"Wrote {len(best)} unique references to {out_path} (collapsed from {len(parts)} records)")
if __name__ == '__main__':
project_dir = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).parent.resolve()
main(project_dir)