# Coeo Bill

> Build/update Sivapol's (CTO, Vonosis) COEO Huawei Cloud billing workbook: Phase 1 builds a new month's workbook from CSV, Phase 2 fills Enterprise Project, then a single finalize pass validates and writes history; Phase 3 produces the customer-facing export. Trigger on "coeo bill", "coeo-bill", COEO billing file, or building/refreshing a COEO monthly workbook.

- Skill: `vonosis/coeo-bill` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vonosis/coeo-bill`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vonosis/coeo-bill/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Vonosis (https://skillmd.com/u/vonosis)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/vonosis/coeo-bill

---


# COEO Billing (coeo-bill)

Builds/updates the monthly COEO (Huawei Cloud) billing workbook for Sivapol (CTO, Vonosis). This skill grows in phases as Sivapol approves each one — append new phases to this file, don't replace the working logic already validated.

**Workflow order for a brand-new month: Phase 1 builds the raw Table1 data, Phase 2 tags Enterprise Project, THEN a single Finalize pass recalculates/validates/writes history once, THEN Phase 3 (when asked) produces the customer export.** Earlier versions of this skill validated and wrote history right after Phase 1, then again after Phase 2 if tagging changed anything — Sivapol pointed out that's redundant double work since tagging often changes the numbers anyway (confirmed on a real month — see Status log), so the standard flow now runs Phase 2 immediately after building Table1 and only validates/writes history once, at the end, in Finalize. **Multiple consecutive months can be run as one batch** — chain each month off the previous month's just-built Partner file, run Phase 1→2→Finalize→Phase 3 for every month in the batch, then present one consolidated report and get a single confirmation before saving all of them (validated on a real 5-month batch — see Status log).

## File & location

- Source of truth lives on Sivapol's computer (OneDrive), connected folder root: `.../COEO/00-Billing report/`.
- Per month subfolder: `YYYY-MM/` (e.g. `2026-08/`).
- **Working file naming (as of Phase 1):** new months are built as **`YYYY-MM-Billing-COEO-Partner.xlsx`** (e.g. `2026-09-Billing-COEO-Partner.xlsx`) — this is the name Phase 1 gives the file it copies/builds, and the one Phase 2 should look for on any month built after this convention started. Months built before this change (e.g. `2026-08`) instead used the older `x-YYYY-MM-Billing-COEO_2.xlsx` naming — that file still exists and is still valid for that month; don't rename it retroactively unless Sivapol asks. When looking for "the working file" for a given month, check for `YYYY-MM-Billing-COEO-Partner.xlsx` first, fall back to `x-YYYY-MM-Billing-COEO_2.xlsx` if that's what exists, and ask Sivapol if neither is present and it's unclear which convention applies.
- **Phase 3 output naming**: `YYYY-MM-Billing-COEO-Customer.xlsx`, in the same month subfolder as the working file it was built from.
- Other files that can appear in the same subfolder and are NOT the working file: a non-prefixed raw `.xlsx`, a `_BK.xlsx` backup copy, and the raw Huawei CSV exports (see below). The 2026-08 folder in particular has accumulated several stray files over testing (`2026-08-Billing-COEO.xlsx` — a raw pivot-only workbook, wrong structure; `2026-08-Billing-COEO_2.xlsx` — corrupted/not a valid zip) — **the only valid template file for 2026-08 is `x-2026-08-Billing-COEO_2.xlsx`**, confirmed by its sheet names (`2026-08-Billing-COEO`, `Control Sheet`, `Project name`, `Project name-customer`). When picking a template file, always open it and check `wb.sheetnames` matches this expected set before trusting it — don't assume by filename alone.
- Do the work with `device_bash` directly on the connected folder (the files live on the user's computer, not the cloud container) — never stage/download unless a step genuinely needs the cloud workspace or a tool the local shell lacks.
- Each month's raw Huawei billing exports land directly in that month's subfolder (Sivapol/Huawei console places them there — no separate "copy into folder" step needed). Two variants can appear:
  - `pricefactor_bill_detail_..._0.csv` — the full daily-granularity export (has a `Date` column, many more rows). **Not** what Table1 is built from.
  - the monthly-cycle-aggregated export, one row per resource for the month, **57 columns matching Table1 columns 1–57 exactly** (no `Date` column). **This is the file Phase 1 uses.** Its filename usually (but not always) has a `_cycle` suffix — **identify it by reading the CSV header and comparing to Table1's 57 column names, not by filename alone** (a real month was found where the only CSV present had no `_cycle` suffix in its name but was in fact the correct cycle-aggregated format). If a month's folder genuinely has only the daily/full export (has a `Date` column, far more rows than resources), stop and tell Sivapol to export the cycle-aggregated version from the Huawei console — don't try to aggregate the full export yourself.
  - A CSV can be a OneDrive cloud-only placeholder that isn't actually downloaded to the device yet — reading it fails with an error like "Resource deadlock avoided". If that happens, don't keep retrying blindly; ask Sivapol to open the file locally once (Finder/Excel, or "Always keep on this device") to force the real download, then retry.

## Workbook structure

- Main data sheet is named `YYYY-MM-Billing-COEO` and contains an Excel structured table named **Table1** (58 columns: `A:BF`, row count grows/shrinks monthly).
- Table1 columns: 1–57 come straight from the Huawei cycle CSV (`Billing Cycle` ... `Hourly Commitment (USD)`). Column 58, **`Environment`, is a formula column, not static data** — every row carries the exact same formula (uses `[#This Row]` structured references, so the text is identical row to row):
  ```
  =IF(Table1[[#This Row],[Account Name]]="coeo-workload-prod","Production",IF(Table1[[#This Row],[Account Name]]="coeo-workload-nonprod","Non-Production",IF(Table1[[#This Row],[Account Name]]="coeo-network-share-prod","Share-Production",IF(Table1[[#This Row],[Account Name]]="coeo-network-share-nonprod","Share-Non-Production","Share-Production"))))
  ```
  **Never delete or skip this formula on any row, old or newly added — Sivapol has flagged this explicitly.**
- Column 2, `Enterprise Project`, is the plain-value field Phase 2 fills — it is NOT a formula. **Important: the raw Huawei CSV itself can already carry real values here, not just `default`.** `HIS`, `ERP` and `Non-project` in particular showed up already tagged straight from the CSV on a real month (Huawei Cloud's own native "Enterprise Project" console feature, applied before export) — these are not something Phase 2 assigns. Phase 2's four rules only ever act on rows still sitting at `default`, and only ever produce `Web`, `PHR`, `Enterprise Router` or `Direct Connect`. Don't assume every fresh month starts 100% `default`; check the actual distribution before reporting. Also don't assume a month must have hits for all 4 rules, or that hits stay flat month to month — real months ranged from 0 tagging hits up to 9, with `Web` and `PHR` hit counts changing as the underlying resource set changed — always inspect the actual Resource Name/Service Type values rather than assuming.
- Other sheets: `Control Sheet` (named ranges `Customer_discount`, `INNOCO_Cost`, `Exchange_rate`), `Project name` and `Project name-customer` (cost-distribution + multi-month history — see Phase 1/Finalize), `Sheet3` if present (a stray pivot, informational only).
- `Project name` / `Project name-customer` layout:
  - `A2:D8` (`Project name` also has C/D cost columns; `Project name-customer` doesn't): `B2` = literal text `List Price - YYYY-MM` for the CURRENT month this workbook represents. `B3:B7` = formulas computing that month's List Price by category (Production, Non-Production, PHR, Direct Connect, Enterprise Router). `B8` = `SUM(B3:B7)` — this must equal the Table1 `Amount (USD)` grand total.
  - `A11:F25`: the underlying SUMIFS/allocation model. Rows 12–19 = ERP/HIS/PHR/Web split by Production/Non-Production, each getting a `C` (direct SUMIFS keyed on the `Enterprise Project` value, e.g. `C18`/`C19` only count rows already tagged `Web`), `D` (% of its production/non-production base, e.g. `D12=IFERROR(C12/SUM(C$12,C$14,C$16,C$18),0)` — see IFERROR note below), `E` (allocated share of pooled "default"/unclassified cost — `=D*Share_production` or `=D*Share_nonProduction`), `F=C+E`. Rows 20–21 = Direct Connect / Enterprise Router (no share allocation). Rows 22–25 = the **`default`-tagged cost pool** split by Environment (Share-Production, Share-Non-Production, Production, Non-Production) — this is what makes the whole sheet reconcile to the Table1 grand total with zero leakage. **Do not re-derive this model from scratch or "fix" it** — Sivapol built and validated it himself; the pipeline only needs to feed it fresh Table1 data and read back the results (the one exception is the `B3` orphaned-pool patch below, made at Sivapol's explicit direction).
  - **Named ranges `Share_production` / `Share_nonProduction` — important dependency for Phase 3**: originally defined as `='Project name'!$B$29` / `$B$30` (`=SUM(C22,C24)` / `=SUM(C23,C25)`). Sivapol has since **repointed both to `'Project name-customer'!$B$29` / `$B$30`** instead — `Project name-customer` already computes the identical `B29`/`B30`/`C22:C25` locally, so this makes `Project name-customer` self-sufficient and safe to keep once `Project name` is deleted (see Phase 3). Confirmed done on the `2026-08` template and `2026-01`'s Partner file, and inherited automatically by every month copy-forward-built since then. **Before running Phase 3 on any month, verify (don't assume) these two named ranges resolve to `Project name-customer`, not `Project name`** — if a template still points at `Project name`, deleting `Project name` in Phase 3 would break `Project name-customer` with `#REF!`. If found still pointing at `Project name`, stop and ask Sivapol to repoint it himself in Excel's Name Manager (he prefers to make this specific edit himself) before proceeding.
  - **`#DIV/0!` fix (IFERROR)**: if a month has zero rows in the Production (or Share-Production) environment at all, `SUM(C$12,C$14,C$16,C$18)` is 0 and `D12/D14/D16/D18` used to divide by zero, cascading `#DIV/0!` into `B3`/`B5`/`B8` (and `C3/C5/C8`, `D3/D5/D8` INNOCO/Customer THB columns on `Project name`). **Fixed at Sivapol's request**: `D12:D19` in both `Project name` and `Project name-customer` are wrapped `=IFERROR(<original formula>,0)`. Applied to `2026-01`'s Partner file; the Finalize step below makes this self-healing going forward regardless of which template a month was copied from.
  - **`B3` orphaned-pool patch — a follow-on edge case, also fixed at Sivapol's request**: the IFERROR fix above stops the error from showing, but when the Production-side base (`SUM(C12,C14,C16,C18)`) is 0 while the Production/Share-Production **default pool** (`Share_production` = `C22+C24`) is NOT 0, the money used to vanish silently (no ERP/HIS/Web/PHR row exists to proportion it against, so `D12/D14/D16/D18` all resolve to 0 via IFERROR and the pool goes nowhere — caught by the validation gate as a real mismatch, not a rounding issue). **Confirmed fix (Sivapol's instruction): when this happens, put the orphaned amount straight into `B3` (Production)**. Implemented by changing `B3` in both `Project name` and `Project name-customer` from `=SUM(F12,F14,F18)` to:
    ```
    =SUM(F12,F14,F18)+IF(SUM(C$12,C$14,C$16,C$18)=0,Share_production,0)
    ```
    This is a no-op whenever the base is nonzero (the normal proportional model already fully distributes `Share_production` across `F12,F14,F16,F18` in that case) — confirmed both on the month that first surfaced it (base became nonzero after Phase 2 tagging, so the patch stopped firing and the money landed in the more accurate category via the normal model instead) and on 5 subsequent real months where Phase 2 tagging always supplied a nonzero base before Finalize ran, so the patch never needed to fire again. Apply this same `B3` patch as a self-heal check (like the IFERROR one) on every month's Finalize pass, on whichever template a month was built from.
  - `G2:T8`: a 12-month history table. `G`=project label, `H`..`S` = Jan..Dec of the current year (fixed mapping: H=01, I=02, J=03, K=04, L=05, M=06, N=07, O=08, P=09, Q=10, R=11, S=12), `T` = `SUM(H:S)` grand total per row. Each month's column holds **hardcoded values**, not formulas — pasted once when that month's workbook is finalized (see Finalize), because Table1 only ever holds one month's data at a time.

## Phase 1 — Build a new month's Table1 from the raw CSV

When a new month's cycle-aggregated CSV is ready and the workbook for that month doesn't exist yet (or Sivapol asks to rebuild it fresh). This phase only rebuilds Table1's rows — it does **not** recalculate, validate, or write history (that happens once, in Finalize, after Phase 2 has also run):

1. **Copy the starting point**: copy the most recently finished working file for the prior month into the new month's subfolder, named **`YYYY-MM-Billing-COEO-Partner.xlsx`** for the new month (e.g. `2026-09-Billing-COEO-Partner.xlsx`) — this is the naming convention Sivapol wants for every month built by this pipeline from here on, regardless of what the prior month's file was named. This carries forward all sheet structure, named ranges, and the allocation model — don't build a workbook from scratch. Rename the main data sheet from `<prev-YYYY-MM>-Billing-COEO` to `<new-YYYY-MM>-Billing-COEO` (safe: `Table1[...]` structured references are table-scoped, not sheet-name-scoped, so nothing else breaks). Verify the chosen template file's `wb.sheetnames` matches the expected set (see "File & location" — some folders accumulate stray/corrupted files with similar names). For a batch of several consecutive months, chain them: month N+1 is built from month N's just-finished Partner file (in scratch, before it's saved to the real folder), not from an older template.
   - **If the target month has no chronologically-prior month at all** (e.g. building the very first month in the series, like `2026-01` when there's no `2025-12`), there's nothing to copy forward from by definition. Confirmed approach with Sivapol: use the most recently finished/validated month's workbook as the structural template instead (e.g. `2026-08`'s), and **clear all 12 history columns `H:S` (rows 3–7, both `Project name` and `Project name-customer`) before writing anything** — the template's own history doesn't belong on the new base month. Then proceed with steps 2 onward as normal, writing this month's own value into its own column later in Finalize.
2. **Read the new cycle CSV**, and hard-fail with a clear message if its header row doesn't match Table1 columns 1–57 exactly (column order and names) — don't guess a mapping. Identify the right CSV by header content, not filename (see "File & location").
3. **Rebuild Table1's data rows** to match the CSV exactly:
   - If the CSV has fewer rows than Table1's current row count (e.g. new month CSV = 200 rows, but the copied-forward Table1 still has 380 rows from the previous month), **delete** the excess rows from the bottom (`ws.delete_rows`, not just clearing values — no leftover phantom rows).
   - If the CSV has more rows, **insert** new rows (`ws.insert_rows`), copying cell style from the row immediately above into every new cell so formatting/number formats carry over.
   - Write the CSV's 57 columns into columns 1–57 for every row. Column 1 (`Billing Cycle`) parses from e.g. `"Aug 2026"` to a real date (first of month). Numeric-looking columns convert to float; the literal placeholder `--` is kept as the literal string `--` (matches the existing convention) rather than blanked or zeroed. Column 2 (`Enterprise Project`) just takes whatever the CSV has — which can be a mix of `default` and already-tagged values (see "Workbook structure"), not necessarily all `default`.
   - **Column 58 (`Environment`) gets the exact formula text above written into every single row, old or new — never leave it blank, never delete it.**
   - Update Table1's `ref` and `autoFilter.ref` to the new `A1:BF<n+1>` range.
4. Report the mechanical result to Sivapol (old/new row counts, deleted/inserted) and move straight into Phase 2 tagging — no recalc or validation yet at this point. For a batch, gather this per-month rather than reporting immediately (see the combined batch report format under Finalize).

Column-type parsing reference (Python): try `datetime.strptime(s, "%b %Y")` for `Billing Cycle`; for every other column try `float(s.replace(',', ''))`, falling back to the raw string (this also correctly preserves `--` placeholders and text fields like Resource Name/Region/etc.).

## Phase 2 — Enterprise Project classification rules

Run immediately after Phase 1 (Table1 rebuilt), before any recalculation/validation. Apply only to rows where `Enterprise Project` is currently `default` (case-insensitive, trimmed). Check in this order (first match wins; conditions are on different fields so overlap is not expected, but keep the order anyway):

1. `Resource Name` contains `web_` (case-insensitive substring) → set `Enterprise Project` = `Web`
2. `Resource Name` contains `RDS_PHR` (case-insensitive substring) → set `Enterprise Project` = `PHR`
3. `Service Type` exactly equals `Enterprise Router` → set `Enterprise Project` = `Enterprise Router`
4. `Service Type` exactly equals `Direct Connect As A Service` → set `Enterprise Project` = `Direct Connect`

If a row matches one of these rules but its current `Enterprise Project` is **not** `default` (i.e. someone/something already tagged it differently — including HIS/ERP/Non-project coming straight from Huawei console, see "Workbook structure" above), do **not** change it — keep the existing value, but collect it into a "skipped/conflict" list to report at the end (row number, Resource Name, Service Type, current value, which rule matched).

Use `openpyxl.load_workbook(path, data_only=False)` to preserve formulas elsewhere, mutate only the `Enterprise Project` column values. Locate columns by header name, not fixed letter. Report the tagging tally (rows changed per label, total changed, rows still `default`, full skipped/conflict list — state it even when zero, and if it's zero, sanity-check the actual Resource Name/Service Type values for that month before reporting it, since a genuinely-zero month is possible but should be confirmed, not assumed) alongside the Phase 1 mechanical report, then move straight into Finalize — don't save or validate yet.

## Finalize — recalculate, validate, write history (runs once, after Phase 1 + Phase 2)

This replaces what used to be two separate validate/history passes (once after Phase 1, again after Phase 2 if tagging changed anything) — Sivapol pointed out that's redundant since tagging usually does change the numbers, so now this runs exactly once, after both building and tagging are done, on an in-memory/scratch copy before anything touches the real working file:

1. **Self-heal checks** on both `Project name` and `Project name-customer`: if `D12:D19` isn't already wrapped `IFERROR(...,0)`, wrap it; if `B3` isn't already `=SUM(F12,F14,F18)+IF(SUM(C$12,C$14,C$16,C$18)=0,Share_production,0)`, patch it. (Both fixes are already baked into `2026-01`'s Partner file, so a month built from it inherits them automatically — this step is the safety net for templates that predate the fixes, e.g. `2026-08`.)
2. **Recalculate with LibreOffice** — openpyxl cannot evaluate formulas, and the SUMIFS/share-allocation model must actually run on the new data:
   ```
   soffice --headless --norestore -env:UserInstallation=file:///tmp/lo_profile --convert-to xlsx:"Calc MS Excel 2007 XML" --outdir <dir> <file.xlsx>
   ```
   (the `-env:UserInstallation` with a scratch profile dir avoids profile-lock issues on repeated runs). Read back the recalculated file to harvest `B3:B7` from both `Project name` and `Project name-customer`.
3. **Validation gate**: sum Table1's `Amount (USD)` column directly in Python. Compare it to the sum of the harvested `B3:B7` values (map any remaining error value to 0 as a last-resort defensive fallback — with the IFERROR + B3 patches this shouldn't occur anymore). They must match (within float rounding). **If they don't match, stop — do not save over the real file — and report the discrepancy to Sivapol** (the model should reconcile to zero by construction; a mismatch means something in the CSV, the rebuild, or a new edge case not yet covered is off — this is exactly how the `B3` orphaned-pool case was first caught).
4. **Update the history**: set `B2` to `List Price - <new-YYYY-MM>` and write the harvested `B3:B7` values into the matching month column (H..S mapping, see "Workbook structure") rows 3–7, in both `Project name` and `Project name-customer`, as plain numbers (not formulas). Never touch other months' columns.
5. **Recalculate once more** so the `T` column (12-month grand total) picks up the newly written month.
6. Report to Sivapol as one combined summary: old/new Table1 row counts, Phase 2 tagging tally + conflicts, any self-heal patches applied, the validation-gate result, the month column written and its values, the new grand total, and the exact output filename — **before** saving over the real file. Treat this as a single STOP-gate action: show the full dry tally, execute (copy/save into the real month subfolder) only after Sivapol confirms. If rebuilding a month that already has a working file, make a `_BK.xlsx` copy of whatever's already there first if no backup sibling exists yet. **For a batch of several months**: run Phase 1→2→Finalize (build/tag/validate in scratch, no saving) for every month in the batch first, then present ONE consolidated table (one row per month: row counts, tagging tally, validation result, grand total) and ask ONE confirmation before saving all of them to their real folders — validated on a real 5-month batch (see Status log). If any single month in the batch fails validation, stop and report just that month's discrepancy — don't silently skip it or save the others without flagging it first.

## Phase 3 — Customer-facing export

Once that month's workbook has been through Phase 1 + Phase 2 + Finalize, produce the version that goes out to the customer. Not run automatically — only when Sivapol asks for it (he has asked for every month so far, but always as a separate explicit instruction, sometimes in the same turn as confirming Finalize and sometimes later):

1. **Prerequisite check**: open the month's working file and confirm the `Share_production` / `Share_nonProduction` defined names resolve to `'Project name-customer'!$B$29` / `$B$30` (not `'Project name'!...`) — see "Workbook structure" above. If they still point at `Project name`, **stop and ask Sivapol to repoint them himself** in Excel's Name Manager before continuing (this is a deliberate one-time fix he makes per-file/per-template, not something this skill does automatically — he explicitly wants to keep control of editing defined names).
2. **Copy** the completed working file to a new file named **`YYYY-MM-Billing-COEO-Customer.xlsx`** in the same month subfolder.
3. **Delete** the `Control Sheet` and `Project name` sheets from the copy (`del wb[sheet_name]`). Keep the main `YYYY-MM-Billing-COEO` data sheet (with Table1) and `Project name-customer`. **Keep all defined names** — Sivapol explicitly wants them preserved even though `Customer_discount`/`INNOCO_Cost`/`Exchange_rate` (pointing at the now-deleted `Control Sheet`) become unresolvable; nothing in the surviving sheets uses them, so this causes no visible error, and don't clean them up unless Sivapol asks.
4. **Recalculate with LibreOffice** (same command as Finalize) and verify: no error-value cells remain anywhere in `Project name-customer` rows 3–30, and `B8` still equals Table1's `Amount (USD)` sum exactly. If either check fails, stop and report — don't hand over a broken customer file.
5. Report to Sivapol: source file used, sheets removed, validation result, output filename. Treat as a STOP-gate action like the other phases. **For a batch of several months**, do this for every month after all their Finalize saves are confirmed, then report all of them together (source/sheets removed/validation/filename per month) rather than one at a time — validated on a real 5-month batch.

## Status log

- Phase 2 (Enterprise Project fill: Web / PHR / Enterprise Router / Direct Connect via Resource Name & Service Type) — implemented and validated on `2026-08/x-2026-08-Billing-COEO_2.xlsx` (31 rows changed, 0 conflicts). Re-run is idempotent.
- Diagnostic note (historical) — traced a 186.16218007 USD gap in Sivapol's `Project name` SUMIFS totals to (a) an over-subtraction bug and (b) `default`-tagged rows outside Share-* environments having no bucket at all. Sivapol redesigned the sheet himself (rows 22–25 + `Share_production`/`Share_nonProduction` named ranges) — this fully closes the gap; do not reintroduce the old subtraction-based formula.
- Phase 1 mechanics — validated in a sandbox (row insert/delete, Environment formula on 100% of rows, LibreOffice recalculation genuine, grand total reconciled exactly in both directions).
- Real run: `2026-01` (first build, then rebuilt from scratch on request) — 80 rows, zero Production-environment rows that month, `#DIV/0!` → 0 handled, grand total 1366.42380896. Phase 2: 0 rows changed (verified not a bug — no resources matching any of the 4 rules that month), 0 conflicts.
- Naming convention: output file naming is `YYYY-MM-Billing-COEO-Partner.xlsx` for every month built from here on; existing months already built under the old `x-YYYY-MM-Billing-COEO_2.xlsx` convention (2026-08) are left as-is.
- Phase numbering: build/tag order swapped to match real workflow — content unchanged, only labels swapped.
- `#DIV/0!` → IFERROR fix (2026-01) — Sivapol asked to make the zero-Production-base edge case show 0 instead of `#DIV/0!` directly in the live formulas. Wrapped `D12:D19` in both `Project name` and `Project name-customer` with `IFERROR(...,0)` on `2026-01-Billing-COEO-Partner.xlsx`. Added as a Finalize self-heal check so every future month gets this regardless of template. **Not yet applied to the `2026-08` template file itself** — the self-heal check catches it automatically whenever a month is next built from it.
- `Share_production`/`Share_nonProduction` named-range repoint (2026-01) — these named ranges pointed at `'Project name'!$B$29`/`$B$30`, which would break `Project name-customer` with `#REF!` once `Project name` is deleted in Phase 3. Sivapol repointed both names himself (Excel Name Manager) to `'Project name-customer'!$B$29`/`$B$30` on both `2026-08/x-2026-08-Billing-COEO_2.xlsx` and `2026-01/2026-01-Billing-COEO-Partner.xlsx` — confirmed via `wb.defined_names`. Prerequisite for Phase 3 (see Phase 3 step 1); check, don't assume, on any file that predates this fix.
- Phase 3 (Customer export) — implemented and validated on `2026-01`: copied the fixed/tagged Partner file to `2026-01-Billing-COEO-Customer.xlsx`, deleted `Control Sheet` + `Project name`, kept all 5 defined names, recalculated — zero errors, `B8` = 1366.42380896 matching Table1 exactly.
- Real run: `2026-02` (build → tag → finalize, single-pass order) — 80→225 rows (+145). First validation attempt (Phase 1 alone, before tagging) failed the gate by exactly 24.26038457 USD: this month had real Production/Share-Production `default`-tagged cost ($23.50322 + $0.75716) but zero rows tagged ERP/HIS/PHR/Web in the Production environment, so `D12/D14/D18` (via IFERROR) all resolved to 0 and the money vanished instead of erroring — a new edge case (nonzero pool, zero base) distinct from the original all-zero case. Sivapol's fix: route it into `B3` (Production) directly — implemented as the `B3` orphaned-pool patch documented above. Sivapol then pointed out that Phase 2 tagging should run **before** the validation/history pass, not after with a possible rewrite, since tagging often changes the numbers anyway — adopted as the new standard order (this Status log entry is what prompted restructuring Phase 1/Phase 2/Finalize into their current separate sections). Re-running with Phase 2 first: 3 rows tagged PHR, which made the Production base nonzero, so the normal proportional model took over on its own (the `B3` patch's `IF` condition no longer fired) and correctly routed the $24.26 into PHR instead of generic Production — validation passed, `B8` = 1290.0295992 matching Table1 exactly. Saved as `2026-02-Billing-COEO-Partner.xlsx`. Phase 3 also run for `2026-02`: zero errors, `B8` = 1290.0295992 matching Table1.
- Batch real run: `2026-03` through `2026-07` (5 consecutive months, chained: each month built from the previous month's just-saved Partner file, Phase 1 → Phase 2 → Finalize → Phase 3 for all 5 before any single confirmation) — Sivapol confirmed this batch/chained approach works and is the expected way to run several months in one go, not just one month at a time. Row counts: 225→215→213→197→220→256. Phase 2 tagging kept finding real hits as the months progressed (PHR +3 every month, Web +6 in `2026-05`, +4 in `2026-06` and `2026-07`) — confirms Phase 2 rules stay relevant, not just a one-off from `2026-08`. All 5 months validated cleanly on the first pass (Phase 2 always run before Finalize, per the `2026-02` lesson) — no `B3`-patch firing needed since Phase 2 tagging consistently supplied a nonzero Production base before Finalize ran. All 5 self-heal checks were no-ops (both fixes already inherited from `2026-02` through the copy-forward chain). Phase 3 ran cleanly on all 5 with zero errors and exact `B8`/Table1 matches. Grand totals: `2026-03`=1790.60449485, `2026-04`=2226.56057435, `2026-05`=3005.4661607, `2026-06`=2651.19627612, `2026-07`=3191.95690991. This leaves `2026-01`–`2026-08` all fully built (Phase 1+2+Finalize+Phase 3); `2026-09`–`2026-12` still need their CSVs before they can be built.
