initial commit
This commit is contained in:
89
export_bom.py
Normal file
89
export_bom.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user