first commit
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import argparse
|
||||
from functools import partial
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
from pasbdc_word_library import WordDocumentBuilder, PageConfig #pyright:ignore
|
||||
|
||||
def parse_and_organize_images(source_folder):
|
||||
"""
|
||||
Parses filenames and groups them by Center and Fiscal Year,
|
||||
keyed by the specific chart tag.
|
||||
"""
|
||||
source_path = Path(source_folder)
|
||||
|
||||
# Structure: organization[center][fiscal_year][chart_tag] = full_path
|
||||
# We change the inner default from 'list' to 'dict'
|
||||
organization = defaultdict(lambda: defaultdict(dict))
|
||||
|
||||
for image_path in source_path.glob('*.png'):
|
||||
filename_stem = image_path.stem
|
||||
parts = filename_stem.split('_')
|
||||
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
center = parts[0]
|
||||
fiscal_year = parts[-1]
|
||||
|
||||
# The key you will use to access this specific file later
|
||||
chart_tag = "_".join(parts[1:-1])
|
||||
|
||||
# Map the tag directly to the path
|
||||
organization[center][fiscal_year][chart_tag] = str(image_path)
|
||||
|
||||
return organization
|
||||
|
||||
def single_year_nbs_page(
|
||||
builder: WordDocumentBuilder,
|
||||
pie_chart_path:str,
|
||||
grouped_bar_path:str,
|
||||
center_name:str,
|
||||
fiscal_year_tag:str,
|
||||
add_documentation_note:bool
|
||||
):
|
||||
# 1. Setup Margins
|
||||
for section in builder.doc.sections:
|
||||
section.top_margin = Inches(0.5)
|
||||
section.bottom_margin = Inches(0.5)
|
||||
section.left_margin = Inches(0.5)
|
||||
section.right_margin = Inches(0.5)
|
||||
|
||||
# 2. Add Title
|
||||
title = builder.doc.add_heading(f'{builder.current_section}. {center_name} New Business Starts Attribution Analysis {fiscal_year_tag}', level=1)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER #pyright:ignore
|
||||
|
||||
# 3. Create a 2x2 Table for Side-by-Side Layout
|
||||
# Row 0 = Images, Row 1 = Captions
|
||||
table = builder.doc.add_table(rows=2, cols=2)
|
||||
table.autofit = False
|
||||
|
||||
# --- LEFT COLUMN (Pie Chart) ---
|
||||
|
||||
# Image (Row 0, Col 0)
|
||||
cell_pie_img = table.cell(0, 0)
|
||||
p_pie = cell_pie_img.paragraphs[0]
|
||||
p_pie.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
# Width set to 3.2 inches to ensure two fit on a 7.5 inch wide printable area
|
||||
p_pie.add_run().add_picture(pie_chart_path, width=Inches(3.2))
|
||||
|
||||
# Caption (Row 1, Col 0)
|
||||
cell_pie_cap = table.cell(1, 0)
|
||||
p_pie_cap = cell_pie_cap.paragraphs[0]
|
||||
p_pie_cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
builder.figure_number += 1
|
||||
p_pie_cap.add_run(f"Figure {builder.current_section}.{builder.figure_number} shows the proportion of new business start milestones that fell into each documentation level").bold = True
|
||||
# --- RIGHT COLUMN (Bar Chart) ---
|
||||
|
||||
# Image (Row 0, Col 1)
|
||||
cell_bar_img = table.cell(0, 1)
|
||||
p_bar = cell_bar_img.paragraphs[0]
|
||||
p_bar.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p_bar.add_run().add_picture(grouped_bar_path, width=Inches(3.2))
|
||||
|
||||
# Caption (Row 1, Col 1)
|
||||
cell_bar_cap = table.cell(1, 1)
|
||||
p_bar_cap = cell_bar_cap.paragraphs[0]
|
||||
p_bar_cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
builder.figure_number += 1
|
||||
p_bar_cap.add_run(f"Figure {builder.current_section}.{builder.figure_number} shows how many of each documentation level were a part of each attribution source").bold = True
|
||||
|
||||
# 4. Add Spacing after table
|
||||
builder.doc.add_paragraph()
|
||||
|
||||
if add_documentation_note:
|
||||
# 6. Add the "NOTE" section (Definitions)
|
||||
builder.doc.add_heading("NOTE: Data is based on business start impact date and new business start milestone records on the client file, extracted as of 1/27/26.", level=4)
|
||||
builder.doc.add_heading('In situations where multiple milestone records existed for a client, a de-duplication / merger script was utilized for data analysis (no changes in NS).The script created some unique combination types (e.g., "Email from Client,Requested on Ecenter"); these combined types were left as separate categories to more easily identify potential data entry training opportunities.', level=4)
|
||||
builder.doc.add_heading("NOTE 2: Documentation levels were determined as follows:", level=4)
|
||||
|
||||
note_paragraph = builder.doc.add_paragraph()
|
||||
note_paragraph.add_run("No documentation (red) - Will NOT be submitted to Nexus\n").bold = True
|
||||
note_paragraph.add_run("\t1. Attribution Source was blank, OR \n\t2. Attribution source was 'Requested on eCenter'")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
note_paragraph.add_run("Affirmation Missing (yellow) - Will NOT be submitted to Nexus\n").bold = True
|
||||
note_paragraph.add_run("\t1. Attribution source field is a non-blank, non-'Requested on eCenter' attribution source, BUT\n\t2. Affirmation Statement was blank")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
note_paragraph.add_run("Documented (green) - Will be submitted to Nexus as long as 'Director Verified' is checked\n").bold = True
|
||||
note_paragraph.add_run("\t1. There is a non-blank, non-'Requested on eCenter' attribution source AND\n\t2. Affirmation Statement was non-blank")
|
||||
note_paragraph.add_run("\n")
|
||||
note_paragraph.add_run("\tNOTE: If the attribution source is eCenter, then no value is required in the affirmation column.")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
def single_year_funding_page(
|
||||
builder: WordDocumentBuilder,
|
||||
pie_chart_path:str,
|
||||
grouped_bar_path:str,
|
||||
center_name:str,
|
||||
fiscal_year_tag:str,
|
||||
add_documentation_note:bool
|
||||
):
|
||||
# 1. Setup Margins
|
||||
for section in builder.doc.sections:
|
||||
section.top_margin = Inches(0.5)
|
||||
section.bottom_margin = Inches(0.5)
|
||||
section.left_margin = Inches(0.5)
|
||||
section.right_margin = Inches(0.5)
|
||||
|
||||
# 2. Add Title
|
||||
title = builder.doc.add_heading(f'{builder.current_section}. {center_name} Capital Transaction Attribution Analysis {fiscal_year_tag}', level=1)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER #pyright:ignore
|
||||
|
||||
# 3. Create a 2x2 Table for Side-by-Side Layout
|
||||
# Row 0 = Images, Row 1 = Captions
|
||||
table = builder.doc.add_table(rows=2, cols=2)
|
||||
table.autofit = False
|
||||
|
||||
# --- LEFT COLUMN (Pie Chart) ---
|
||||
|
||||
# Image (Row 0, Col 0)
|
||||
cell_pie_img = table.cell(0, 0)
|
||||
p_pie = cell_pie_img.paragraphs[0]
|
||||
p_pie.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
# Width set to 3.2 inches to ensure two fit on a 7.5 inch wide printable area
|
||||
p_pie.add_run().add_picture(pie_chart_path, width=Inches(3.2))
|
||||
|
||||
# Caption (Row 1, Col 0)
|
||||
cell_pie_cap = table.cell(1, 0)
|
||||
p_pie_cap = cell_pie_cap.paragraphs[0]
|
||||
p_pie_cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
builder.figure_number += 1
|
||||
p_pie_cap.add_run(f"Figure {builder.current_section}.{builder.figure_number} shows the proportion of funding milestones that fell into each documentation level").bold = True
|
||||
# --- RIGHT COLUMN (Bar Chart) ---
|
||||
|
||||
# Image (Row 0, Col 1)
|
||||
cell_bar_img = table.cell(0, 1)
|
||||
p_bar = cell_bar_img.paragraphs[0]
|
||||
p_bar.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
p_bar.add_run().add_picture(grouped_bar_path, width=Inches(3.2))
|
||||
|
||||
# Caption (Row 1, Col 1)
|
||||
cell_bar_cap = table.cell(1, 1)
|
||||
p_bar_cap = cell_bar_cap.paragraphs[0]
|
||||
p_bar_cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
builder.figure_number += 1
|
||||
p_bar_cap.add_run(f"Figure {builder.current_section}.{builder.figure_number} shows how many of each documentation level were a part of each attribution source").bold = True
|
||||
|
||||
# 4. Add Spacing after table
|
||||
builder.doc.add_paragraph()
|
||||
|
||||
if add_documentation_note:
|
||||
builder.doc.add_heading("NOTE: Data is based on those impacts showing up in the q1 scorecard as of 1/27/26", level=4)
|
||||
builder.doc.add_heading("NOTE 2: Documentation levels were determined as follows:", level=4)
|
||||
|
||||
note_paragraph = builder.doc.add_paragraph()
|
||||
note_paragraph.add_run("No documentation (red) - Will NOT be submitted to Nexus\n").bold = True
|
||||
note_paragraph.add_run("\t1. Attribution Source was blank, OR \n\t2. Attribution source was 'Requested on eCenter'")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
note_paragraph.add_run("Affirmation Missing (yellow) - Will NOT be submitted to Nexus\n").bold = True
|
||||
note_paragraph.add_run("\t1. Attribution source field is a non-blank, non-'Requested on eCenter' attribution source, BUT\n\t2. Affirmation Statement was blank")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
note_paragraph.add_run("Documented (green) - Will be submitted to Nexus as long as 'Director Verified' is checked\n").bold = True
|
||||
note_paragraph.add_run("\t1. There is a non-blank, non-'Requested on eCenter' attribution source AND\n\t2. Affirmation Statement was non-blank")
|
||||
note_paragraph.add_run("\n")
|
||||
note_paragraph.add_run("\tNOTE: If the attribution source is eCenter, then no value is required in the affirmation column.")
|
||||
note_paragraph.add_run("\n\n")
|
||||
|
||||
def process_documents(organization_data, out_path):
|
||||
network_wide_nbs = []
|
||||
network_wide_funding = []
|
||||
is_first_network_funding_page = True
|
||||
is_first_network_nbs_page = True
|
||||
for fiscal_year, chart_map in sorted(organization_data["network"].items()):
|
||||
path_funding_grouped = chart_map.get('funding_attribution_grouped')
|
||||
if not path_funding_grouped:
|
||||
print(f" WARNING: Missing 'funding_attribution_grouped' for network wide fy {fiscal_year}")
|
||||
else:
|
||||
print(path_funding_grouped)
|
||||
|
||||
path_funding_pie = chart_map.get('funding_attribution_pie')
|
||||
if not path_funding_pie:
|
||||
print(f" WARNING: Missing 'funding_attribution_pie' for network wide fy {fiscal_year}")
|
||||
else:
|
||||
print(path_funding_pie)
|
||||
|
||||
path_nbs_grouped = chart_map.get('nbs_attribution_grouped')
|
||||
if not path_nbs_grouped:
|
||||
print(f" WARNING: Missing 'nbs_attribution_grouped' for network wide fy {fiscal_year}")
|
||||
else:
|
||||
print(path_nbs_grouped)
|
||||
|
||||
path_nbs_pie = chart_map.get('nbs_attribution_pie')
|
||||
if not path_nbs_pie:
|
||||
print(f" WARNING: Missing 'nbs_attribution_pie' for network wide fy {fiscal_year}")
|
||||
else:
|
||||
print(path_nbs_pie)
|
||||
|
||||
if path_nbs_pie and path_nbs_grouped:
|
||||
nbs_page = partial(single_year_nbs_page,
|
||||
pie_chart_path=path_nbs_pie,
|
||||
grouped_bar_path=path_nbs_grouped,
|
||||
center_name="Network Wide",
|
||||
fiscal_year_tag=fiscal_year,
|
||||
add_documentation_note=True if is_first_network_nbs_page else False
|
||||
)
|
||||
network_wide_nbs.append(PageConfig(page_function=nbs_page, add_page_break=True if is_first_network_nbs_page else False))
|
||||
if is_first_network_nbs_page:
|
||||
is_first_network_nbs_page = False
|
||||
else:
|
||||
print(f" SKIPPING: Network Wide NBS page for {fiscal_year} due to missing graphs.")
|
||||
|
||||
if path_funding_pie and path_funding_grouped:
|
||||
funding_page = partial(single_year_funding_page,
|
||||
pie_chart_path=path_funding_pie,
|
||||
grouped_bar_path=path_funding_grouped,
|
||||
center_name="Network Wide",
|
||||
fiscal_year_tag=fiscal_year,
|
||||
add_documentation_note=True if is_first_network_funding_page else False
|
||||
)
|
||||
network_wide_funding.append(PageConfig(page_function=funding_page, add_page_break=True if is_first_network_funding_page else False))
|
||||
if is_first_network_funding_page:
|
||||
is_first_network_funding_page = False
|
||||
else:
|
||||
print(f" SKIPPING: Network Wide Funding page for {fiscal_year} due to missing graphs.")
|
||||
|
||||
nbs_pages = []
|
||||
funding_pages = []
|
||||
for center, fy_groups in sorted(organization_data.items()):
|
||||
if center == "network":
|
||||
continue
|
||||
# Used to store completed PageConfig objects
|
||||
center_nbs_pages = []
|
||||
# Used to tell the script whether to place the documentation level notes or not
|
||||
is_first_nbs_page = True
|
||||
|
||||
center_funding_pages = []
|
||||
is_first_funding_page = True
|
||||
|
||||
for fy, chart_map in sorted(fy_groups.items()):
|
||||
|
||||
print(f"--- Creating Doc for {center} {fy} ---")
|
||||
|
||||
# Create the funding pages
|
||||
path_funding_grouped = chart_map.get('funding_attribution_grouped')
|
||||
if not path_funding_grouped:
|
||||
print(f" WARNING: Missing 'funding_attribution_grouped' for {center}")
|
||||
else:
|
||||
print(path_funding_grouped)
|
||||
|
||||
# Example: Getting the 'pie' chart
|
||||
path_funding_pie = chart_map.get('funding_attribution_pie')
|
||||
if not path_funding_pie:
|
||||
print(f" WARNING: Missing 'funding_attribution_pie' for {center}")
|
||||
else:
|
||||
print(path_funding_pie)
|
||||
|
||||
# Create the new busiuness start pages
|
||||
path_nbs_grouped = chart_map.get('nbs_attribution_grouped')
|
||||
if not path_nbs_grouped:
|
||||
print(f" WARNING: Missing 'nbs_attribution_grouped' for {center}")
|
||||
else:
|
||||
print(path_nbs_grouped)
|
||||
|
||||
# Example: Getting the 'pie' chart
|
||||
path_nbs_pie = chart_map.get('nbs_attribution_pie')
|
||||
if not path_nbs_pie:
|
||||
print(f" WARNING: Missing 'nbs_attribution_pie' for {center}")
|
||||
else:
|
||||
print(path_nbs_pie)
|
||||
|
||||
if path_nbs_pie and path_nbs_grouped:
|
||||
nbs_page = partial(single_year_nbs_page,
|
||||
pie_chart_path=path_nbs_pie,
|
||||
grouped_bar_path=path_nbs_grouped,
|
||||
center_name=center,
|
||||
fiscal_year_tag=fy,
|
||||
add_documentation_note=True if is_first_nbs_page else False
|
||||
)
|
||||
|
||||
nbs_pages.append(PageConfig(page_function=nbs_page, add_page_break=True if is_first_nbs_page else False))
|
||||
center_nbs_pages.append(PageConfig(page_function=nbs_page, add_page_break=True if is_first_nbs_page else False))
|
||||
if is_first_nbs_page:
|
||||
is_first_nbs_page = False
|
||||
else:
|
||||
print(f" SKIPPING: NBS page for {center} {fy} due to missing graphs.")
|
||||
|
||||
if path_funding_pie and path_funding_grouped:
|
||||
funding_page = partial(single_year_funding_page,
|
||||
pie_chart_path=path_funding_pie,
|
||||
grouped_bar_path=path_funding_grouped,
|
||||
center_name=center,
|
||||
fiscal_year_tag=fy,
|
||||
add_documentation_note=True if is_first_funding_page else False
|
||||
)
|
||||
|
||||
funding_pages.append(PageConfig(page_function=funding_page, add_page_break=True if is_first_funding_page else False))
|
||||
center_funding_pages.append(PageConfig(page_function=funding_page, add_page_break=True if is_first_funding_page else False))
|
||||
if is_first_funding_page:
|
||||
is_first_funding_page = False
|
||||
else:
|
||||
print(f" SKIPPING: Funding page for {center} {fy} due to missing graphs.")
|
||||
|
||||
|
||||
center_builder = WordDocumentBuilder()
|
||||
center_builder.create_document(
|
||||
center_nbs_pages + network_wide_nbs + center_funding_pages + network_wide_funding,
|
||||
os.path.join(out_path, f"{center}_MilestoneAttributionStudy.docx"),
|
||||
)
|
||||
|
||||
master_builder = WordDocumentBuilder()
|
||||
master_builder.create_document(
|
||||
nbs_pages + network_wide_nbs + funding_pages + network_wide_funding,
|
||||
os.path.join(out_path, f"AllCenters_MilestoneAttributionStudy.docx")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--images",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The folder containing all of the report images for the milestones report")
|
||||
|
||||
parser.add_argument("--out",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The folder to place the generated reports into")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.out):
|
||||
os.makedirs(args.out)
|
||||
|
||||
# Run the parsing logic
|
||||
grouped_image_data = parse_and_organize_images(args.images)
|
||||
# print(grouped_image_data)
|
||||
# Run the document generation loops
|
||||
process_documents(grouped_image_data, args.out)
|
||||
Reference in New Issue
Block a user