Skill/scripts/fill_templates.py

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()