Skill/scripts/fill_templates.py

151 lines
6.7 KiB
Python
Executable File

#!/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, re
import openpyxl
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from common import (norm, part_tag, version_labels, changelog_for_typeid,
COL_TAG, COL_SKILL_VER, COL_TEMPLATE_VER, DESIGN_COLS)
GREEN = "B6D7A8"; GRAY = "BFBFBF"
VERSION_SHEET = "Version History"
def _vtrans(v):
"""'v2' -> 'v1 -> v2' (each changelog row records the new version; the previous is n-1)."""
m = re.match(r"v?(\d+)", str(v).strip()) if v is not None else None
if not m:
return "" if v is None else str(v)
n = int(m.group(1))
return f"v{n-1} → v{n}" if n > 1 else f"v{n}"
def add_version_history(wb, typeid):
"""Sheet 2 of a part workbook: the cumulative version history for this typeid — every
template/skill change (v1->v2, v2->v3, ...) with its date and description. Added only when
there has actually been a template/skill update; still-v1 typeids get no second sheet."""
rows = changelog_for_typeid(typeid)
if not rows:
return False
ws = wb.create_sheet(VERSION_SHEET)
s = Side(style="thin", color=GRAY)
border = Border(left=s, right=s, top=s, bottom=s)
headers = ["Date", "Skill Version", "Template Version", "Description", "By"]
for c, h in enumerate(headers, start=1):
cell = ws.cell(1, c, h)
cell.font = Font(name="Calibri", bold=True)
cell.fill = PatternFill("solid", fgColor=GREEN)
cell.border = border
cell.alignment = Alignment(horizontal="center", vertical="center")
for row in rows:
r = ws.max_row + 1
vals = [row["date"], _vtrans(row["skill"]), _vtrans(row["template"]),
row["description"], row.get("by") or ""]
for c, v in enumerate(vals, start=1):
cell = ws.cell(r, c, v)
cell.border = border
cell.alignment = Alignment(vertical="center", wrap_text=(c == 4), horizontal="left")
for col, w in zip("ABCDE", (18, 16, 18, 50, 22)):
ws.column_dimensions[col].width = w
ws.freeze_panes = "A2"
return True
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, ""))
add_version_history(wb, typeid) # Sheet 2: cumulative version history (if any)
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()