Drop closing blanks, honor primary_driver, catalog data-services SKUs
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 23s
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 23s
- oci_deck_gen / oci_bizcase_gen: stop appending the Thank You + Oracle logo-only closing slides at the end of every generated deck. - oci_bizcase_gen: Business Drivers now reads primary_driver at the drivers-level and at the top level, and renders natural-language values verbatim (only snake_case enum tokens are Title-cased). A spec-provided drivers.items / additional / secondary list replaces the hardcoded "Financial Impact of Inaction" + "Operational Impact" fallback cards. - oci-sku-catalog.yaml: add data_services category with the four real Big Data Service SKUs (B91128/B91129/B91130/B93555) plus EST-DS-NOTEBOOK, EST-DS-MODEL, EST-DF-SPARK as estimate placeholders (OCI Data Science / Data Flow have no dedicated SKUs in the public pricing API — they are billed via the underlying compute shape). - oci_bom_gen: resolver now prints a stderr warning and tags estimate/ unknown line items with an explicit note; the xlsx writer renders "other" category items under an "Uncategorized — confirm SKUs" section at the end instead of silently dropping them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1262,6 +1262,60 @@ categories:
|
|||||||
list_price_usd: 0.0003
|
list_price_usd: 0.0003
|
||||||
default_hours_units: 1
|
default_hours_units: 1
|
||||||
billing_type: per_unit
|
billing_type: per_unit
|
||||||
|
data_services:
|
||||||
|
display_name: Oracle Cloud Infrastructure - Data & Analytics Services
|
||||||
|
skus:
|
||||||
|
- sku: B91128
|
||||||
|
product: Oracle Big Data Service - Compute - Standard
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.1344
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
- sku: B91129
|
||||||
|
product: Oracle Big Data Service - Compute - Dense I/O
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.214
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
- sku: B91130
|
||||||
|
product: Oracle Big Data Service - Compute - HPC
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.1536
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
- sku: B93555
|
||||||
|
product: Oracle Big Data Service - Core
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.015
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
# OCI Data Science and Data Flow do not have dedicated SKUs in the Oracle
|
||||||
|
# public pricing API — they are billed via the underlying Compute + Block/Object
|
||||||
|
# Storage consumed by notebook sessions, model-deployment endpoints, and Spark
|
||||||
|
# runs. The entries below are placeholder estimates (VM.Standard3.Flex OCPU rate)
|
||||||
|
# so BOMs can include them as line items; always confirm against the customer's
|
||||||
|
# actual shape + usage before quoting.
|
||||||
|
- sku: EST-DS-NOTEBOOK
|
||||||
|
product: '[ESTIMATE] OCI Data Science - Notebook Session (billed via underlying VM.Standard compute OCPU)'
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.0255
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
estimate: true
|
||||||
|
- sku: EST-DS-MODEL
|
||||||
|
product: '[ESTIMATE] OCI Data Science - Model Deployment endpoint (billed via underlying VM.Standard compute OCPU)'
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.0255
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
estimate: true
|
||||||
|
- sku: EST-DF-SPARK
|
||||||
|
product: '[ESTIMATE] OCI Data Flow - Managed Spark runtime (billed via underlying VM.Standard compute OCPU)'
|
||||||
|
metric: OCPU Per Hour
|
||||||
|
list_price_usd: 0.0255
|
||||||
|
default_hours_units: 730
|
||||||
|
billing_type: hourly
|
||||||
|
estimate: true
|
||||||
roving_edge:
|
roving_edge:
|
||||||
display_name: Oracle Roving Edge Infrastructure
|
display_name: Oracle Roving Edge Infrastructure
|
||||||
skus:
|
skus:
|
||||||
|
|||||||
@@ -637,14 +637,47 @@ class BusinessCaseDeckGenerator(OraclePresBase):
|
|||||||
|
|
||||||
# Slide 3: Business Drivers
|
# Slide 3: Business Drivers
|
||||||
drivers_data = bc.get("drivers", {})
|
drivers_data = bc.get("drivers", {})
|
||||||
if drivers_data:
|
# Accept primary_driver at the top level as well (common user shape)
|
||||||
|
top_primary = pick(bc, "primary_driver", "main_driver")
|
||||||
|
# Accept a plain string or list for drivers
|
||||||
|
if isinstance(drivers_data, str):
|
||||||
|
drivers_data = {"primary": drivers_data}
|
||||||
|
elif isinstance(drivers_data, list):
|
||||||
|
drivers_data = {"items": drivers_data}
|
||||||
|
elif not isinstance(drivers_data, dict):
|
||||||
|
drivers_data = {}
|
||||||
|
|
||||||
|
if drivers_data or top_primary:
|
||||||
driver_statements = []
|
driver_statements = []
|
||||||
primary = pick(drivers_data, "primary")
|
primary = pick(drivers_data, "primary", "primary_driver", "main_driver") or top_primary
|
||||||
urgency = pick(drivers_data, "urgency")
|
urgency = pick(drivers_data, "urgency", "why_now")
|
||||||
coi = drivers_data.get("cost_of_inaction", {})
|
coi = drivers_data.get("cost_of_inaction", {}) or {}
|
||||||
|
|
||||||
if primary:
|
if primary:
|
||||||
driver_statements.append(f"Primary Driver: {primary.replace('_', ' ').title()}\n{urgency}" if urgency else primary.replace('_', ' ').title())
|
primary_text = primary if isinstance(primary, str) else str(primary)
|
||||||
|
# Only rewrite snake_case enum tokens ("compliance_driven"); leave natural
|
||||||
|
# language intact so values like "Soberanía de datos + compliance regulatorio"
|
||||||
|
# render verbatim.
|
||||||
|
if "_" in primary_text and " " not in primary_text and len(primary_text) <= 40:
|
||||||
|
headline = f"Primary Driver: {primary_text.replace('_', ' ').title()}"
|
||||||
|
else:
|
||||||
|
headline = primary_text
|
||||||
|
driver_statements.append(f"{headline}\n{urgency}" if urgency else headline)
|
||||||
|
|
||||||
|
# Spec-provided additional cards take precedence over the inaction fallback
|
||||||
|
extra_items = pick_list(drivers_data, "items", "additional", "secondary", "cards")
|
||||||
|
for item in extra_items:
|
||||||
|
if isinstance(item, str) and item.strip():
|
||||||
|
driver_statements.append(item.strip())
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
label = pick(item, "label", "headline", "title", "name")
|
||||||
|
desc = pick(item, "detail", "description", "text", "body")
|
||||||
|
if label and desc:
|
||||||
|
driver_statements.append(f"{label}\n{desc}")
|
||||||
|
elif label or desc:
|
||||||
|
driver_statements.append(label or desc)
|
||||||
|
|
||||||
|
if not extra_items:
|
||||||
financial = pick(coi, "financial")
|
financial = pick(coi, "financial")
|
||||||
operational = pick(coi, "operational")
|
operational = pick(coi, "operational")
|
||||||
strategic = pick(coi, "strategic")
|
strategic = pick(coi, "strategic")
|
||||||
@@ -700,10 +733,6 @@ class BusinessCaseDeckGenerator(OraclePresBase):
|
|||||||
next_steps=pick_list(rec, "next_steps"),
|
next_steps=pick_list(rec, "next_steps"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Closing slides
|
|
||||||
prepared_by = pick(bc, "prepared_by", "author")
|
|
||||||
gen.add_closing_slide(name=prepared_by)
|
|
||||||
|
|
||||||
return gen
|
return gen
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,16 @@ class OCIBomGenerator:
|
|||||||
sku = str(sku)
|
sku = str(sku)
|
||||||
cat_entry = self.catalog.get(sku, {})
|
cat_entry = self.catalog.get(sku, {})
|
||||||
|
|
||||||
|
note = custom_note
|
||||||
|
if not cat_entry:
|
||||||
|
warn = f"[WARN] SKU {sku!r} not in catalog — included as line item with $0 price and warning note"
|
||||||
|
print(warn, file=sys.stderr)
|
||||||
|
flag = "⚠ SKU not in catalog — estimated $0, must be confirmed before quoting"
|
||||||
|
note = f"{note}. {flag}" if note else flag
|
||||||
|
elif cat_entry.get("estimate"):
|
||||||
|
flag = "⚠ Estimate only — billed via underlying compute/storage; confirm actual shape + hours"
|
||||||
|
note = f"{note}. {flag}" if note else flag
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
"sku": sku,
|
"sku": sku,
|
||||||
"product": cat_entry.get("product", f"Unknown SKU ({sku})"),
|
"product": cat_entry.get("product", f"Unknown SKU ({sku})"),
|
||||||
@@ -176,7 +186,7 @@ class OCIBomGenerator:
|
|||||||
"discountable": cat_entry.get("discountable", True),
|
"discountable": cat_entry.get("discountable", True),
|
||||||
"billing_type": cat_entry.get("billing_type", "hourly"),
|
"billing_type": cat_entry.get("billing_type", "hourly"),
|
||||||
"custom_label": custom_label,
|
"custom_label": custom_label,
|
||||||
"custom_note": custom_note,
|
"custom_note": note,
|
||||||
}
|
}
|
||||||
self.line_items.append(item)
|
self.line_items.append(item)
|
||||||
|
|
||||||
@@ -308,12 +318,22 @@ class OCIBomGenerator:
|
|||||||
data_start_row = row
|
data_start_row = row
|
||||||
cat_subtotal_rows = []
|
cat_subtotal_rows = []
|
||||||
|
|
||||||
for cat_key in self.category_order:
|
# Ensure categories present in items but missing from catalog order
|
||||||
|
# (e.g., "other" from unknown SKUs) still render at the end.
|
||||||
|
render_order = list(self.category_order)
|
||||||
|
for cat_key in items_by_cat:
|
||||||
|
if cat_key not in render_order:
|
||||||
|
render_order.append(cat_key)
|
||||||
|
|
||||||
|
for cat_key in render_order:
|
||||||
if cat_key not in items_by_cat:
|
if cat_key not in items_by_cat:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cat_items = items_by_cat[cat_key]
|
cat_items = items_by_cat[cat_key]
|
||||||
display_name = self.category_names.get(cat_key, cat_key)
|
display_name = self.category_names.get(
|
||||||
|
cat_key,
|
||||||
|
"Uncategorized — confirm SKUs" if cat_key == "other" else cat_key,
|
||||||
|
)
|
||||||
|
|
||||||
# Category header row
|
# Category header row
|
||||||
ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font
|
ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font
|
||||||
@@ -579,11 +599,19 @@ class OCIBomGenerator:
|
|||||||
cat = item["category"]
|
cat = item["category"]
|
||||||
items_by_cat.setdefault(cat, []).append(item)
|
items_by_cat.setdefault(cat, []).append(item)
|
||||||
|
|
||||||
for cat_key in self.category_order:
|
render_order = list(self.category_order)
|
||||||
|
for cat_key in items_by_cat:
|
||||||
|
if cat_key not in render_order:
|
||||||
|
render_order.append(cat_key)
|
||||||
|
|
||||||
|
for cat_key in render_order:
|
||||||
if cat_key not in items_by_cat:
|
if cat_key not in items_by_cat:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
display_name = self.category_names.get(cat_key, cat_key)
|
display_name = self.category_names.get(
|
||||||
|
cat_key,
|
||||||
|
"Uncategorized — confirm SKUs" if cat_key == "other" else cat_key,
|
||||||
|
)
|
||||||
|
|
||||||
# Category header
|
# Category header
|
||||||
ws_bom.cell(row=row, column=4, value=display_name).font = cat_font
|
ws_bom.cell(row=row, column=4, value=display_name).font = cat_font
|
||||||
|
|||||||
@@ -1440,12 +1440,6 @@ class OCIDeckGenerator(OraclePresBase):
|
|||||||
contact_info=pick(ns, "contact_info"),
|
contact_info=pick(ns, "contact_info"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Closing slides
|
|
||||||
gen.add_closing_slide(
|
|
||||||
name=meta.get("architect", ""),
|
|
||||||
title=meta.get("firm", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
return gen
|
return gen
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user