Etmam / ppt_fill.py
DAM20's picture
Upload 8 files
d755f9f verified
Raw
History Blame Contribute Delete
17.9 kB
# -*- coding: utf-8 -*-
from __future__ import annotations
from io import BytesIO
import re
import pandas as pd
from pptx import Presentation
from pptx.dml.color import RGBColor
def _norm_ar(s: str) -> str:
if not isinstance(s, str):
return ""
s = s.strip()
s = (
s.replace("أ", "ا")
.replace("إ", "ا")
.replace("آ", "ا")
.replace("ى", "ي")
.replace("ة", "ه")
)
s = re.sub(r"\s+", " ", s)
s = s.replace(" -", "-").replace("- ", "-")
return s
def _keywords(s: str) -> set[str]:
s = _norm_ar(s)
stop = {
"الاداره", "اداره", "الادارة",
"العامه", "عامه", "العامة",
"بلديه", "بلدية",
"امانه", "امانة",
"مدينه", "مدينة", "المدينه",
"منطقه", "منطقة", "المنطقه",
}
toks: list[str] = []
for t in s.split():
if not t:
continue
if t.startswith("لل") and len(t) > 2:
t = t[2:]
elif t.startswith("ال") and len(t) > 2:
t = t[2:]
elif t.startswith("ل") and len(t) > 2:
t = t[1:]
if t and t not in stop:
toks.append(t)
return set(toks)
def _find_main_table(slide):
tables = [sh.table for sh in slide.shapes if sh.has_table]
if not tables:
raise RuntimeError("لم يتم العثور على جدول في الشريحة.")
main = max(tables, key=lambda t: len(t.columns))
return main
def _detect_main_columns(table):
header = table.rows[0].cells
m = dict(admin=None, open=None, reopen=None, near=None, late=None, other=None)
for i, c in enumerate(header):
txt = c.text
tnorm = _norm_ar(txt)
if any(k in tnorm for k in ["الادارات", "الاداره", "الادارة"]):
m["admin"] = i
if "البلاغات المفتوحه" in tnorm or "البلاغات المفتوحة" in txt:
m["open"] = i
if "اعادة فتح" in tnorm or "المعاد فتحها" in tnorm:
m["reopen"] = i
if "قارب" in tnorm or "قارب على تجاوز" in tnorm:
m["near"] = i
if "المتأخره" in tnorm or "المتأخرة" in txt or "تجاوز sla" in tnorm:
m["late"] = i
if "مصادر اخرى" in tnorm or "مصادر أخرى" in txt:
m["other"] = i
if m["admin"] is None:
m["admin"] = 0
return m
def _make_shape_text_white(shape) -> None:
if not shape.has_text_frame:
return
for p in shape.text_frame.paragraphs:
for run in p.runs:
run.font.color.rgb = RGBColor(255, 255, 255)
def _make_cell_text_white(cell) -> None:
if not cell.text_frame:
return
for p in cell.text_frame.paragraphs:
for run in p.runs:
run.font.color.rgb = RGBColor(255, 255, 255)
def _fill_date_placeholder(prs: Presentation, closed_dt: pd.Timestamp) -> None:
weekday_map = {
0: "الاثنين",
1: "الثلاثاء",
2: "الأربعاء",
3: "الخميس",
4: "الجمعة",
5: "السبت",
6: "الأحد",
}
weekday_ar = weekday_map[closed_dt.weekday()]
date_str = closed_dt.strftime("%Y-%m-%d")
full_text = f"التحديث اليومي {weekday_ar} {date_str}"
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
txt = shape.text
if "{{DATE}}" not in txt and "{DATE}" not in txt:
continue
txt = txt.replace("{{DATE}}", full_text).replace("{DATE}", full_text)
shape.text = txt
_make_shape_text_white(shape)
def _fill_total_placeholders(
prs: Presentation,
open_total_all: int,
near_total_all: int,
late_total_all: int,
other_total_all: int,
) -> None:
repl = {
"{OPEN_TOTAL}": str(open_total_all),
"{{OPEN_TOTAL}}": str(open_total_all),
"{NEAR_SLA_TOTAL}": str(near_total_all),
"{{NEAR_SLA_TOTAL}}": str(near_total_all),
"{LATE_TOTAL}": str(late_total_all),
"{{LATE_TOTAL}}": str(late_total_all),
"{OTHER_TOTAL}": str(other_total_all),
"{{OTHER_TOTAL}}": str(other_total_all),
}
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
text = shape.text
changed = False
for token, val in repl.items():
if token in text:
text = text.replace(token, val)
changed = True
if changed:
shape.text = text
def _find_reopen_column(df: pd.DataFrame) -> str | None:
for col in df.columns:
s = _norm_ar(str(col))
if ("معلق" in s or "مغلق" in s) and ("فتح" in s):
return col
return None
def _find_sla_columns(df: pd.DataFrame):
near_col = None
late_col = None
for col in df.columns:
s = _norm_ar(str(col))
if "قارب" in s:
near_col = col
if "تجاوز" in s:
late_col = col
return near_col, late_col
def _match_admin_indices(admin_in_slide: str, all_admins: list[str]) -> list[str]:
s = _norm_ar(admin_in_slide)
kw_s = _keywords(s)
matches: list[str] = []
is_muni = ("بلديه" in s or "بلدية" in s)
is_south = "جنوب" in s
is_north = "شمال" in s
is_center = "وسط" in s
is_main_clean = (
("النظافه" in s or "النظافة" in s)
and ("اداره" in s or "الاداره" in s or "الادارة" in s)
and ("عامه" in s or "العامه" in s or "العامة" in s)
)
is_main_projects = (
("مشاريع" in s)
and ("اداره" in s or "الاداره" in s or "الادارة" in s)
and ("عامه" in s or "العامه" in s or "العامة" in s)
)
for name in all_admins:
n = _norm_ar(name)
kw_n = _keywords(n)
if is_muni and (is_south or is_north or is_center):
if is_south and "جنوب" in n and (
"تعمير" in n or ("رقابه" in n and "الخدمات" in n)
):
matches.append(name)
continue
if is_north and "شمال" in n and (
"تعمير" in n or ("رقابه" in n and "الخدمات" in n)
):
matches.append(name)
continue
if is_center and "وسط" in n and (
"تعمير" in n or ("رقابه" in n and "الخدمات" in n)
):
matches.append(name)
continue
continue
if is_main_clean:
if "النظافه" in n or "النظافة" in n:
matches.append(name)
continue
if is_main_projects:
if ("مشاريع" in n) and ("تنسيق" not in n) and (
"مدينة" in n or "المدينه" in n):
matches.append(name)
continue
if is_main_projects:
if "مشاريع" in n:
matches.append(name)
continue
if ("تشغيل" in s and "صيان" in s):
if ("تشغيل" in n and "صيان" in n) or ("اناره" in n) or ("انارة" in n):
matches.append(name)
continue
if ("الاصحاح" in s and "البيئي" in s):
if "الاصحاح" in n and "البيئي" in n:
matches.append(name)
continue
if n == s:
matches.append(name)
continue
if kw_n == kw_s or kw_n.issubset(kw_s) or kw_s.issubset(kw_n):
matches.append(name)
continue
seen = set()
ordered = []
for name in all_admins:
if name in matches and name not in seen:
ordered.append(name)
seen.add(name)
return ordered
def _aggregate_for_admin(admin_in_slide: str, df: pd.DataFrame) -> pd.Series:
if df is None or df.empty:
return pd.Series(0, index=df.columns if hasattr(df, "columns") else [])
df = df.copy()
df.index = df.index.astype(str)
if "الإجمالي الكلي" in df.index:
df = df.drop(index="الإجمالي الكلي")
all_admins = list(df.index)
idxs = _match_admin_indices(admin_in_slide, all_admins)
if not idxs:
return pd.Series(0, index=df.columns)
sub = df.loc[idxs]
if isinstance(sub, pd.Series):
return sub
return sub.sum(axis=0)
def _fill_main_table(
table,
p_open: pd.DataFrame,
p_sla: pd.DataFrame,
p_other: pd.DataFrame | None,
):
colmap = _detect_main_columns(table)
reopen_col = _find_reopen_column(p_open)
near_col, late_col = _find_sla_columns(p_sla)
total_open = total_reopen = total_near = total_late = total_other = 0
total_row_idx = None
n_rows = len(table.rows)
for r in range(1, n_rows):
cells = table.rows[r].cells
admin_txt = cells[colmap["admin"]].text.strip()
norm_admin = _norm_ar(admin_txt)
if not admin_txt:
continue
if "الاجمالي" in norm_admin:
total_row_idx = r
continue
vals_open = _aggregate_for_admin(admin_txt, p_open)
if reopen_col is not None:
reopen_val = int(vals_open.get(reopen_col, 0))
else:
reopen_val = 0
open_val = int(vals_open.sum() - reopen_val)
vals_sla = _aggregate_for_admin(admin_txt, p_sla)
near_val = int(vals_sla.get(near_col, 0)) if near_col else 0
late_val = int(vals_sla.get(late_col, 0)) if late_col else 0
if p_other is not None and not p_other.empty:
vals_other = _aggregate_for_admin(admin_txt, p_other)
other_val = int(vals_other.sum())
else:
other_val = 0
if colmap["open"] is not None:
cells[colmap["open"]].text = str(open_val)
if colmap["reopen"] is not None:
cells[colmap["reopen"]].text = str(reopen_val)
if colmap["near"] is not None:
cells[colmap["near"]].text = str(near_val)
if colmap["late"] is not None:
cells[colmap["late"]].text = str(late_val)
if colmap["other"] is not None:
cells[colmap["other"]].text = str(other_val)
total_open += open_val
total_reopen += reopen_val
total_near += near_val
total_late += late_val
total_other += other_val
if total_row_idx is not None:
cells = table.rows[total_row_idx].cells
if colmap["open"] is not None:
cells[colmap["open"]].text = str(total_open)
_make_cell_text_white(cells[colmap["open"]])
if colmap["reopen"] is not None:
cells[colmap["reopen"]].text = str(total_reopen)
_make_cell_text_white(cells[colmap["reopen"]])
if colmap["near"] is not None:
cells[colmap["near"]].text = str(total_near)
_make_cell_text_white(cells[colmap["near"]])
if colmap["late"] is not None:
cells[colmap["late"]].text = str(total_late)
_make_cell_text_white(cells[colmap["late"]])
if colmap["other"] is not None:
cells[colmap["other"]].text = str(total_other)
_make_cell_text_white(cells[colmap["other"]])
return total_open, total_near, total_late, total_other
def _fill_side_tables(
slide,
main_table,
p_open: pd.DataFrame,
p_sla: pd.DataFrame,
p_other: pd.DataFrame | None,
):
reopen_col = _find_reopen_column(p_open)
near_col, late_col = _find_sla_columns(p_sla)
for shape in slide.shapes:
if not shape.has_table:
continue
table = shape.table
if table is main_table:
continue
colmap = _detect_main_columns(table)
admin_idx = colmap.get("admin")
if admin_idx is None:
continue
n_rows = len(table.rows)
for r in range(1, n_rows):
cells = table.rows[r].cells
admin_txt = cells[admin_idx].text.strip()
if not admin_txt:
continue
if "الاجمالي" in _norm_ar(admin_txt):
continue
vals_open = _aggregate_for_admin(admin_txt, p_open)
if reopen_col is not None:
reopen_val = int(vals_open.get(reopen_col, 0))
else:
reopen_val = 0
open_val = int(vals_open.sum() - reopen_val)
vals_sla = _aggregate_for_admin(admin_txt, p_sla)
near_val = int(vals_sla.get(near_col, 0)) if near_col else 0
late_val = int(vals_sla.get(late_col, 0)) if late_col else 0
if p_other is not None and not p_other.empty:
vals_other = _aggregate_for_admin(admin_txt, p_other)
other_val = int(vals_other.sum())
else:
other_val = 0
if colmap["open"] is not None:
cells[colmap["open"]].text = str(open_val)
if colmap["reopen"] is not None:
cells[colmap["reopen"]].text = str(reopen_val)
if colmap["near"] is not None:
cells[colmap["near"]].text = str(near_val)
if colmap["late"] is not None:
cells[colmap["late"]].text = str(late_val)
if colmap["other"] is not None:
cells[colmap["other"]].text = str(other_val)
_CARD_PATTERN = re.compile(r"\{CARD_(OPEN|NEAR|LATE|OTHER)([^}]*)\}", re.DOTALL)
def _fill_left_cards(
prs: Presentation,
p_open: pd.DataFrame,
p_sla: pd.DataFrame,
p_other: pd.DataFrame | None,
):
reopen_col = _find_reopen_column(p_open)
near_col, late_col = _find_sla_columns(p_sla)
cache: dict[str, dict[str, int]] = {}
def get_metrics(admin_name: str) -> dict[str, int]:
admin_name = admin_name.strip()
if not admin_name:
return {"open": 0, "near": 0, "late": 0, "other": 0}
if admin_name in cache:
return cache[admin_name]
vals_open = _aggregate_for_admin(admin_name, p_open)
if reopen_col is not None:
reopen_val = int(vals_open.get(reopen_col, 0))
else:
reopen_val = 0
open_val = int(vals_open.sum() - reopen_val)
vals_sla = _aggregate_for_admin(admin_name, p_sla)
near_val = int(vals_sla.get(near_col, 0)) if near_col else 0
late_val = int(vals_sla.get(late_col, 0)) if late_col else 0
if p_other is not None and not p_other.empty:
vals_other = _aggregate_for_admin(admin_name, p_other)
other_val = int(vals_other.sum())
else:
other_val = 0
metrics = {
"open": open_val,
"near": near_val,
"late": late_val,
"other": other_val,
}
cache[admin_name] = metrics
return metrics
from pptx.enum.text import PP_ALIGN
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
txt = shape.text
if "CARD_" not in txt:
continue
def repl(match: re.Match) -> str:
kind = match.group(1).upper()
rest = match.group(2) or ""
admin = rest.replace("\n", " ")
admin = admin.replace(":", " ").replace("ـ", " ").replace("-", " ")
admin = admin.strip()
metrics = get_metrics(admin)
key_map = {
"OPEN": "open",
"NEAR": "near",
"LATE": "late",
"OTHER": "other",
}
return str(metrics.get(key_map[kind], 0))
new_txt = _CARD_PATTERN.sub(repl, txt)
if new_txt != txt:
shape.text = new_txt
tf = shape.text_frame
for p in tf.paragraphs:
p.alignment = PP_ALIGN.CENTER
for run in p.runs:
run.font.color.rgb = RGBColor(255, 255, 255)
def fill_ppt(
template_path: str,
p_open: pd.DataFrame,
p_sla: pd.DataFrame,
p_other: pd.DataFrame | None,
closed_dt: pd.Timestamp,
) -> BytesIO:
prs = Presentation(template_path)
slide = prs.slides[0]
main_table = _find_main_table(slide)
open_total_all, near_total_all, late_total_all, other_total_all = _fill_main_table(
main_table, p_open, p_sla, p_other
)
_fill_side_tables(slide, main_table, p_open, p_sla, p_other)
_fill_left_cards(prs, p_open, p_sla, p_other)
_fill_date_placeholder(prs, closed_dt)
_fill_total_placeholders(
prs,
open_total_all=open_total_all,
near_total_all=near_total_all,
late_total_all=late_total_all,
other_total_all=other_total_all,
)
out = BytesIO()
prs.save(out)
out.seek(0)
return out