Improve SKU catalog hygiene and BOM accuracy
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 21s

- refresh_sku_catalog.py: add --discover to report API SKUs missing from
  catalog, filtered to already-curated serviceCategory values. Auto-runs
  at end of --validate and --refresh so gaps surface on every maintenance
  pass. First run found 162 missing SKUs (Blackwell GPUs, X12 Ax compute,
  VMware reserved tiers, WebLogic-on-OKE, Analytics/OIC BYOL).
- oci_bom_gen.py: fix Cost % column being blank. Back-fill was gated on
  formula detection but data rows write raw numbers, so the predicate
  never matched. Now tracks data_item_rows explicitly.
- .gitea/workflows/sku-catalog-refresh.yaml: monthly automation (1st at
  09:00 UTC). Auto-refresh prices → push branch + open PR; detect new
  SKUs → open issue with labels. Gated on secrets.GITEA_TOKEN.
- kb/pricing/oci-sku-catalog.yaml: add B95714 / B95715 (Autonomous ATP
  Dedicated ECPU, LI + BYOL) — canonical SKUs for ADB-D pricing.
- kb/services/compute.yaml: add X12 family (VM.Standard4.Ax.Flex,
  BM.Standard4.Ax.120), E6.Ax and A4.Ax variants. Verified against
  docs.oracle.com computeshapes.htm. Bump last_verified + changelog.
- templates/bom-spec.yaml: document 730 hrs/month convention as default
  (= real annual billing / 12; 744 overstates by 1.92% on 12-month TCO).
- Makefile: new 'make sku-discover' target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-24 11:40:53 -03:00
parent a22711c065
commit 874d40d38a
7 changed files with 327 additions and 13 deletions

View File

@@ -328,6 +328,7 @@ class OCIBomGenerator:
data_start_row = row
cat_subtotal_rows = []
data_item_rows = [] # line-item rows (excludes category headers/subtotals/totals)
# Ensure categories present in items but missing from catalog order
# (e.g., "other" from unknown SKUs) still render at the end.
@@ -418,6 +419,7 @@ class OCIBomGenerator:
ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00'
ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00'
data_item_rows.append(row)
row += 1
# Category subtotal row
@@ -492,17 +494,15 @@ class OCIBomGenerator:
# ── Cost proportion formulas (back-fill) ─────────────────
# Cost % = line monthly w/ discount / total monthly w/ discount
for item_row in range(data_start_row, total_row):
cell_val = ws.cell(row=item_row, column=COL_M_W).value
if cell_val and str(cell_val).startswith("=") and "SUM" not in str(cell_val):
mw_l = get_column_letter(COL_M_W)
ws.cell(
row=item_row, column=COL_PCT,
value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})",
)
ws.cell(row=item_row, column=COL_PCT).number_format = '0.0%'
ws.cell(row=item_row, column=COL_PCT).font = data_font
ws.cell(row=item_row, column=COL_PCT).border = thin_border
mw_l = get_column_letter(COL_M_W)
for item_row in data_item_rows:
ws.cell(
row=item_row, column=COL_PCT,
value=f"=IF({mw_l}{total_row}=0,0,{mw_l}{item_row}/{mw_l}{total_row})",
)
ws.cell(row=item_row, column=COL_PCT).number_format = '0.0%'
ws.cell(row=item_row, column=COL_PCT).font = data_font
ws.cell(row=item_row, column=COL_PCT).border = thin_border
# ── Notes ─────────────────────────────────────────────────
if self.notes:

View File

