106 lines
4.8 KiB
Python
106 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Build ONE per-part Excel sheet from the master template.
|
|
|
|
The new Vecmocon layout stores each component on its own: a folder ``MPN_make_typeid/``
|
|
holding that part's own single-sheet workbook (plus its datasheet, symbol and footprint).
|
|
There is no accumulating master workbook any more - so this script takes one part and writes
|
|
``<MPN>_<make>_<typeid>.xlsx``, a copy of just that typeid's template sheet (headers, styling,
|
|
widths, freeze preserved) with a single filled data row.
|
|
|
|
What gets stamped automatically (never read from a datasheet):
|
|
- column A ``MPN_make_type`` = the part tag ``<MPN>_<make>_<typeid>``
|
|
- column B ``Skill Version`` = this typeid's skill version (vN, from versions.json)
|
|
- column C ``Template Version``= this typeid's template version (vN, from versions.json)
|
|
|
|
The four design columns (Library Ref/Path, Footprint Ref/Path) are filled from a --design
|
|
map when the symbol/footprint step provides them, and left blank otherwise. Everything else
|
|
comes from the extracted ``values``.
|
|
|
|
# one part, values only (design columns left blank for now)
|
|
python fill_templates.py part.json --template assets/template/template.xlsx --dest out/
|
|
# later, same part with the Altium-derived design fields merged in
|
|
python fill_templates.py part.json --template ... --dest out/ --design design.json
|
|
|
|
``part.json`` is either a single part object or {"parts": [ ... ]} (each part built to its
|
|
own file). A part = {"mpn","manufacturer" (or "make"),"typeid","values":{header:value,...}}.
|
|
"""
|
|
import argparse, json, os
|
|
import openpyxl
|
|
from common import (norm, part_tag, version_labels,
|
|
COL_TAG, COL_SKILL_VER, COL_TEMPLATE_VER, DESIGN_COLS)
|
|
|
|
|
|
def template_headers(template_path, typeid):
|
|
"""Canonical headers for a typeid sheet, or None if the template has no such sheet."""
|
|
wb = openpyxl.load_workbook(template_path, read_only=True)
|
|
if typeid not in wb.sheetnames:
|
|
return None
|
|
ws = wb[typeid]
|
|
return [ws.cell(1, c).value for c in range(1, ws.max_column + 1)]
|
|
|
|
|
|
def part_to_row(part, headers, design=None):
|
|
"""Map one part to {header: value}, following the fixed-column rules above."""
|
|
vals = {norm(k): v for k, v in part.get("values", {}).items()}
|
|
dmap = {norm(k): v for k, v in (design or {}).items()}
|
|
tag = part_tag(part)
|
|
sver, tver = version_labels(part.get("typeid", "NA"))
|
|
row = {}
|
|
for h in headers:
|
|
key = norm(h)
|
|
if key == norm(COL_TAG): row[h] = tag
|
|
elif key == norm(COL_SKILL_VER): row[h] = sver
|
|
elif key == norm(COL_TEMPLATE_VER): row[h] = tver
|
|
elif key == norm("Manufacturer"): row[h] = part.get("manufacturer", vals.get("manufacturer", ""))
|
|
elif h in DESIGN_COLS: row[h] = dmap.get(key, vals.get(key, ""))
|
|
else: row[h] = vals.get(key, "")
|
|
return tag, row
|
|
|
|
|
|
def build_part_sheet(part, template_path, dest_dir, design=None):
|
|
"""Write ``<tag>.xlsx`` = just this part's typeid sheet with one filled row. Returns
|
|
(path, tag). Reused for backfill: re-extract a part and call this again to regenerate its
|
|
sheet against the current template (new columns) and current stamped versions."""
|
|
typeid = part["typeid"]
|
|
headers = template_headers(template_path, typeid)
|
|
if headers is None:
|
|
raise SystemExit(f"no template sheet for typeid '{typeid}'")
|
|
wb = openpyxl.load_workbook(template_path)
|
|
for name in list(wb.sheetnames): # keep only this typeid's sheet
|
|
if name != typeid:
|
|
del wb[name]
|
|
ws = wb[typeid]
|
|
tag, row = part_to_row(part, headers, design)
|
|
for r in range(2, ws.max_row + 1): # clear any stray rows below the header
|
|
for c in range(1, len(headers) + 1):
|
|
ws.cell(r, c).value = None
|
|
for c, h in enumerate(headers, start=1):
|
|
ws.cell(2, c, row.get(h, ""))
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
out = os.path.join(dest_dir, f"{tag}.xlsx")
|
|
wb.save(out)
|
|
return out, tag
|
|
|
|
|
|
def _iter_parts(data):
|
|
return data["parts"] if isinstance(data, dict) and "parts" in data else [data]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("part_json")
|
|
ap.add_argument("--template", required=True)
|
|
ap.add_argument("--dest", required=True)
|
|
ap.add_argument("--design", default=None, help="JSON map of design columns to merge in")
|
|
a = ap.parse_args()
|
|
data = json.load(open(a.part_json, encoding="utf-8"))
|
|
design = json.load(open(a.design, encoding="utf-8")) if a.design else None
|
|
for p in _iter_parts(data):
|
|
out, tag = build_part_sheet(p, a.template, a.dest, design)
|
|
sver, tver = version_labels(p.get("typeid", "NA"))
|
|
print(f"wrote {out} (tag {tag}, skill {sver}, template {tver})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|