Files
dev-process/tools/build_word_template.py
T
Stefan Lohmaier 6e458ae76f Initial commit: slohmaier Dev Process v1.0
ASPICE 4.0 / ISO 26262 Entwicklungsprozess fuer kleine Teams.

Inhalte:
- README mit hybrider Format-Strategie (Word + Markdown)
- Toolstack (Gitea, Doorstop, Cppcheck, gcov, CppUTest, pandoc)
- Markdown-Vorlagen fuer Requirements + Architektur (SA, SWA)
- Markdown-Vorlagen fuer formelle Dokumente (PID, PM-Plan, QA-Plan,
  SWE-Plan, Test-Plan, Reviews, Non-Conformity, MISRA Permits/Records)
- Word-Master-Template (slohmaier-doc-template.docx) mit ISO-9001-
  konformer Document Control, Formatvorlagen, Auto-Verzeichnissen
- Build-Scripts (build_word_template.py, generate_word_vorlagen.sh)
- gitea-aspice-setup.md, V-Modell-Infografik
2026-05-11 13:40:51 -07:00

584 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Build the neutral slohmaier Word document template.
Output: vorlagen-word/slohmaier-doc-template.docx
The template is ISO 9001 / ASPICE-friendly:
- Cover page with project, doc-ID, version, classification
- Document Control (Approvals, Revision History, Distribution)
- Inhaltsverzeichnis / Abbildungsverzeichnis / Tabellenverzeichnis (auto-fields)
- Custom styles: Heading 1-4, Body, Caption, Code, Note, Warning, Requirement
- Header/Footer with project + doc-ID + classification + page
Run: python3 tools/build_word_template.py
"""
from __future__ import annotations
import sys
from pathlib import Path
from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
# ---------------------------------------------------------------------------
# OXML helpers
# ---------------------------------------------------------------------------
def _qn_set(elem, attr, value):
elem.set(qn(attr), value)
def add_field(paragraph, instr_text, default_text="<Feld aktualisieren mit F9>"):
"""Insert a Word field (e.g. TOC, PAGE) into a paragraph."""
run = paragraph.add_run()
r_elem = run._r
fld_begin = OxmlElement("w:fldChar")
_qn_set(fld_begin, "w:fldCharType", "begin")
r_elem.append(fld_begin)
instr = OxmlElement("w:instrText")
_qn_set(instr, "xml:space", "preserve")
instr.text = instr_text
r_elem.append(instr)
fld_sep = OxmlElement("w:fldChar")
_qn_set(fld_sep, "w:fldCharType", "separate")
r_elem.append(fld_sep)
default_t = OxmlElement("w:t")
default_t.text = default_text
r_elem.append(default_t)
fld_end = OxmlElement("w:fldChar")
_qn_set(fld_end, "w:fldCharType", "end")
r_elem.append(fld_end)
def set_cell_shading(cell, fill_hex):
tc_pr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
_qn_set(shd, "w:val", "clear")
_qn_set(shd, "w:color", "auto")
_qn_set(shd, "w:fill", fill_hex)
tc_pr.append(shd)
def set_cell_borders(cell, color="808080", size=4):
tc_pr = cell._tc.get_or_add_tcPr()
tc_borders = OxmlElement("w:tcBorders")
for edge in ("top", "left", "bottom", "right"):
b = OxmlElement(f"w:{edge}")
_qn_set(b, "w:val", "single")
_qn_set(b, "w:sz", str(size))
_qn_set(b, "w:space", "0")
_qn_set(b, "w:color", color)
tc_borders.append(b)
tc_pr.append(tc_borders)
def set_paragraph_border(paragraph, color="808080", size=12, side="left"):
p_pr = paragraph._p.get_or_add_pPr()
p_bdr = OxmlElement("w:pBdr")
b = OxmlElement(f"w:{side}")
_qn_set(b, "w:val", "single")
_qn_set(b, "w:sz", str(size))
_qn_set(b, "w:space", "8")
_qn_set(b, "w:color", color)
p_bdr.append(b)
p_pr.append(p_bdr)
def set_paragraph_shading(paragraph, fill_hex):
p_pr = paragraph._p.get_or_add_pPr()
shd = OxmlElement("w:shd")
_qn_set(shd, "w:val", "clear")
_qn_set(shd, "w:color", "auto")
_qn_set(shd, "w:fill", fill_hex)
p_pr.append(shd)
# ---------------------------------------------------------------------------
# Styles
# ---------------------------------------------------------------------------
NEUTRAL_GREY = "595959"
SOFT_GREY = "808080"
LIGHT_GREY = "F2F2F2"
ACCENT_DARK = "1F3864"
NOTE_BG = "E7F0FA"
NOTE_BORDER = "2E74B5"
WARN_BG = "FFF4E5"
WARN_BORDER = "C55A11"
REQ_BG = "EAEAEA"
REQ_BORDER = "404040"
def configure_styles(doc):
styles = doc.styles
# --- Normal / Body Text ---
normal = styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
normal.font.color.rgb = RGBColor(0x20, 0x20, 0x20)
normal.paragraph_format.space_after = Pt(6)
normal.paragraph_format.line_spacing = 1.15
# --- Headings ---
heading_specs = [
("Heading 1", 18, True, RGBColor(0x1F, 0x38, 0x64), 18, 6),
("Heading 2", 14, True, RGBColor(0x1F, 0x38, 0x64), 12, 6),
("Heading 3", 12, True, RGBColor(0x40, 0x40, 0x40), 8, 4),
("Heading 4", 11, True, RGBColor(0x40, 0x40, 0x40), 6, 4),
]
for name, size, bold, color, before, after in heading_specs:
s = styles[name]
s.font.name = "Calibri"
s.font.size = Pt(size)
s.font.bold = bold
s.font.color.rgb = color
s.paragraph_format.space_before = Pt(before)
s.paragraph_format.space_after = Pt(after)
s.paragraph_format.keep_with_next = True
# --- Title ---
title = styles["Title"]
title.font.name = "Calibri"
title.font.size = Pt(36)
title.font.bold = True
title.font.color.rgb = RGBColor(0x1F, 0x38, 0x64)
# --- Subtitle ---
subtitle = styles["Subtitle"]
subtitle.font.name = "Calibri"
subtitle.font.size = Pt(16)
subtitle.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Caption (figure/table caption) ---
caption = styles["Caption"]
caption.font.name = "Calibri"
caption.font.size = Pt(10)
caption.font.italic = True
caption.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Code (custom paragraph style) ---
if "Code" not in [s.name for s in styles]:
code = styles.add_style("Code", WD_STYLE_TYPE.PARAGRAPH)
code.base_style = styles["Normal"]
code.font.name = "Consolas"
code.font.size = Pt(10)
code.paragraph_format.left_indent = Cm(0.5)
code.paragraph_format.space_before = Pt(4)
code.paragraph_format.space_after = Pt(4)
# --- Note (custom paragraph style) ---
if "Note" not in [s.name for s in styles]:
note = styles.add_style("Note", WD_STYLE_TYPE.PARAGRAPH)
note.base_style = styles["Normal"]
note.font.name = "Calibri"
note.font.size = Pt(10)
note.paragraph_format.left_indent = Cm(0.4)
note.paragraph_format.space_before = Pt(6)
note.paragraph_format.space_after = Pt(6)
# --- Warning ---
if "Warning" not in [s.name for s in styles]:
warn = styles.add_style("Warning", WD_STYLE_TYPE.PARAGRAPH)
warn.base_style = styles["Normal"]
warn.font.name = "Calibri"
warn.font.size = Pt(10)
warn.font.bold = True
warn.paragraph_format.left_indent = Cm(0.4)
# --- Requirement Box ---
if "Requirement" not in [s.name for s in styles]:
req = styles.add_style("Requirement", WD_STYLE_TYPE.PARAGRAPH)
req.base_style = styles["Normal"]
req.font.name = "Calibri"
req.font.size = Pt(11)
req.paragraph_format.left_indent = Cm(0.4)
req.paragraph_format.space_before = Pt(6)
req.paragraph_format.space_after = Pt(6)
# ---------------------------------------------------------------------------
# Page setup, header/footer
# ---------------------------------------------------------------------------
def setup_page(doc):
for section in doc.sections:
section.top_margin = Cm(2.5)
section.bottom_margin = Cm(2.5)
section.left_margin = Cm(2.5)
section.right_margin = Cm(2.5)
section.header_distance = Cm(1.25)
section.footer_distance = Cm(1.25)
def build_header_footer(doc, doc_id_placeholder="<DOC-ID>", classification="VERTRAULICH"):
section = doc.sections[0]
section.different_first_page_header_footer = True
# --- Default header (skipped on cover page) ---
header = section.header
header_para = header.paragraphs[0]
header_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
tabs = header_para.paragraph_format.tab_stops
tabs.add_tab_stop(Cm(8), WD_ALIGN_PARAGRAPH.CENTER)
tabs.add_tab_stop(Cm(16), WD_ALIGN_PARAGRAPH.RIGHT)
r1 = header_para.add_run("<PROJEKT>")
r1.font.size = Pt(9)
r1.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
header_para.add_run("\t")
r2 = header_para.add_run("<DOKUMENT-TITEL>")
r2.font.size = Pt(9)
r2.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
r2.bold = True
header_para.add_run("\t")
r3 = header_para.add_run(doc_id_placeholder)
r3.font.size = Pt(9)
r3.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
# --- Default footer (skipped on cover page) ---
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
f_tabs = footer_para.paragraph_format.tab_stops
f_tabs.add_tab_stop(Cm(8), WD_ALIGN_PARAGRAPH.CENTER)
f_tabs.add_tab_stop(Cm(16), WD_ALIGN_PARAGRAPH.RIGHT)
fr1 = footer_para.add_run("© slohmaier.com")
fr1.font.size = Pt(9)
fr1.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
footer_para.add_run("\t")
fr2 = footer_para.add_run(classification)
fr2.font.size = Pt(9)
fr2.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
fr2.bold = True
footer_para.add_run("\t")
fr3 = footer_para.add_run("Seite ")
fr3.font.size = Pt(9)
fr3.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
add_field(footer_para, "PAGE", "1")
fr4 = footer_para.add_run(" / ")
fr4.font.size = Pt(9)
fr4.font.color.rgb = RGBColor(0x59, 0x59, 0x59)
add_field(footer_para, "NUMPAGES", "1")
# --- First-page header/footer (cover): empty ---
fp_header = section.first_page_header
fp_header.paragraphs[0].text = ""
fp_footer = section.first_page_footer
fp_footer.paragraphs[0].text = ""
# ---------------------------------------------------------------------------
# Content
# ---------------------------------------------------------------------------
def add_cover_page(doc):
# Top logo placeholder
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
r = p.add_run("[LOGO]")
r.font.size = Pt(10)
r.font.color.rgb = RGBColor(0x80, 0x80, 0x80)
# Vertical space
for _ in range(8):
doc.add_paragraph()
# Classification banner
cls_p = doc.add_paragraph()
cls_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
cls_r = cls_p.add_run("VERTRAULICH")
cls_r.font.size = Pt(11)
cls_r.font.bold = True
cls_r.font.color.rgb = RGBColor(0xC5, 0x5A, 0x11)
# Title
title_p = doc.add_paragraph(style="Title")
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
title_p.add_run("<Dokument-Titel>")
# Subtitle
sub_p = doc.add_paragraph(style="Subtitle")
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
sub_p.add_run("<Untertitel oder Kurzbeschreibung>")
for _ in range(4):
doc.add_paragraph()
# Metadata block
meta_tbl = doc.add_table(rows=6, cols=2)
meta_tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
meta_tbl.autofit = False
meta_tbl.columns[0].width = Cm(4.5)
meta_tbl.columns[1].width = Cm(8.5)
meta_data = [
("Projekt", "<Projektname>"),
("Dokument-ID", "<DOC-ID>"),
("Version", "1.0"),
("Datum", "YYYY-MM-DD"),
("Status", "Entwurf"),
("Klassifikation", "Vertraulich"),
]
for i, (k, v) in enumerate(meta_data):
c1 = meta_tbl.cell(i, 0)
c1.width = Cm(4.5)
c1.text = ""
c1_p = c1.paragraphs[0]
c1_r = c1_p.add_run(k)
c1_r.bold = True
c1_r.font.size = Pt(11)
set_cell_shading(c1, LIGHT_GREY)
set_cell_borders(c1, SOFT_GREY)
c2 = meta_tbl.cell(i, 1)
c2.width = Cm(8.5)
c2.text = ""
c2_p = c2.paragraphs[0]
c2_r = c2_p.add_run(v)
c2_r.font.size = Pt(11)
set_cell_borders(c2, SOFT_GREY)
# Page break
doc.add_page_break()
def add_document_control(doc):
doc.add_heading("Dokumentenlenkung", level=1)
doc.add_paragraph(
"Diese Seite dokumentiert die formale Lenkung dieses Dokuments gemaess "
"ISO 9001. Aenderungen werden nur ueber den Freigabeprozess wirksam."
)
doc.add_heading("Freigaben", level=2)
appr_tbl = doc.add_table(rows=4, cols=4)
appr_tbl.style = "Light Grid Accent 1"
hdr = appr_tbl.rows[0].cells
for i, h in enumerate(["Rolle", "Name", "Unterschrift / Datum", "Bemerkung"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
roles = ["Erstellt von", "Geprueft von", "Freigegeben von"]
for i, role in enumerate(roles, start=1):
appr_tbl.rows[i].cells[0].text = role
for j in (1, 2, 3):
appr_tbl.rows[i].cells[j].text = ""
doc.add_heading("Aenderungshistorie", level=2)
rev_tbl = doc.add_table(rows=3, cols=4)
rev_tbl.style = "Light Grid Accent 1"
rev_hdr = rev_tbl.rows[0].cells
for i, h in enumerate(["Version", "Datum", "Aenderung", "Autor"]):
rev_hdr[i].text = ""
r = rev_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(rev_hdr[i], LIGHT_GREY)
rev_data = [
("0.1", "YYYY-MM-DD", "Initialer Entwurf", "<Autor>"),
("1.0", "YYYY-MM-DD", "Erstfreigabe", "<Autor>"),
]
for i, row in enumerate(rev_data, start=1):
for j, v in enumerate(row):
rev_tbl.rows[i].cells[j].text = v
doc.add_heading("Verteilerkreis", level=2)
dist_tbl = doc.add_table(rows=3, cols=3)
dist_tbl.style = "Light Grid Accent 1"
dist_hdr = dist_tbl.rows[0].cells
for i, h in enumerate(["Empfaenger", "Rolle", "Organisation"]):
dist_hdr[i].text = ""
r = dist_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(dist_hdr[i], LIGHT_GREY)
for i in range(1, 3):
for j in range(3):
dist_tbl.rows[i].cells[j].text = ""
doc.add_page_break()
def add_toc_pages(doc):
doc.add_heading("Inhaltsverzeichnis", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\o "1-3" \\h \\z \\u',
"Inhaltsverzeichnis aktualisieren: F9 (rechte Maustaste auf TOC > Felder aktualisieren)",
)
doc.add_page_break()
doc.add_heading("Abbildungsverzeichnis", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\h \\z \\c "Abbildung"',
"Abbildungsverzeichnis aktualisieren: F9",
)
doc.add_page_break()
doc.add_heading("Tabellenverzeichnis", level=1)
p = doc.add_paragraph()
add_field(
p,
'TOC \\h \\z \\c "Tabelle"',
"Tabellenverzeichnis aktualisieren: F9",
)
doc.add_page_break()
def add_abbreviations(doc):
doc.add_heading("Abkuerzungsverzeichnis", level=1)
tbl = doc.add_table(rows=4, cols=2)
tbl.style = "Light Grid Accent 1"
hdr = tbl.rows[0].cells
for i, h in enumerate(["Abkuerzung", "Bedeutung"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
examples = [
("ASIL", "Automotive Safety Integrity Level"),
("ECU", "Electronic Control Unit"),
("MISRA", "Motor Industry Software Reliability Association"),
]
for i, (k, v) in enumerate(examples, start=1):
tbl.rows[i].cells[0].text = k
tbl.rows[i].cells[1].text = v
doc.add_page_break()
def add_main_content(doc):
# Section 1
doc.add_heading("1. Einleitung", level=1)
doc.add_heading("1.1 Zweck", level=2)
doc.add_paragraph(
"<Kurzer Absatz: Wozu existiert dieses Dokument? Welche Frage beantwortet es?>"
)
doc.add_heading("1.2 Geltungsbereich", level=2)
doc.add_paragraph("<Welches Produkt, welches Projekt, welche Phase?>")
doc.add_heading("1.3 Definitionen", level=2)
doc.add_paragraph(
"<Spezielle Begriffe, die im Dokument verwendet werden. Allgemeine Abkuerzungen "
"siehe Abkuerzungsverzeichnis.>"
)
doc.add_heading("1.4 Referenzen", level=2)
ref_tbl = doc.add_table(rows=3, cols=3)
ref_tbl.style = "Light Grid Accent 1"
ref_hdr = ref_tbl.rows[0].cells
for i, h in enumerate(["ID", "Titel", "Version / Ort"]):
ref_hdr[i].text = ""
r = ref_hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(ref_hdr[i], LIGHT_GREY)
for i in range(1, 3):
for j in range(3):
ref_tbl.rows[i].cells[j].text = ""
# Section 2
doc.add_heading("2. Hauptinhalt", level=1)
doc.add_paragraph(
"<Hier beginnt der dokumenttyp-spezifische Inhalt. Die Vorlage liefert Struktur "
"und Formatvorlagen; konkrete Sektionen kommen aus der jeweiligen Dokumentart "
"(PID, QA-Plan, SWE-Plan, ...).>"
)
# Demonstrate styles
doc.add_heading("2.1 Beispiel: Formatvorlagen", level=2)
doc.add_paragraph(
"Body-Text in der Vorlage. Schriftart Calibri 11 mit 1,15-fachem Zeilenabstand."
)
code_p = doc.add_paragraph(style="Code")
code_p.add_run(
"// Beispiel-Code im Code-Stil (Consolas 10)\n"
"Status epb_apply(uint8_t force_percent);"
)
set_paragraph_shading(code_p, LIGHT_GREY)
note_p = doc.add_paragraph(style="Note")
note_p.add_run("HINWEIS: ").bold = True
note_p.add_run("Hinweis-Stil fuer ergaenzende Informationen.")
set_paragraph_border(note_p, NOTE_BORDER, size=18, side="left")
set_paragraph_shading(note_p, NOTE_BG)
warn_p = doc.add_paragraph(style="Warning")
warn_p.add_run("ACHTUNG: ").bold = True
warn_p.add_run("Warn-Stil fuer sicherheitsrelevante Hinweise.")
set_paragraph_border(warn_p, WARN_BORDER, size=18, side="left")
set_paragraph_shading(warn_p, WARN_BG)
req_p = doc.add_paragraph(style="Requirement")
req_p.add_run("REQ-001: ").bold = True
req_p.add_run(
"Requirement-Stil fuer in-line Anforderungen "
"(meist in Markdown via Doorstop, in Word fuer formelle Berichte)."
)
set_paragraph_border(req_p, REQ_BORDER, size=18, side="left")
set_paragraph_shading(req_p, REQ_BG)
doc.add_heading("2.2 Beispiel-Tabelle", level=2)
tbl = doc.add_table(rows=4, cols=3)
tbl.style = "Light Grid Accent 1"
hdr = tbl.rows[0].cells
for i, h in enumerate(["ID", "Beschreibung", "ASIL"]):
hdr[i].text = ""
r = hdr[i].paragraphs[0].add_run(h)
r.bold = True
set_cell_shading(hdr[i], LIGHT_GREY)
rows = [
("F-01", "Apply bei Fahrer-Anforderung", "D"),
("F-05", "Release bei Fahrer-Anforderung", "B"),
("F-10", "HMI: LED-Steuerung", "QM"),
]
for i, row in enumerate(rows, start=1):
for j, v in enumerate(row):
tbl.rows[i].cells[j].text = v
# Caption demo
cap_p = doc.add_paragraph(style="Caption")
cap_p.add_run("Tabelle 1: Beispiel-Anforderungen mit ASIL-Klassifikation")
# Section 3 — Anhang
doc.add_heading("3. Anhang", level=1)
doc.add_paragraph(
"<Anhaenge: Detail-Berechnungen, Skripte, Zusatz-Tabellen, Glossar.>"
)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def build_template(out_path: Path):
doc = Document()
setup_page(doc)
configure_styles(doc)
build_header_footer(doc)
add_cover_page(doc)
add_document_control(doc)
add_toc_pages(doc)
add_abbreviations(doc)
add_main_content(doc)
out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(out_path)
print(f"Wrote: {out_path}")
if __name__ == "__main__":
out = Path(sys.argv[1] if len(sys.argv) > 1 else
Path(__file__).resolve().parent.parent / "vorlagen-word" / "slohmaier-doc-template.docx")
build_template(out)