# -*- coding: utf-8 -*- """Build the Approval Email Alert deck from the List Views template.""" import re from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN SRC = r'C:\Users\HUAWEI\Downloads\NexusCRM-Private-List-Views.pptx' OUT = r'C:\Users\HUAWEI\AppData\Local\Temp\NexusCRM-Approval-Email-Notifications.pptx' prs = Presentation(SRC) slides = list(prs.slides) def set_para_text(tf, lines): for i, para in enumerate(tf.paragraphs): if i < len(lines): if para.runs: para.runs[0].text = lines[i] for extra in para.runs[1:]: extra._r.getparent().remove(extra._r) else: r = para.add_run(); r.text = lines[i] else: for run in para.runs: run.text = "" def edit(si, name, txt): for sh in slides[si].shapes: if sh.name == name and sh.has_text_frame: set_para_text(sh.text_frame, txt.split("\n")); return print(f" [MISS] s{si} {name}") M = { 0: [ ("Text 2", "Approval Email Alert"), ("Text 3", "Pick an email template per lifecycle event — Salesforce-style. A Flow under the hood; no engine change."), ("Text 5", "per-event picker"), ("Text 7", "74 templates"), ("Text 9", "L1 Flow"), ("Text 11", "L0 sugar"), ("Text 12", "Chen Du · 2026-07-01 · design + issue #3530 · pingcap-inc/nexuscrm-pingcap-pack"), ], 1: [ ("Text 1", "THE PROBLEM"), ("Text 2", "No Email Alert, and the picker is empty"), ("Text 7", "I want to pick an email template per approval step — like Salesforce. But there's no Email Alert option, and a Flow's Send Email step has no templates to choose."), ("Text 8", "— approval admin, staging"), ("Text 12", "Alert disabled"), ("Text 13", "backend fails it closed (cutover)"), ("Text 17", "Flow is the path"), ("Text 18", "email = a Flow's Send Email node"), ("Text 22", "Picker empty"), ("Text 23", "no templates seeded"), ("Text 27", "No per-step UX"), ("Text 28", "must hand-build a Flow"), ], 2: [ ("Text 2", "The gap, at a glance"), ("Text 5", "The engine is there (Flow + sendEmail); what's missing is templates and a per-event picker."), ], 3: [ ("Text 2", "Templates + a picker; a Flow under the hood"), ("Text 6", "1 · CATALOG"), ("Text 7", "74 email templates + 11 per-object folders (L2 seed)."), ("Text 10", "2 · CONVENIENCE"), ("Text 11", "L0 front-end: an “Email Alert” option\n+ pick a template per lifecycle event\n+ scaffolds a Flow (no backend arm)"), ("Text 14", "3 · FLOW"), ("Text 15", "The scaffolded Flow's Send Email node sends the template — the live, supported path."), ("Text 16", "Keystone: the sendEmail verb + Flow seam already exist (L0). We add content + ergonomics — not an engine arm:"), ("Text 18", "sendEmail verb"), ("Text 20", "Flow approval seam"), ("Text 22", "template catalog"), ("Text 24", "picker sugar"), ("Text 25", "L0 = Flow seam + sendEmail verb + front-end sugar · L2 = 74 templates + 11 folders · no backend engine arm · #3530"), ], 4: [ ("Text 1", "BEFORE / AFTER · THE PICKER"), ("Text 2", "From empty picker to per-event Email Alert"), ("Text 3", "Pick a template on Initial Submission, Final Approval, Final Rejection…"), ("Text 9", "Add Action — today"), ("Text 10", "✗ Action types: Field Update / Flow only\n✗ No “Email Alert” option\n✗ Flow Send Email picker: empty\n✗ Must hand-build a Flow to email"), ("Text 14", "Add Action — proposed"), ("Text 15", "✓ “Email Alert” option per lifecycle event\n✓ Pick a template from the catalog\n✓ Scaffolds a Flow under the hood\n✓ Runs on the live Flow path"), ], 5: [ ("Text 1", "BEFORE / AFTER · DYNAMIC TEXT"), ("Text 2", "Merge fields that resolve real data"), ("Text 3", "Salesforce {!tokens} become native {{Record.*}} that render today."), ("Text 9", "Salesforce tokens — as imported"), ("Text 10", "✗ {!Quote.Name} / {!Account.Name} render literally\n✗ No template in the catalog to bind\n✗ {!ApprovalRequest.Internal_URL} unresolved"), ("Text 14", "Native merge — migrated"), ("Text 15", "✓ {{Record.Name}}, {{Record.Discount__c}} resolve now\n✓ 74 templates shipped and selectable\n✓ {{Approval.RequestUrl}} lights up with #2267"), ], 6: [ ("Text 1", "STANDARD & RISKS"), ("Text 2", "A Flow, not a backend Alert arm"), ("Text 3", "l0-l1-standard T3: buildable on existing engines → registered content."), ("Text 8", "STANDARD"), ("Text 9", "Ruled by the standard"), ("Text 10", "— “Send email” = verb + seam L0; binding L1 (calibration)\n— T3: buildable on the Flow engine → registered content\n— A new Alert arm = duplicate engine branch (forbidden)"), ("Text 13", "SHAPE"), ("Text 14", "Recommended"), ("Text 15", "✓ Email-alert behaviour = a Flow (L1/L2)\n✓ Templates = L2 catalog; picker = L0 sugar\n✓ No backend change; pack seed + upstream FE PR"), ], 8: [ ("Text 1", "THE CHANGE"), ("Text 2", "What ships — and what does not"), ("Text 7", "STANDARD-COMPLIANT"), ("Text 8", "Email Alert — 74 · 11 · 0"), ("Text 10", "Email templates"), ("Text 11", "74 — native {{Record.*}}, active (L2)"), ("Text 14", "Per-object folders"), ("Text 15", "11 — Quote, Lead, PoC, Order… (L2)"), ("Text 16", "Backend engine arms"), ("Text 17", "0 — the Flow path is reused"), ("Text 20", "Front-end convenience"), ("Text 21", "Email Alert picker → scaffolds a Flow (L0)"), ("Text 22", "Backend change"), ("Text 23", "none"), ("Text 26", "Delivery"), ("Text 27", "L2 seed (no rebuild) + upstream FE PR"), ("Text 30", "seed catalog (L2) → add FE Email Alert sugar (L0) → pick template → scaffolds Flow → sends"), ], 9: [ ("Text 1", "DEV REFERENCE · L0 / L1 / L2"), ("Text 2", "Ownership: L0 engine · L2 content"), ("Text 5", "Behaviour = a Flow (registered content, L1/L2). sendEmail verb + Flow seam + front-end sugar = L0. No backend arm."), ("Text 9", "engine + verb + sugar (have)"), ("Text 10", "Flow seam · sendEmail verb · front-end — upstream NexusCRM"), ("Text 11", "Flow approval action + executeApprovalFlowNotification\nsendEmail verb + Send Email node picker\nReportFolder (Type='Email') folders\nNative {{Record.*}} rendering\n_System_EmailTemplate table\nFront-end “Email Alert” sugar (scaffolds a Flow)\nAction types: Field Update / Flow (no new arm)\nNo backend engine change"), ("Text 14", "L2"), ("Text 15", "tenant catalog content"), ("Text 16", "enters as seed data, tenant-owned"), ("Text 17", "Email templates"), ("Text 18", "74 active _System_EmailTemplate"), ("Text 19", "→ deploy-baseline.seed.yaml"), ("Text 20", "Per-object folders"), ("Text 21", "11 ReportFolder (Type='Email')"), ("Text 22", "→ ships in nexuscrm-pingcap-pack"), ("Text 23", "The email-alert behaviour = a Flow (L1/L2)"), ("Text 25", "T3: buildable on the Flow engine → registered content."), ("Text 26", "sendEmail verb + Flow seam + FE sugar stay L0 (app-neutral)."), ("Text 27", "pingcap-pack/docs/design/approval-email-notifications.md"), ], 10: [ ("Text 2", "Deploy: catalog now, sugar next"), ("Text 8", "Catalog — L2, now"), ("Text 9", "74 templates → native merge\n11 per-object folders\ndeploy-baseline seed (no rebuild)\nSend Email picker populated"), ("Text 10", "Ships via the pack."), ("Text 14", "Email Alert sugar — L0 FE"), ("Text 15", "“Email Alert” option per event\nscaffolds a Flow (upstream FE PR)\nno backend change"), ("Text 16", "SFDC-parity UX."), ("Text 20", "Flows + polish"), ("Text 21", "{{Approval.RequestUrl}} (#2267)\nremaining approvals (#3556)\nHTML templates"), ("Text 22", "Follow-ups."), ], 11: [ ("Text 2", "Tracking & next steps"), ("Text 5", "Flow-native email / empty picker = #2267\nPack approval-email gap = #3530\nRemaining approvals/templates = #3556\nDesign = docs/design/approval-email-notifications.md"), ("Text 8", "Seed 74 templates + 11 folders (L2)\nUpstream FE PR: Email Alert convenience (L0)\nUAT: approval-email-template-catalog case\nVerify the per-event picker on staging"), ("Text 9", "github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/3530"), ], } for si, edits in M.items(): for name, txt in edits: edit(si, name, txt) grid = [ ["Capability", "Salesforce", "NexusCRM today"], ["Approval action types", "Email Alert + more", "Field Update / Flow (Alert off)"], ["Email per lifecycle event", "yes — pick template", "via a Flow Send Email node"], ["Template picker", "classic templates", "reads catalog ✓ · empty"], ["Template catalog", "74 approval templates", "none seeded ✗ — the fix"], ["Dynamic merge fields", "{!Object.Field}", "{{Record.*}} native ✓"], ] for sh in slides[2].shapes: if sh.has_table: t = sh.table for r, row in enumerate(grid): if r < len(t.rows): for c, val in enumerate(row): if c < len(t.columns): set_para_text(t.cell(r, c).text_frame, [val]) for sl in slides: for sh in sl.shapes: if sh.has_text_frame and "Per-User Private List Views" in sh.text_frame.text: for p in sh.text_frame.paragraphs: for r in p.runs: r.text = r.text.replace("Per-User Private List Views", "Approval Email Notifications") for nm in ["Shape 12","Text 13","Shape 18","Text 19","Shape 24","Text 25","Shape 28","Text 29"]: for sh in list(slides[8].shapes): if sh.name == nm: sh._element.getparent().remove(sh._element) _tb = slides[8].shapes.add_textbox(Inches(0.6), Inches(6.52), Inches(12.13), Inches(0.32)) _tf = _tb.text_frame; _tf.word_wrap = True _p = _tf.paragraphs[0]; _p.alignment = PP_ALIGN.CENTER _r = _p.add_run() _r.text = "Scope: SFDC-style per-event email alert. Capability = a Flow (L1/L2). Templates = L2 catalog. Picker = L0 front-end sugar. No backend engine arm." _r.font.size = Pt(9.5); _r.font.name = "Segoe UI"; _r.font.color.rgb = RGBColor(0x9A, 0xA3, 0xB2) sldIdLst = prs.slides._sldIdLst sldIdLst.remove(list(sldIdLst)[7]) pos = 1 for sl in list(prs.slides): for sh in sl.shapes: if sh.has_text_frame and re.fullmatch(r"\s*\d+\s*/\s*12\s*", sh.text_frame.text or ""): for p in sh.text_frame.paragraphs: if p.runs: p.runs[0].text = f"{pos:02d} / 11" for extra in p.runs[1:]: extra._r.getparent().remove(extra._r) pos += 1 prs.save(OUT) print("SAVED", OUT, "slides:", len(list(prs.slides)))