90 lines
3.1 KiB
Python
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)
|