#!/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=""): """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="", 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("") r1.font.size = Pt(9) r1.font.color.rgb = RGBColor(0x59, 0x59, 0x59) header_para.add_run("\t") r2 = header_para.add_run("") 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("") # Subtitle sub_p = doc.add_paragraph(style="Subtitle") sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER sub_p.add_run("") 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", ""), ("Dokument-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", ""), ("1.0", "YYYY-MM-DD", "Erstfreigabe", ""), ] 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( "" ) doc.add_heading("1.2 Geltungsbereich", level=2) doc.add_paragraph("") doc.add_heading("1.3 Definitionen", level=2) doc.add_paragraph( "" ) 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( "" ) # 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( "" ) # --------------------------------------------------------------------------- # 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)