"""Shared document styles for all Performance West DOCX generators. Import this module in every generator to ensure consistent typography, spacing, and formatting across all compliance documents. Usage: from scripts.document_gen.templates._styles import ( PW, apply_doc_defaults, add_body, add_heading, add_bullets, add_signature_block, add_page_numbers, add_horizontal_rule, ) All generators should call apply_doc_defaults(doc) first, then use the helper functions instead of building paragraphs manually. """ from __future__ import annotations import logging from typing import Optional LOG = logging.getLogger("document_gen.styles") try: from docx import Document from docx.shared import Pt, Inches, RGBColor, Emu from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn from docx.oxml import OxmlElement except ImportError: LOG.warning("python-docx not installed") Document = None # ═══════════════════════════════════════════════════════════════════════════ # Color palette # ═══════════════════════════════════════════════════════════════════════════ class PW: """Performance West brand colors and typography constants.""" NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None DARK_GRAY = RGBColor(0x37, 0x41, 0x51) if Document else None MEDIUM_GRAY = RGBColor(0x6B, 0x72, 0x80) if Document else None LIGHT_GRAY = RGBColor(0x9C, 0xA3, 0xAF) if Document else None BLACK = RGBColor(0x00, 0x00, 0x00) if Document else None GREEN = RGBColor(0x05, 0x96, 0x69) if Document else None RED = RGBColor(0xDC, 0x26, 0x26) if Document else None # Typography FONT_FAMILY = "Calibri" BODY_SIZE = Pt(9.5) if Document else None BODY_SMALL = Pt(8.5) if Document else None HEADING_1_SIZE = Pt(13) if Document else None HEADING_2_SIZE = Pt(11) if Document else None HEADING_3_SIZE = Pt(10) if Document else None TITLE_SIZE = Pt(15) if Document else None SUBTITLE_SIZE = Pt(10) if Document else None FOOTER_SIZE = Pt(7.5) if Document else None SIGNATURE_SIZE = Pt(9.5) if Document else None # Spacing BODY_AFTER = Pt(8) if Document else None HEADING_BEFORE = Pt(16) if Document else None HEADING_AFTER = Pt(6) if Document else None BULLET_AFTER = Pt(4) if Document else None LINE_SPACING = 1.15 # Margins MARGIN_TOP = Inches(0.9) if Document else None MARGIN_BOTTOM = Inches(0.9) if Document else None MARGIN_LEFT = Inches(1.1) if Document else None MARGIN_RIGHT = Inches(1.1) if Document else None # ═══════════════════════════════════════════════════════════════════════════ # Document setup # ═══════════════════════════════════════════════════════════════════════════ def apply_doc_defaults(doc, title: str = "", entity_name: str = "") -> None: """Apply standard margins, default font, and optional header/footer.""" for section in doc.sections: section.top_margin = PW.MARGIN_TOP section.bottom_margin = PW.MARGIN_BOTTOM section.left_margin = PW.MARGIN_LEFT section.right_margin = PW.MARGIN_RIGHT # Set default font on the document's style style = doc.styles["Normal"] style.font.name = PW.FONT_FAMILY style.font.size = PW.BODY_SIZE style.font.color.rgb = PW.DARK_GRAY style.paragraph_format.space_after = PW.BODY_AFTER style.paragraph_format.line_spacing = PW.LINE_SPACING def add_page_numbers(doc) -> None: """Add centered page numbers to the document footer.""" for section in doc.sections: footer = section.footer footer.is_linked_to_previous = False p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER # Page X run = p.add_run() run.font.size = PW.FOOTER_SIZE run.font.color.rgb = PW.LIGHT_GRAY run.font.name = PW.FONT_FAMILY fc_begin = OxmlElement("w:fldChar") fc_begin.set(qn("w:fldCharType"), "begin") run._element.append(fc_begin) r2 = p.add_run() r2.font.size = PW.FOOTER_SIZE r2.font.color.rgb = PW.LIGHT_GRAY instr = OxmlElement("w:instrText") instr.set(qn("xml:space"), "preserve") instr.text = " PAGE " r2._element.append(instr) r3 = p.add_run() fc_end = OxmlElement("w:fldChar") fc_end.set(qn("w:fldCharType"), "end") r3._element.append(fc_end) def add_pw_footer(doc, entity_name: str = "") -> None: """Add 'Prepared by Performance West Inc.' footer with page numbers.""" for section in doc.sections: footer = section.footer footer.is_linked_to_previous = False p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER # "Prepared by Performance West Inc." text run = p.add_run("Prepared by Performance West Inc., a regulatory compliance consulting firm") run.font.size = PW.FOOTER_SIZE run.font.color.rgb = PW.LIGHT_GRAY run.font.name = PW.FONT_FAMILY run.italic = True # Add separator and page number sep = p.add_run(" \u2014 Page ") sep.font.size = PW.FOOTER_SIZE sep.font.color.rgb = PW.LIGHT_GRAY sep.font.name = PW.FONT_FAMILY # Page number field fc_begin = OxmlElement("w:fldChar") fc_begin.set(qn("w:fldCharType"), "begin") r_page = p.add_run() r_page.font.size = PW.FOOTER_SIZE r_page.font.color.rgb = PW.LIGHT_GRAY r_page._element.append(fc_begin) r_instr = p.add_run() r_instr.font.size = PW.FOOTER_SIZE instr = OxmlElement("w:instrText") instr.set(qn("xml:space"), "preserve") instr.text = " PAGE " r_instr._element.append(instr) r_end = p.add_run() fc_end = OxmlElement("w:fldChar") fc_end.set(qn("w:fldCharType"), "end") r_end._element.append(fc_end) # ═══════════════════════════════════════════════════════════════════════════ # Content helpers # ═══════════════════════════════════════════════════════════════════════════ def add_title(doc, text: str, subtitle: str = "") -> None: """Add a document title (centered, navy, large).""" p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(2) run = p.add_run(text) run.font.size = PW.TITLE_SIZE run.bold = True run.font.color.rgb = PW.NAVY run.font.name = PW.FONT_FAMILY if subtitle: p2 = doc.add_paragraph() p2.alignment = WD_ALIGN_PARAGRAPH.CENTER p2.paragraph_format.space_after = Pt(12) run2 = p2.add_run(subtitle) run2.font.size = PW.SUBTITLE_SIZE run2.italic = True run2.font.color.rgb = PW.MEDIUM_GRAY run2.font.name = PW.FONT_FAMILY def add_heading(doc, text: str, level: int = 1) -> None: """Add a section heading (navy, bold).""" p = doc.add_paragraph() size = {1: PW.HEADING_1_SIZE, 2: PW.HEADING_2_SIZE, 3: PW.HEADING_3_SIZE}.get(level, PW.HEADING_2_SIZE) before = PW.HEADING_BEFORE if level == 1 else Pt(10) p.paragraph_format.space_before = before p.paragraph_format.space_after = PW.HEADING_AFTER run = p.add_run(text) run.bold = True run.font.size = size run.font.color.rgb = PW.NAVY run.font.name = PW.FONT_FAMILY def add_body(doc, text: str, bold: bool = False, italic: bool = False, size=None, color=None, alignment=None) -> None: """Add a body paragraph.""" p = doc.add_paragraph() p.paragraph_format.space_after = PW.BODY_AFTER p.paragraph_format.line_spacing = PW.LINE_SPACING if alignment: p.alignment = alignment run = p.add_run(text) run.font.size = size or PW.BODY_SIZE run.font.name = PW.FONT_FAMILY run.font.color.rgb = color or PW.DARK_GRAY run.bold = bold run.italic = italic def add_field_value(doc, label: str, value: str) -> None: """Add a label: value pair on one line.""" p = doc.add_paragraph() p.paragraph_format.space_after = Pt(3) p.paragraph_format.line_spacing = PW.LINE_SPACING run_label = p.add_run(f"{label}: ") run_label.font.size = PW.BODY_SIZE run_label.font.name = PW.FONT_FAMILY run_label.font.color.rgb = PW.MEDIUM_GRAY run_label.bold = False run_value = p.add_run(value) run_value.font.size = PW.BODY_SIZE run_value.font.name = PW.FONT_FAMILY run_value.font.color.rgb = PW.DARK_GRAY run_value.bold = True def add_bullets(doc, items: list[str], indent: float = 0.25) -> None: """Add a bulleted list.""" for item in items: p = doc.add_paragraph(style="List Bullet") p.paragraph_format.left_indent = Inches(indent) p.paragraph_format.space_after = PW.BULLET_AFTER p.paragraph_format.line_spacing = PW.LINE_SPACING p.clear() run = p.add_run(item) run.font.size = PW.BODY_SIZE run.font.name = PW.FONT_FAMILY run.font.color.rgb = PW.DARK_GRAY def add_horizontal_rule(doc) -> None: """Add a thin navy horizontal rule.""" p = doc.add_paragraph() p.paragraph_format.space_before = Pt(6) p.paragraph_format.space_after = Pt(6) pPr = p._p.get_or_add_pPr() pBdr = OxmlElement("w:pBdr") bottom = OxmlElement("w:bottom") bottom.set(qn("w:val"), "single") bottom.set(qn("w:sz"), "4") bottom.set(qn("w:space"), "1") bottom.set(qn("w:color"), "1A2744") pBdr.append(bottom) pPr.append(pBdr) def add_signature_block( doc, signer_name: str = "", signer_title: str = "", entity_name: str = "", date_str: str = "", include_perjury: bool = False, ) -> None: """Add a standardized signature block.""" if include_perjury: add_body(doc, ( "I declare under penalty of perjury under the laws of the " "United States of America that the foregoing is true and correct." ), italic=True, size=PW.BODY_SMALL) # Signature line add_body(doc, "", size=Pt(20)) # spacer p = doc.add_paragraph() p.paragraph_format.space_after = Pt(2) run = p.add_run("_" * 45) run.font.size = PW.SIGNATURE_SIZE run.font.name = PW.FONT_FAMILY run.font.color.rgb = PW.LIGHT_GRAY if signer_name: add_body(doc, signer_name, bold=True, size=PW.SIGNATURE_SIZE) if signer_title: add_body(doc, signer_title, size=PW.BODY_SMALL, color=PW.MEDIUM_GRAY) if entity_name: add_body(doc, entity_name, size=PW.BODY_SMALL, color=PW.MEDIUM_GRAY) if date_str: add_body(doc, f"Date: {date_str}", size=PW.BODY_SMALL, color=PW.MEDIUM_GRAY)