151 lines
6.7 KiB
Python
Executable File
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()
|