@@ -220,6 +220,13 @@ def refresh_catalog(verbose=False):
print(" Not in API (possibly retired): {}".format(not_found))
print(" Catalog saved: {}".format(CATALOG_PATH))
# Reverse-direction check: SKUs in API we haven't catalogued yet.
catalog_skus_after = {str(e["sku"]) for cat in raw.get("categories", {}).values()
for e in cat.get("skus", [])}
stub_map = {s: {} for s in catalog_skus_after}
new_skus = discover_missing_skus(api_map, stub_map, verbose=verbose)
print_missing_skus(new_skus, limit=None if verbose else 20)
def validate_catalog(verbose=False):
"""Compare current catalog prices against API and report differences."""
@@ -282,6 +289,104 @@ def validate_catalog(verbose=False):
if len(missing) > 10:
print(" ... and {} more".format(len(missing) - 10))
# Reverse-direction check: SKUs in API we haven't catalogued yet.
new_skus = discover_missing_skus(api_map, sku_map, verbose=verbose)
print_missing_skus(new_skus, limit=None if verbose else 20)
def discover_missing_skus(api_map, catalog_sku_map, verbose=False):
"""Find SKUs present in the API but missing from the catalog.
The filter is auto-derived: we collect every `serviceCategory` value for
SKUs already in the catalog and only report new SKUs whose serviceCategory
falls within that curated set. This keeps the report relevant — as new
categories are added to the catalog, the discovery scope expands.
"""
curated_categories = set()
for sku in catalog_sku_map:
if sku in api_map:
sc = api_map[sku].get("serviceCategory", "") or ""
if sc:
curated_categories.add(sc)
if verbose:
print(" Curated serviceCategory values ({}): {}".format(
len(curated_categories), ", ".join(sorted(curated_categories))
))
missing = []
for sku, product in api_map.items():
if sku in catalog_sku_map:
continue
sc = product.get("serviceCategory", "") or ""
if sc not in curated_categories:
continue
missing.append({
"sku": sku,
"product": product.get("displayName", ""),
"service_category": sc,
"metric": product.get("metricName", ""),
"price": extract_payg_price(product),
})
missing.sort(key=lambda x: (x["service_category"], x["sku"]))
return missing
def print_missing_skus(missing, limit=None):
"""Print a readable report of missing SKUs, grouped by serviceCategory."""
if not missing:
print("\nNo new SKUs discovered in curated categories. Catalog covers the API well.")
return
print("\n{} SKUs present in API but missing from catalog".format(len(missing)))
print("(filtered to serviceCategory values already represented in our catalog)\n")
by_cat = {}
for m in missing:
by_cat.setdefault(m["service_category"], []).append(m)
shown = 0
truncated = False
for cat, items in sorted(by_cat.items()):
print(" [{}] ({} new)".format(cat, len(items)))
for m in items:
if limit is not None and shown >= limit:
truncated = True
break
price_str = "${:.4f}".format(m["price"]) if m["price"] else " -"
print(" {:<10} {:>10} {}".format(
m["sku"], price_str, m["product"][:70]
))
shown += 1
if truncated:
break
if truncated:
print("\n ... and {} more (re-run with -v to list all)".format(len(missing) - shown))
print("\nNext step: review and add relevant entries to kb/pricing/oci-sku-catalog.yaml")
print(" under the appropriate category block.")
def discover_catalog(verbose=False):
"""Stand-alone --discover entry point: report new API SKUs not yet in catalog."""
print("Fetching current products from Oracle API...")
products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose)
api_map = {}
for p in products:
pn = p.get("partNumber", "")
if pn:
api_map[pn] = p
raw, sku_map = load_current_catalog()
if not raw:
print("Error: Could not load catalog at {}".format(CATALOG_PATH))
return 1
missing = discover_missing_skus(api_map, sku_map, verbose=verbose)
print_missing_skus(missing, limit=None if verbose else 40)
return 0
def inspect_sku(part_number):
"""Fetch and display a single SKU from the API."""
@@ -571,6 +676,9 @@ def main():
"(currently: compute)")
parser.add_argument("--validate", action="store_true",
help="Validate catalog prices against API")
parser.add_argument("--discover", action="store_true",
help="Report SKUs present in API but missing from catalog "
"(filtered to serviceCategory values already curated)")
parser.add_argument("--sku", type=str,
help="Inspect a single SKU from the API")
parser.add_argument("--dump", type=str, metavar="FILE",
@@ -589,6 +697,8 @@ def main():
refresh_catalog(verbose=args.verbose or args.diff)
elif args.validate:
validate_catalog(verbose=args.verbose)
elif args.discover:
return discover_catalog(verbose=args.verbose)
elif args.dump:
dump_all(args.dump, verbose=args.verbose)
else: