roster_workbook.py
Language Python8.4 KB202 lines
"""refresh-workbook: regenerate the Drive-swap roster workbook.The vault copy of the roster is not connected to Google Drive (it is swapped in byhand), so each run carries the human-maintained columns forward from the previousgenerated copy (the base), refreshes only the Plinth-derived columns, appends newlyqualified partner clients, rebuilds the all-customer tab, and leaves the curatedworking tab untouched. A human-maintained column on an existing row is never written."""from __future__ import annotationsimport datetime as dtimport jsonimport sysfrom pathlib import Pathfrom .config import NON_PARTNER, guard_or_exitfrom .history import load_merge_registryfrom .paths import OUT_DIR, TODAYPARTNER_TAB = "Complete Partner Clients List"ALL_TAB = "All Accounts"def latest_updated_workbook(exclude: Path | None = None):cands = sorted(OUT_DIR.glob("Complete-Partner-Clients-List_updated-*.xlsx"))cands = [c for c in cands if exclude is None or c.resolve() != exclude.resolve()]return cands[-1] if cands else Nonedef _as_date(s):try:return dt.date.fromisoformat(str(s)[:10])except Exception:return s or ""def cmd_refresh_workbook(args):guard_or_exit()from openpyxl import load_workbookfrom copy import copy as _copydef _snap(cell):# snapshot a cell's full visual style (survives row deletion)return (_copy(cell.font), _copy(cell.fill), _copy(cell.border),_copy(cell.alignment), cell.number_format, _copy(cell.protection))def _apply(cell, snap):cell.font, cell.fill, cell.border, cell.alignment = snap[0], snap[1], snap[2], snap[3]cell.number_format = snap[4]cell.protection = snap[5]plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))rows = plan["rows"]by_uid = {str(r["user_id"]): r for r in rows if r.get("user_id") not in (None, "")}partner = [r for r in rows if r["assigned_tier"] not in NON_PARTNER]absorbed_uids = set(load_merge_registry()[0])stale_merged = []out = OUT_DIR / f"Complete-Partner-Clients-List_updated-{TODAY}.xlsx"base = Path(args.base) if args.base else latest_updated_workbook(exclude=out)if not base or not base.exists():sys.exit("No base workbook found (Complete-Partner-Clients-List_updated-*.xlsx in "f"{OUT_DIR}); pass --base, or generate the first copy by hand.")wb = load_workbook(base)d = dt.date.fromisoformat(TODAY)date_us = f"{d.month:02d}/{d.day:02d}/{str(d.year)[2:]}"# Partner tab: refresh the Plinth columns, append newly qualified partners.ws = wb[PARTNER_TAB]hdr = [c.value for c in ws[1]]def col(name, prefix=False):for idx, h in enumerate(hdr):if h is None:continueif (prefix and str(h).startswith(name)) or (not prefix and h == name):return idxreturn Nonec_id = col("Id")c_fn = col("First Name")c_ln = col("Last Name")c_em = col("Email")c_co = col("Company")c_ph = col("Phone")c_ug = col("User Group")c_cg = col("Correct Group")c_ltvd = col("Plinth LTV", prefix=True) # dated lifetime columnc_tot = col("Total Spent (Orders)")c_last = col("Last Order")c_ord = col("# Orders")c_rr = col("Repeat Revenue (Plinth)")c_first = col("First Order")if c_ltvd is not None: # rename dated header to this runws.cell(row=1, column=c_ltvd + 1).value = f"Plinth LTV {date_us}"def _uid_of(v):if v is None:return Noneif isinstance(v, float) and v.is_integer():return str(int(v))return str(v).strip()# Per-column master style from an existing data row (row 2). row 2's Plinth cells are# always populated and correctly formatted (currency, mm/dd/yyyy, Arial), so we clone# it onto every Plinth cell we write (existing rows and appended rows), so formatting# always matches the master, even on rows a prior run left unformatted. Plinth columns# only on existing rows; human columns are never restyled and never revalued.tmpl = [_snap(ws.cell(row=2, column=ci + 1)) for ci in range(len(hdr))] if ws.max_row >= 2 else Noneplinth_cols = [c for c in (c_ug, c_ltvd, c_tot, c_last, c_ord, c_rr, c_first) if c is not None]seen, refreshed = set(), 0for row in ws.iter_rows(min_row=2):uid = _uid_of(row[c_id].value)if uid is None:continueseen.add(uid)if uid in absorbed_uids:stale_merged.append(uid) # merged-away account: leave the row alone,continue # flag below for a human to strike by handr = by_uid.get(uid)if not r:continue# Only Plinth-derived columns. Identity and all human columns are left untouched.if c_ug is not None: row[c_ug].value = r.get("store_user_group") or ""if c_ltvd is not None: row[c_ltvd].value = round(r["ltv"], 2)if c_tot is not None: row[c_tot].value = round(r["ltv"], 2)if c_last is not None: row[c_last].value = _as_date(r.get("last_order_date"))if c_ord is not None: row[c_ord].value = r.get("total_orders_count") or 0if c_rr is not None: row[c_rr].value = round(r.get("repeat_revenue", 0), 2)if c_first is not None: row[c_first].value = _as_date(r.get("first_order_date"))if tmpl: # normalize Plinth-cell formatting to the masterfor ci in plinth_cols:_apply(row[ci], tmpl[ci])refreshed += 1ncols, appended = len(hdr), 0for r in partner:uid = _uid_of(r.get("user_id"))if uid is None or uid in seen:continuenr = [None] * ncolsdef setc(ci, val):if ci is not None:nr[ci] = valsetc(c_id, int(uid) if uid.isdigit() else uid)setc(c_fn, r.get("first_name") or "")setc(c_ln, r.get("last_name") or "")setc(c_em, r.get("email") or "")setc(c_co, r.get("company") or "")setc(c_ph, r.get("phone") or "")setc(c_ug, r.get("store_user_group") or "")setc(c_cg, r.get("assigned_tier") or "") # new row: record computed tier (no human value to preserve)setc(c_ltvd, round(r["ltv"], 2))setc(c_tot, round(r["ltv"], 2))setc(c_last, _as_date(r.get("last_order_date")))setc(c_first, _as_date(r.get("first_order_date")))setc(c_ord, r.get("total_orders_count") or 0)setc(c_rr, round(r.get("repeat_revenue", 0), 2))ws.append(nr)rr = ws.max_rowif tmpl:for ci in range(ncols):_apply(ws.cell(row=rr, column=ci + 1), tmpl[ci])seen.add(uid)appended += 1# All Accounts: rebuild the all-customer base.base_n = 0if ALL_TAB in wb.sheetnames:b = wb[ALL_TAB]bcols = b.max_column# snapshot the master row style per column before deleting (currency on LTV/RR etc.)btmpl = [_snap(b.cell(row=2, column=c)) for c in range(1, bcols + 1)] if b.max_row >= 2 else Noneif b.max_row > 1:b.delete_rows(2, b.max_row - 1)for r in rows:uid = _uid_of(r.get("user_id"))b.append([int(uid) if (uid and uid.isdigit()) else (uid or ""),r.get("first_name") or "", r.get("last_name") or "", r.get("email") or "",round(r["ltv"], 2), round(r.get("repeat_revenue", 0), 2),])base_n += 1if btmpl: # re-apply the master per-column formatting to every rebuilt rowfor rr in range(2, b.max_row + 1):for c in range(1, bcols + 1):_apply(b.cell(row=rr, column=c), btmpl[c - 1])wb.save(out)if stale_merged:print(f"MERGE REGISTRY NOTE: partner-tab row(s) for absorbed account(s) "f"{', '.join(stale_merged)} were left untouched: strike them by hand ""(their spend now lives on the survivor's row).")print(f"Workbook refreshed: base {base.name} -> {out.name}. "f"Partner tab: {refreshed} rows refreshed, {appended} appended. "f"All Accounts rebuilt to {base_n} customers. Dated col renamed 'Plinth LTV {date_us}'. "f"Working tab and all human columns untouched.")print("REMINDER: append one LEDGER entry for this change-set.")