From 2b923f1aedbe75187297c6eae5526fe84f26e9a3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Mar 2026 15:19:26 -0300 Subject: [PATCH] Add SA Tools: BOM generator and AppCA export - New tools/oci_bom_gen.py: generates .xlsx BOM from YAML spec with Oracle Redwood styling, Excel formulas, and Oracle disclaimer. Supports standard BOM (--output) and AppCA import format (--appca) - New tools/refresh_sku_catalog.py: refreshes SKU catalog prices from Oracle's public pricing API (apexapps.oracle.com) - New kb/pricing/oci-sku-catalog.yaml: ~160 OCI SKUs across 14 categories, validated against Oracle API - New templates/bom-spec.yaml: input spec template for BOM generation - Add SA TOOLS section to SKILL.md (options 13-14) - Register BOM generator in oci_output.py --- .agents/skills/oci-deal-accelerator/SKILL.md | 28 + .claude/settings.local.json | 16 +- CLAUDE.md | 15 +- SKILL.md | 28 + examples/exacs-bom-appca-v2.xlsx | Bin 0 -> 6642 bytes examples/exacs-bom-appca-v3.xlsx | Bin 0 -> 6641 bytes examples/exacs-bom-appca-v4.xlsx | Bin 0 -> 6647 bytes examples/exacs-bom-appca.xlsx | Bin 0 -> 6606 bytes examples/exacs-bom-v2.xlsx | Bin 0 -> 7022 bytes examples/exacs-bom.xlsx | Bin 0 -> 7080 bytes examples/exacs-x11m-bom-appca.xlsx | Bin 0 -> 6644 bytes examples/exacs-x11m-bom.xlsx | Bin 0 -> 6990 bytes examples/~$exacs-bom.xlsx | Bin 0 -> 165 bytes kb/pricing/oci-sku-catalog.yaml | 1327 ++++++++++++++++++ templates/bom-spec.yaml | 50 + tools/oci_bom_gen.py | 730 ++++++++++ tools/oci_output.py | 8 + tools/refresh_sku_catalog.py | 335 +++++ 18 files changed, 2535 insertions(+), 2 deletions(-) create mode 100644 examples/exacs-bom-appca-v2.xlsx create mode 100644 examples/exacs-bom-appca-v3.xlsx create mode 100644 examples/exacs-bom-appca-v4.xlsx create mode 100644 examples/exacs-bom-appca.xlsx create mode 100644 examples/exacs-bom-v2.xlsx create mode 100644 examples/exacs-bom.xlsx create mode 100644 examples/exacs-x11m-bom-appca.xlsx create mode 100644 examples/exacs-x11m-bom.xlsx create mode 100644 examples/~$exacs-bom.xlsx create mode 100644 kb/pricing/oci-sku-catalog.yaml create mode 100644 templates/bom-spec.yaml create mode 100644 tools/oci_bom_gen.py create mode 100644 tools/refresh_sku_catalog.py diff --git a/.agents/skills/oci-deal-accelerator/SKILL.md b/.agents/skills/oci-deal-accelerator/SKILL.md index a022ce0..bc033d8 100644 --- a/.agents/skills/oci-deal-accelerator/SKILL.md +++ b/.agents/skills/oci-deal-accelerator/SKILL.md @@ -59,6 +59,11 @@ Present these options as a compact numbered list. Each option has a bold title f ───────────────── 12. 📊 ECAL readiness score — *60-artefact gap analysis* + SA TOOLS + ───────────────── + 13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* + 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* + ━━━━━━━━━━━━━━━━━━━━━━━ Pick a number, or just describe what you need. ``` @@ -247,6 +252,29 @@ Pick a number, or just describe what you need. → [D] Re-score after updates ``` +- If the user picks **13**, ask: "What services does the customer need? (e.g., 'ExaCS X11M BYOL 2 DB servers + 4 storage + 128 ECPUs + ADB-S 8 ECPU + 2TB Block Storage + FastConnect 1Gbps'). I'll generate the BOM with only those SKUs." + Then follow the BOM generation flow: + 1. Parse the customer request to identify needed OCI services and quantities + 2. Match services against `kb/pricing/oci-sku-catalog.yaml` to select exact SKUs + 3. Ask for discount % and contract duration if not specified (default: 0%, 12 months) + 4. Ask if currency conversion is needed (e.g., USD→BRL with exchange rate and tax) + 5. Generate the BOM spec YAML and save to the output folder + 6. Run `tools/oci_bom_gen.py` to produce the .xlsx + 7. Present a summary table in the terminal showing key totals (monthly, ARR) + 8. List the files generated + + **BOM Output Rules:** + - NEVER include "Confidential: Internal ONLY" or any confidentiality marking + - ALWAYS include the Oracle Cost Estimator disclaimer at the bottom + - Only include SKUs the customer actually requested — never dump the full catalog + - Show cost proportions so the customer can see where their spend concentrates + - Use Excel formulas (not static values) so the customer can adjust quantities + +- If the user picks **14**, follow the same flow as option 13 (BOM generator) but generate the output in AppCA import format. AppCA is Oracle's internal deal approval tool. The generated .xlsx has two sheets: + - **"Export to AppCA"**: Flat table with columns SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE — ready to paste/import into AppCA + - **"BOM.C1"**: Full BOM detail with product names, metrics, prices, and formulas for cost calculations + Run with: `python tools/oci_bom_gen.py --spec .yaml --output .xlsx --appca` + - If the user sends discovery notes directly (without picking a number), detect this and go straight to option 1 (full proposal flow). - If the user asks a specific question (e.g., "does ADB-S support vector search?"), detect this and go straight to the relevant capability without showing the menu. - Only show the welcome menu on the FIRST message if it's a greeting or empty context. Don't re-show it on every turn. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5229293..8602955 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -60,7 +60,21 @@ "Bash(PYTHONPATH=/tmp/pptx_ok python3 -c \"from pptx import Presentation; print\\(''''OK''''\\)\")", "Bash(PYTHONPATH=/tmp/pptx_ok python3 /tmp/migrate_to_oracle.py)", "Bash(PYTHONPATH=/tmp/pptx_ok python3 -c \"\nfrom pptx import Presentation\nprs = Presentation\\('/mnt/c/Users/Diego/Documents/oci-deal-accelerator/oci-deal-accelerator/templates/Oracle_PPT-template_FY26.pptx'\\)\nprint\\('Template has {} existing slides'.format\\(len\\(prs.slides\\)\\)\\)\n\")", - "Bash(grep:*)" + "Bash(grep:*)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/Oracle_PPT\"*)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/\"*.pptx)", + "Bash(ls \"/mnt/c/Users/Diego/Documents/oci-deal-accelerator-oracle/oci-deal-accelerator/templates/\"*.potx)", + "Bash(python3.12 -c \"import drawpyo\")", + "Bash(python3.12 tools/oci_diagram_gen.py --spec examples/output-meli-kvs/diagram-spec.yaml --output examples/output-meli-kvs/architecture.drawio)", + "Bash(python3.12 tools/oci_pdf_gen.py --spec examples/output-meli-kvs/proposal-spec.yaml --output examples/output-meli-kvs/customer-proposal.pdf)", + "Bash(python3.12 -c \":*)", + "Bash(python3.12 tools/oci_deck_gen.py --spec examples/output-meli-kvs/proposal-spec.yaml --output examples/output-meli-kvs/architecture-proposal.pptx)", + "Bash(python3.12 tools/oci_bizcase_gen.py --spec examples/output-meli-kvs/business-case.yaml --output examples/output-meli-kvs/business-case.pptx)", + "Bash(python3 -c \":*)", + "WebFetch(domain:github.com)", + "Bash(curl -s --max-time 15 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=2&partNumber=B110627\")", + "Bash(curl -v --max-time 10 -H \"X-Oracle-Accept-CurrencyCode: USD\" \"https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products?limit=1\")", + "Bash(curl -s --max-time 15 \"https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/?partNumber=B110627¤cyCode=USD\")" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index ebcdb46..93996b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,7 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ │ └── (dirs: database-ha/, database-dr/, networking-hub-spoke/, etc.) │ ├── sizing/ # CPU conversion ratios, IOPS, scaling rules │ ├── pricing/ # Simplified pricing for estimation +│ │ └── oci-sku-catalog.yaml # ~160 OCI SKUs for BOM generation │ ├── competitive/ # AWS/Azure/GCP service mapping │ ├── well-architected/ # 5-pillar WA Framework checklists │ ├── compatibility/ # Feature matrices (ADB, etc.) @@ -47,7 +48,9 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ ├── oci_deck_gen.py # .pptx slide deck generator (DEFAULT output) │ ├── oci_pdf_gen.py # .pdf customer-facing document (branded, no internal refs) │ ├── oci_diagram_gen.py # .drawio diagram generator +│ ├── oci_bom_gen.py # .xlsx Bill of Materials generator (SA tool) │ ├── oci_output.py # Output orchestrator +│ ├── refresh_sku_catalog.py # SKU catalog refresh from Oracle pricing API │ └── refresh_arch_catalog.py # Architecture Center catalog refresh tool ├── scripts/ # Validation and utilities │ └── validate-architecture.py # WA validation engine @@ -72,7 +75,8 @@ AI skill aligned with Oracle ECAL framework (Define → Design → Deliver) that │ ├── handover-document.yaml # DELIVER: Implementation handover │ ├── go-live-checklist.yaml # DELIVER: Pre-cutover verification │ ├── success-criteria.yaml # DELIVER: Post go-live metrics -│ └── lessons-learned.yaml # DELIVER: Engagement retrospective +│ ├── lessons-learned.yaml # DELIVER: Engagement retrospective +│ └── bom-spec.yaml # SA TOOL: BOM input spec template ├── codex/ # Codex setup guide (README only) └── examples/ # Example specs and generated outputs ``` @@ -115,6 +119,9 @@ python tools/oci_deck_gen.py --spec examples/proposal-spec.yaml --output proposa python tools/oci_pdf_gen.py --spec examples/proposal-spec.yaml --output proposal.pdf python tools/oci_pdf_gen.py --spec examples/proposal-spec.yaml --output proposal.pdf --diagram arch.png +# Generate BOM (.xlsx Bill of Materials) +python tools/oci_bom_gen.py --spec examples/bom-spec.yaml --output customer-bom.xlsx + # Generate diagram python tools/oci_diagram_gen.py --spec examples/diagram-spec.yaml --output arch.drawio @@ -127,6 +134,12 @@ python scripts/validate-architecture.py \ # Output orchestrator (multiple formats at once) python tools/oci_output.py --spec examples/proposal-spec.yaml --format full --output-dir output/ +# Refresh SKU catalog from Oracle pricing API +python tools/refresh_sku_catalog.py --refresh # update all prices +python tools/refresh_sku_catalog.py --refresh --diff # update + show changes +python tools/refresh_sku_catalog.py --validate # check for stale prices +python tools/refresh_sku_catalog.py --sku B110627 # inspect single SKU + # Refresh Architecture Center catalog python tools/refresh_arch_catalog.py --whats-new # crawl What's New pages python tools/refresh_arch_catalog.py --url # add single entry diff --git a/SKILL.md b/SKILL.md index ddc0323..b7020b8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -59,6 +59,11 @@ Present these options as a compact numbered list. Each option has a bold title f ───────────────── 12. 📊 ECAL readiness score — *60-artefact gap analysis* + SA TOOLS + ───────────────── + 13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* + 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* + ━━━━━━━━━━━━━━━━━━━━━━━ Pick a number, or just describe what you need. ``` @@ -264,6 +269,29 @@ Pick a number, or just describe what you need. → [D] Re-score after updates ``` +- If the user picks **13**, ask: "What services does the customer need? (e.g., 'ExaCS X11M BYOL 2 DB servers + 4 storage + 128 ECPUs + ADB-S 8 ECPU + 2TB Block Storage + FastConnect 1Gbps'). I'll generate the BOM with only those SKUs." + Then follow the BOM generation flow: + 1. Parse the customer request to identify needed OCI services and quantities + 2. Match services against `kb/pricing/oci-sku-catalog.yaml` to select exact SKUs + 3. Ask for discount % and contract duration if not specified (default: 0%, 12 months) + 4. Ask if currency conversion is needed (e.g., USD→BRL with exchange rate and tax) + 5. Generate the BOM spec YAML and save to the output folder + 6. Run `tools/oci_bom_gen.py` to produce the .xlsx + 7. Present a summary table in the terminal showing key totals (monthly, ARR) + 8. List the files generated + + **BOM Output Rules:** + - NEVER include "Confidential: Internal ONLY" or any confidentiality marking + - ALWAYS include the Oracle Cost Estimator disclaimer at the bottom + - Only include SKUs the customer actually requested — never dump the full catalog + - Show cost proportions so the customer can see where their spend concentrates + - Use Excel formulas (not static values) so the customer can adjust quantities + +- If the user picks **14**, follow the same flow as option 13 (BOM generator) but generate the output in AppCA import format. AppCA is Oracle's internal deal approval tool. The generated .xlsx has two sheets: + - **"Export to AppCA"**: Flat table with columns SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE — ready to paste/import into AppCA + - **"BOM.C1"**: Full BOM detail with product names, metrics, prices, and formulas for cost calculations + Run with: `python tools/oci_bom_gen.py --spec .yaml --output .xlsx --appca` + - If the user sends discovery notes directly (without picking a number), detect this and go straight to option 1 (full proposal flow). - If the user asks a specific question (e.g., "does ADB-S support vector search?"), detect this and go straight to the relevant capability without showing the menu. - Only show the welcome menu on the FIRST message if it's a greeting or empty context. Don't re-show it on every turn. diff --git a/examples/exacs-bom-appca-v2.xlsx b/examples/exacs-bom-appca-v2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1b3cad3f62526a8ec941589773f57aaa5f14e6b6 GIT binary patch literal 6642 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 JJ>uU_{{tr2!G8b% literal 0 HcmV?d00001 diff --git a/examples/exacs-bom-appca-v3.xlsx b/examples/exacs-bom-appca-v3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..128f90ce50ead9252adab3bdfdff783a0bb3c24a GIT binary patch literal 6641 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 I-tW7A0guSn$N&HU literal 0 HcmV?d00001 diff --git a/examples/exacs-bom-appca-v4.xlsx b/examples/exacs-bom-appca-v4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d3a3b9a7e599a6b41bff0e0b8184f9d88561d5cf GIT binary patch literal 6647 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 WIh1>49{|9({{`PCEzT#u@BR;#g}~wf literal 0 HcmV?d00001 diff --git a/examples/exacs-bom-v2.xlsx b/examples/exacs-bom-v2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..172dd80886ea7103dd9c30deeb9ee8ae6ff9c6d8 GIT binary patch literal 7022 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 azbq>~ExhZd&2<`m{gb_>dqajl-u@5z&b?Rw literal 0 HcmV?d00001 diff --git a/examples/exacs-x11m-bom-appca.xlsx b/examples/exacs-x11m-bom-appca.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c6f7b0f99bc807fa8db62998caf11b8e70f5cf90 GIT binary patch literal 6644 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 literal 0 HcmV?d00001 diff --git a/kb/pricing/oci-sku-catalog.yaml b/kb/pricing/oci-sku-catalog.yaml new file mode 100644 index 0000000..b5fac67 --- /dev/null +++ b/kb/pricing/oci-sku-catalog.yaml @@ -0,0 +1,1327 @@ +last_verified: '2026-03-27' +source: Oracle OCI Price List + internal BOM reference v1.73 +description: 'OCI SKU catalog for BOM generation. ~160 SKUs across 14+ categories. Prices are approximate and for estimation + only. Always verify against https://www.oracle.com/cloud/pricing/ + + ' +currency: USD +disclaimer: 'This sample quote is provided solely for evaluation purposes and is intended to further discussions between you + and Oracle. This sample quote is not eligible for acceptance by you and is not a binding contract between you and Oracle + for the services specified. If you would like to purchase the services specified in this sample quote, please request that + Oracle issue you a formal quote (which may include an OMA or a CSA if you do not already have an appropriate agreement in + place with Oracle) for your acceptance and execution. Your formal quote will be effective only upon Oracle''s acceptance + of the formal quote (and the OMA or CSA, if required). + + ' +categories: + iaas: + display_name: Oracle Cloud Infrastructure - IaaS + skus: + - sku: B97384 + product: Compute - Standard - E5 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.03 + default_hours_units: 730 + billing_type: hourly + - sku: B97385 + product: Compute - Standard - E5 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + - sku: B111129 + product: Compute - Standard - E6 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.03 + default_hours_units: 730 + billing_type: hourly + - sku: B111130 + product: Compute - Standard - E6 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + - sku: B94176 + product: Compute - Standard - X9 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.04 + default_hours_units: 730 + billing_type: hourly + - sku: B94177 + product: Compute - Standard - X9 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B93311 + product: Compute - Optimized - X9 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.054 + default_hours_units: 730 + billing_type: hourly + - sku: B93312 + product: Compute - Optimized - X9 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B93297 + product: 'Compute - Standard - A1 (OCPU) (Free Tier: 4 OCPUs)' + metric: OCPU Per Hour + list_price_usd: 0.01 + default_hours_units: 730 + billing_type: hourly + - sku: B93298 + product: 'Compute - Standard - A1 (Memory) (Free Tier: 24 GB RAM)' + metric: Gigabyte Per Hour + list_price_usd: 0.0015 + default_hours_units: 730 + billing_type: hourly + - sku: B109529 + product: Compute - Standard - A2 (OCPU) + metric: OCPU Per Hour + list_price_usd: 0.014 + default_hours_units: 730 + billing_type: hourly + - sku: B109530 + product: Compute - Standard - A2 (Memory) + metric: Gigabyte Per Hour + list_price_usd: 0.002 + default_hours_units: 730 + billing_type: hourly + gpu: + display_name: Oracle Cloud Infrastructure - GPUs + skus: + - sku: B95909 + product: Compute - GPU - A10 (1, 2 or 4 GPUs NVIDIA Ampere A10 / 24 GB per GPU) + metric: GPU Per Hour + list_price_usd: 2.0 + default_hours_units: 730 + billing_type: hourly + - sku: B109479 + product: Compute - GPU - BM.GPU.L40S.4 (4 GPUs NVIDIA Hopper L40S / 192 GB Total) + metric: GPU Per Hour + list_price_usd: 3.5 + default_hours_units: 730 + billing_type: hourly + - sku: B92740 + product: Compute - GPU - BM.GPU4.8 (8 GPUs NVIDIA Ampere A100 v1 / 320 GB Total) + metric: GPU Per Hour + list_price_usd: 3.05 + default_hours_units: 730 + billing_type: hourly + - sku: B95907 + product: Compute - GPU - BM.GPU.A100-v2.8 (8 GPUs NVIDIA Ampere A100 / 640 GB Total) + metric: GPU Per Hour + list_price_usd: 4.0 + default_hours_units: 730 + billing_type: hourly + - sku: B109485 + product: Compute - GPU - BM.GPU.MI300X.8 (8 GPUs AMD MI300X / 1536 GB Total) + metric: GPU Per Hour + list_price_usd: 6.0 + default_hours_units: 730 + billing_type: hourly + - sku: B98415 + product: Compute - GPU - BM.GPU.H100.8 (8 GPUs NVIDIA Hopper H100 / 640 GB Total) + metric: GPU Per Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110519 + product: Compute - GPU - BM.GPU.H200.8 (8 GPUs NVIDIA Hopper H200 / 1128 GB Total) + metric: GPU Per Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110978 + product: Compute - GPU - BM.GPU.B200.8 (8 GPUs NVIDIA Blackwell B200 / 1440 GB Total) + metric: GPU Per Hour + list_price_usd: 14.0 + default_hours_units: 730 + billing_type: hourly + - sku: B110979 + product: Compute - GPU - BM.GPU.GB200.4 (4 GPUs NVIDIA Blackwell GB200 NVL72 / 768 GB Total) + metric: GPU Per Hour + list_price_usd: 16.0 + default_hours_units: 730 + billing_type: hourly + vmware: + display_name: Oracle Cloud Infrastructure - VMware + skus: + - sku: B95178 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - Hourly Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.2437 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95179 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - Monthly Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.2031 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95180 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - 1 Year Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1625 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95181 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.64 - 3 Year Commit (minimum 192 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.132 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95415 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - Hourly Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.195 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95416 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - Monthly Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1625 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95417 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - 1 Year Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.13 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95418 + product: Oracle Cloud VMware Solution - BM.DenseIO.E4.128 - 3 Year Commit (minimum 384 OCPUs) + metric: OCPU Per Hour + list_price_usd: 0.1056 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108809 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - Hourly Commit (minimum 144 OCPUs) - 8 hours min + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 16.6617 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108810 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - 1 Year Commit (minimum 144 OCPUs) + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 13.1604 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108811 + product: Oracle Cloud VMware Solution - BM.Standard.E5.48 - 3 Year Commit (minimum 144 OCPUs) + metric: Node Per Hour (48 OCPUs each, min x3) + list_price_usd: 10.6119 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110680 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - Hourly Commit (minimum 96 OCPUs) - 8 hours min + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 10.2379 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110681 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - Monthly Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 8.531 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110682 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - 1 Year Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 6.8242 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110683 + product: Oracle Cloud VMware Solution - BM.DenseIO.E5.32 - 3 Year Commit (minimum 96 OCPUs) + metric: Node Per Hour (32 OCPUs each, min x3) + list_price_usd: 6.072 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97105 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - Hourly Commit (minimum 48 OCPUs) - 8 hours min + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 5.554 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97106 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - 1 Year Commit (minimum 48 OCPUs) + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 4.2943 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97107 + product: Oracle Cloud VMware Solution - BM.Standard3.16 - 3 Year Commit (minimum 48 OCPUs) + metric: Node Per Hour (16 OCPUs each, min x3) + list_price_usd: 3.3522 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97111 + product: Oracle Cloud VMware Solution - Expansion - Hourly Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1605 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B110684 + product: Oracle Cloud VMware Solution - Expansion - Monthly Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1338 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97112 + product: Oracle Cloud VMware Solution - Expansion - 1 Year Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.1092 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B97113 + product: Oracle Cloud VMware Solution - Expansion - 3 Year Commit (Add. OCPUs for BM.E5 / X9) + metric: OCPU Per Hour + list_price_usd: 0.0739 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B93421 + product: Oracle Cloud VMware Solution - HCX Enterprise - Monthly + metric: OCPU Per Hour + list_price_usd: 0.0126 + default_hours_units: 730 + billing_type: hourly + discountable: false + software: + display_name: Oracle Cloud Infrastructure - Software + skus: + - sku: B88318 + product: OCI - Compute - Windows OS (license only) + metric: OCPU Per Hour + list_price_usd: 0.092 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B91373 + product: OCI - Compute - Microsoft SQL Standard (LICENSE ONLY, no IaaS, X9.Std3 only) + metric: OCPU Per Hour (minimum 2 OCPUs) + list_price_usd: 0.37 + default_hours_units: 730 + billing_type: hourly + - sku: B91372 + product: OCI - Compute - Microsoft SQL Enterprise (LICENSE ONLY, no IaaS, X7 only) + metric: OCPU Per Hour (minimum 2 OCPUs) + list_price_usd: 1.47 + default_hours_units: 730 + billing_type: hourly + storage: + display_name: Oracle Cloud Infrastructure - Storage + skus: + - sku: B91961 + product: OCI - Block Volume Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0255 + default_hours_units: 1 + billing_type: monthly + - sku: B91962 + product: OCI - Block Volume Performance (VPU) + metric: Performance Units Per Gigabyte Per Month + list_price_usd: 0.0017 + default_hours_units: 1 + billing_type: monthly + - sku: B91628 + product: 'OCI - Object Storage - Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0255 + default_hours_units: 1 + billing_type: monthly + - sku: B91627 + product: 'OCI - Object Storage - Requests (Free Tier: 50000 Requests)' + metric: 10,000 (10K) Requests Per Month + list_price_usd: 0.0034 + default_hours_units: 1 + billing_type: monthly + - sku: B93000 + product: 'OCI - Infrequent Access - Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.01 + default_hours_units: 1 + billing_type: monthly + - sku: B93001 + product: 'OCI - Infrequent Access - Data Retrieval (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.01 + default_hours_units: 1 + billing_type: monthly + - sku: B91633 + product: 'OCI - Archive Storage (Free Tier: 10 GB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0026 + default_hours_units: 1 + billing_type: monthly + - sku: B89057 + product: OCI - File Storage Service + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.3 + default_hours_units: 1 + billing_type: monthly + - sku: B109546 + product: 'OCI - File Storage Service - High Performance Mount Target (Options: 20, 40 or 80 TB)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.3 + default_hours_units: 1 + billing_type: monthly + base_database: + display_name: Oracle Cloud Infrastructure - Base Database (DBCS) + skus: + - sku: B90573 + product: Oracle Base Database Service - BYOL + metric: OCPU Per Hour + list_price_usd: 0.1935 + default_hours_units: 730 + billing_type: hourly + - sku: B90569 + product: Oracle Base Database Service - Standard Edition + metric: OCPU Per Hour + list_price_usd: 0.215 + default_hours_units: 730 + billing_type: hourly + - sku: B90570 + product: Oracle Base Database Service - Enterprise Edition + metric: OCPU Per Hour + list_price_usd: 0.4301 + default_hours_units: 730 + billing_type: hourly + - sku: B90571 + product: Oracle Base Database Service - Enterprise High Performance Edition + metric: OCPU Per Hour + list_price_usd: 0.8871 + default_hours_units: 730 + billing_type: hourly + - sku: B90572 + product: Oracle Base Database Service - Enterprise Extreme Performance Edition + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B97200 + product: Oracle Base Database Service on ARM - BYOL + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.0968 + default_hours_units: 730 + billing_type: hourly + - sku: B97197 + product: Oracle Base Database Service on ARM - Enterprise Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.2151 + default_hours_units: 730 + billing_type: hourly + - sku: B97198 + product: Oracle Base Database Service on ARM - Enterprise High Performance Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.4436 + default_hours_units: 730 + billing_type: hourly + - sku: B97199 + product: Oracle Base Database Service on ARM - Enterprise Extreme Performance Edition + metric: OCPU (1 vCPU, single thread) Per Hour + list_price_usd: 0.6721 + default_hours_units: 730 + billing_type: hourly + - sku: B90230 + product: Database Backup Cloud - Object Storage (add SKU) + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0051 + default_hours_units: 1 + billing_type: monthly + - sku: B90231 + product: Database Backup Cloud - Archive Storage (add SKU) + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0005 + default_hours_units: 1 + billing_type: monthly + autonomous_exadata: + display_name: Oracle Cloud Infrastructure - Autonomous Database & Exadata + skus: + - sku: B95701 + product: Oracle Autonomous Database (ATP or ADW) - ECPU (2 ECPU minimum) + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B95703 + product: Oracle Autonomous Database (ATP or ADW) - BYOL ECPU (2 ECPU minimum) + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + - sku: B95706 + product: Oracle Autonomous Database Storage for Transaction Processing (ATP), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.1156 + default_hours_units: 1 + billing_type: monthly + - sku: B95754 + product: Oracle Autonomous Database Storage for Data Warehouse (ADW), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0244 + default_hours_units: 1 + billing_type: monthly + - sku: B95754a + product: Oracle Autonomous Database Backup Storage (ATP or ADW), using ECPU + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0244 + default_hours_units: 1 + billing_type: monthly + - sku: B92992 + product: OCI - GoldenGate + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B92993 + product: OCI - GoldenGate - BYOL + metric: OCPU Per Hour + list_price_usd: 0.3226 + default_hours_units: 730 + billing_type: hourly + exadata_infra: + display_name: Oracle Cloud Infrastructure - Database Exadata Infrastructure + skus: + - sku: B90777 + product: OCI - Database Exadata Infrastructure - Base System + metric: Hosted Environment Per Hour + list_price_usd: 10.7527 + default_hours_units: 730 + billing_type: hourly + - sku: B92380 + product: OCI - Database Exadata Infrastructure - Quarter Rack - X8M + metric: Hosted Environment Per Hour + list_price_usd: 14.5162 + default_hours_units: 730 + billing_type: hourly + - sku: B93380 + product: OCI - Database Exadata Infrastructure - Quarter Rack - X9M + metric: Hosted Environment Per Hour + list_price_usd: 14.5162 + default_hours_units: 730 + billing_type: hourly + - sku: B88592 + product: OCI - Database Exadata OCPU + metric: OCPU Per Hour + list_price_usd: 1.3441 + default_hours_units: 730 + billing_type: hourly + - sku: B88847 + product: OCI - Database Exadata OCPU - BYOL + metric: OCPU Per Hour + list_price_usd: 0.3226 + default_hours_units: 730 + billing_type: hourly + - sku: B110627 + product: OCI - Exadata Cloud Infrastructure - Database Server - X11M + metric: Hosted Environment Per Hour + list_price_usd: 2.9032 + default_hours_units: 730 + billing_type: hourly + - sku: B110629 + product: OCI - Exadata Cloud Infrastructure - Storage Server - X11M + metric: Hosted Environment Per Hour + list_price_usd: 2.9032 + default_hours_units: 730 + billing_type: hourly + - sku: B110631 + product: OCI - Exadata Database ECPU - Dedicated Infrastructure (X11M) + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B110632 + product: OCI - Exadata Database ECPU - Dedicated Infrastructure - BYOL (X11M) + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + exascale: + display_name: Oracle Cloud Infrastructure - Database Exadata Exascale Infrastructure + skus: + - sku: B109356 + product: Oracle Exadata Exascale Database - ECPU + metric: ECPU Per Hour + list_price_usd: 0.336 + default_hours_units: 730 + billing_type: hourly + - sku: B109357 + product: Oracle Exadata Exascale Database - ECPU BYOL + metric: ECPU Per Hour + list_price_usd: 0.0807 + default_hours_units: 730 + billing_type: hourly + - sku: B109355 + product: Oracle Exadata Exascale RDMA Compute Infrastructure + metric: ECPU Per Hour + list_price_usd: 0.025 + default_hours_units: 730 + billing_type: hourly + - sku: B107951 + product: Oracle Exadata Exascale VM Filesystem Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.0425 + default_hours_units: 1 + billing_type: monthly + - sku: B107952 + product: Oracle Exadata Exascale Smart Database Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.1156 + default_hours_units: 1 + billing_type: monthly + - sku: B109375 + product: Oracle Exadata Exascale Additional Flash Cache + metric: Gigabyte Per Hour + list_price_usd: 0.0005 + default_hours_units: 730 + billing_type: hourly + - sku: B110989 + product: Oracle Globally Distributed Exadata Exascale Database ECPU + metric: ECPU Per Hour + list_price_usd: 0.3864 + default_hours_units: 730 + billing_type: hourly + - sku: B110990 + product: Oracle Globally Distributed Exadata Exascale Database ECPU - BYOL + metric: ECPU Per Hour + list_price_usd: 0.0928 + default_hours_units: 730 + billing_type: hourly + postgresql: + display_name: Oracle Cloud Infrastructure - Database with PostgreSQL + skus: + - sku: B99060 + product: 'OCI - PostgreSQL: OCPU Database with PostgreSQL (additional cost to IaaS)' + metric: OCPU Per Hour + list_price_usd: 0.098 + default_hours_units: 730 + billing_type: hourly + - sku: B99062 + product: 'OCI - PostgreSQL: Database Optimized Storage' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.072 + default_hours_units: 1 + billing_type: monthly + mysql: + display_name: Oracle Cloud Infrastructure - MySQL Database Service (MDS) + skus: + - sku: B108030 + product: MySQL Database - ECPU (includes GB RAM, according to shapes) + metric: ECPU Per Hour + list_price_usd: 0.0366 + default_hours_units: 730 + billing_type: hourly + - sku: B92426 + product: MySQL Database - Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B92483 + product: MySQL Database - Backup Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B96626 + product: 'OCI - HeatWave Node (Options: 32 GB RAM or 512 GB RAM each)' + metric: Node per Hour (bundle 16 GB RAM) + list_price_usd: 0.011 + default_hours_units: 730 + billing_type: hourly + - sku: B96625 + product: OCI - HeatWave - Storage + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: monthly + - sku: B109169 + product: MySQL Database - Outbound Data Transfer - Inter OCI Region + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + networking: + display_name: Oracle Cloud Infrastructure - Networking + skus: + - sku: B88327 + product: 'OCI - Outbound Data Transfer - Originating in North America, Europe, and UK (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.0085 + default_hours_units: 1 + billing_type: monthly + - sku: B93455 + product: 'OCI - Outbound Data Transfer - Originating in APAC, Japan, and South America (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.025 + default_hours_units: 1 + billing_type: monthly + - sku: B93456 + product: 'OCI - Outbound Data Transfer - Originating in Middle East and Africa (Free Tier: 10240 GB)' + metric: Gigabyte Outbound Data Transfer Per Month + list_price_usd: 0.05 + default_hours_units: 1 + billing_type: monthly + - sku: B93030 + product: 'OCI - Load Balancer Base (Free Tier: 1 Load Balancer Instance)' + metric: Load Balancer Hour + list_price_usd: 0.0113 + default_hours_units: 730 + billing_type: hourly + - sku: B93031 + product: 'OCI - Load Balancer Bandwidth (Free Tier: 10 Mbps Per Hour)' + metric: Mbps Per Hour + list_price_usd: 0.0001 + default_hours_units: 730 + billing_type: hourly + - sku: B88325 + product: OCI - FastConnect 1 Gbps + metric: Port Hour + list_price_usd: 0.2125 + default_hours_units: 730 + billing_type: hourly + - sku: B88326 + product: OCI - FastConnect 10 Gbps + metric: Port Hour + list_price_usd: 1.275 + default_hours_units: 730 + billing_type: hourly + - sku: B93126 + product: OCI - FastConnect 100 Gbps + metric: Port Hour + list_price_usd: 10.75 + default_hours_units: 730 + billing_type: hourly + - sku: B88525 + product: OCI - DNS + metric: 1 Million Queries + list_price_usd: 0.85 + default_hours_units: 1 + billing_type: per_unit + - sku: B90327 + product: OCI - DNS Traffic Management + metric: 1 Million Queries + list_price_usd: 4.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B88523 + product: 'OCI - Email Delivery (Free Tier: 3000 Emails Sent)' + metric: 1,000 (1K) Emails Sent + list_price_usd: 0.085 + default_hours_units: 1 + billing_type: per_unit + security: + display_name: Oracle Cloud Infrastructure - Security + skus: + - sku: B94579 + product: 'OCI - Web Application Firewall - Instance (Free Tier: 1 WAF Instance)' + metric: WAF Instance Per Month + list_price_usd: 5.0 + default_hours_units: 1 + billing_type: monthly + - sku: B94277 + product: 'OCI - Web Application Firewall - Requests (Free Tier: 10 Million Incoming Requests)' + metric: 1 Million Incoming Requests Per Month + list_price_usd: 0.6 + default_hours_units: 1 + billing_type: monthly + - sku: B92092 + product: 'OCI - KMS Vault - Key Versions (Free Tier: 20 Key Versions)' + metric: Key Version Per Month + list_price_usd: 0.5334 + default_hours_units: 1 + billing_type: monthly + - sku: B95403 + product: OCI - Network Firewall Instance + metric: Instance Per Hour + list_price_usd: 2.75 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B95404 + product: 'OCI - Network Firewall Data Processing (Free Tier: 10240 GB)' + metric: Gigabyte of Data Processed + list_price_usd: 0.01 + default_hours_units: 730 + billing_type: hourly + discountable: false + observability: + display_name: Oracle Cloud Infrastructure - Observability and Management + skus: + - sku: B90940 + product: 'OCI - Notifications - HTTPS Delivery (Free Tier: 1 Million Delivery Operations)' + metric: 1 Million Delivery Operations + list_price_usd: 0.6 + default_hours_units: 1 + billing_type: per_unit + - sku: B90941 + product: 'OCI - Notifications - Email Delivery (Free Tier: 1000 Emails Sent)' + metric: 1,000 (1K) Emails Sent + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: per_unit + - sku: B93006 + product: 'OCI - Notifications - SMS Outbound to Country Zone 3 [Brazil] (Free Tier: 100 SMS Messages Sent)' + metric: SMS Message Sent + list_price_usd: 0.086 + default_hours_units: 1 + billing_type: per_unit + - sku: B92593 + product: 'OCI - Logging - Storage (Free Tier: 10 GB)' + metric: Gigabyte Log Storage Per Month + list_price_usd: 0.05 + default_hours_units: 1 + billing_type: monthly + - sku: B92941 + product: 'Application Performance Monitoring Service - Tracing Data (Free Tier: 1000 Events Per Hour)' + metric: 100,000 (100K) Events Per Hour + list_price_usd: 0.65 + default_hours_units: 1 + billing_type: per_unit + - sku: B92942 + product: Application Performance Monitoring Service - Synthetic Usage + metric: 10 Monitor Runs Per Hour + list_price_usd: 0.02 + default_hours_units: 1 + billing_type: per_unit + - sku: B95264 + product: Application Performance Monitoring Service - Stack Monitoring - 10 Monitored Resources Per Hour + metric: 10 Monitored Resources Per Hour + list_price_usd: 0.075 + default_hours_units: 1 + billing_type: per_unit + - sku: B92682 + product: Oracle Analytics - Professional (min 10 users) + metric: User Per Month + list_price_usd: 16.0 + default_hours_units: 1 + billing_type: monthly + - sku: B92683 + product: Oracle Analytics - Enterprise (min 10 users) + metric: User Per Month + list_price_usd: 80.0 + default_hours_units: 1 + billing_type: monthly + - sku: B89630 + product: Oracle Analytics - Professional (per OCPU) + metric: OCPU Per Hour + list_price_usd: 1.0753 + default_hours_units: 730 + billing_type: hourly + - sku: B89631 + product: Oracle Analytics - Enterprise (per OCPU) + metric: OCPU Per Hour + list_price_usd: 2.1506 + default_hours_units: 730 + billing_type: hourly + - sku: B95485 + product: OCI - Full Stack Disaster Recovery Service (per OCPU) + metric: OCPUs Per Month + list_price_usd: 0.0128 + default_hours_units: 730 + billing_type: hourly + - sku: B110274 + product: OCI - Full Stack Disaster Recovery Service (per ECPU) + metric: ECPUs Per Month + list_price_usd: 0.0032 + default_hours_units: 730 + billing_type: hourly + - sku: B95240 + product: Oracle Database Zero Data Loss Recovery Service + metric: Virtualized Gigabyte Per Month + list_price_usd: 0.0306 + default_hours_units: 1 + billing_type: monthly + - sku: B95241 + product: Oracle Database Zero Data Loss Autonomous Recovery Service (Real-Time Protection) + metric: Virtualized Gigabyte Per Month + list_price_usd: 0.04 + default_hours_units: 1 + billing_type: monthly + - sku: B90323 + product: OCI - Health Checks - Basic + metric: Endpoints Per Month + list_price_usd: 0.3 + default_hours_units: 730 + billing_type: hourly + - sku: B90925 + product: 'OCI - Monitoring - Ingestion (Free Tier: 500 Million Datapoints)' + metric: 1 Million Datapoints + list_price_usd: 0.0025 + default_hours_units: 730 + billing_type: hourly + - sku: B90926 + product: 'OCI - Monitoring - Retrieval (Free Tier: 1 Billion Datapoints)' + metric: 1 Billion Data Points + list_price_usd: 0.0015 + default_hours_units: 1 + billing_type: per_unit + - sku: B110475 + product: 'OCI - Fleet Application Management Service (Free Tier: 25 Managed Resources)' + metric: Managed Resource Per Month + list_price_usd: 2.604 + default_hours_units: 1 + billing_type: monthly + devops: + display_name: Oracle Cloud Infrastructure - DevOps + skus: + - sku: B92072 + product: OCI - API Gateway - 1,000,000 API Calls + metric: 1 Million API Calls Per Month + list_price_usd: 3.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B90938 + product: OCI - Streaming - PUT or GET + metric: Gigabytes of Data Transferred + list_price_usd: 0.025 + default_hours_units: 1 + billing_type: per_unit + - sku: B90939 + product: OCI - Streaming - Storage + metric: Gigabyte Per Hour + list_price_usd: 0.0002 + default_hours_units: 730 + billing_type: hourly + - sku: B110500 + product: OCI - Streaming With Apache Kafka + metric: OCPU Per Hour + list_price_usd: 0.1 + default_hours_units: 730 + billing_type: hourly + - sku: B90617 + product: 'Oracle Functions - Execution Time (Free Tier: 40 Blocks of 10000 Gigabyte Memory-Seconds)' + metric: 10,000 (10K) Gigabyte Memory-Seconds + list_price_usd: 0.1417 + default_hours_units: 1 + billing_type: per_unit + - sku: B90618 + product: 'Oracle Functions - Invocations (Free Tier: 2 Blocks of 1M Function Invocations)' + metric: 1 Million Function Invocations + list_price_usd: 0.2 + default_hours_units: 1 + billing_type: per_unit + - sku: B89737 + product: 'Oracle NoSQL Database Cloud - Write (Free Tier: 50 Units)' + metric: Write Unit Per Month (2.6M Writes Month) + list_price_usd: 0.1254 + default_hours_units: 1 + billing_type: monthly + - sku: B89738 + product: 'Oracle NoSQL Database Cloud - Read (Free Tier: 50 Units)' + metric: Read Unit Per Month (2.6M Reads Month) + list_price_usd: 0.0064 + default_hours_units: 1 + billing_type: monthly + - sku: B89739 + product: 'Oracle NoSQL Database Cloud - Storage (Free Tier: 25 GB per table and a maximum of 3 tables)' + metric: Gigabyte Storage Capacity Per Month + list_price_usd: 0.066 + default_hours_units: 1 + billing_type: monthly + - sku: B93709 + product: Oracle Cloud Infrastructure Search Service with Open Search HA (Over 2 Data Nodes) + metric: Node Per Hour + list_price_usd: 0.25 + default_hours_units: 730 + billing_type: hourly + - sku: B95697 + product: 'OCI Queue - Requests (Free Tier: 1 Million Requests)' + metric: 1 Million Requests per Month + list_price_usd: 0.22 + default_hours_units: 1 + billing_type: monthly + - sku: B96109 + product: Oracle Container Engine for Kubernetes - Virtual Node (Virtual Node Per Hour) + metric: Virtual Node Per Hour + list_price_usd: 0.015 + default_hours_units: 730 + billing_type: hourly + - sku: B96545 + product: Oracle Container Engine for Kubernetes - Enhanced Cluster (Cluster Per Hour) + metric: Cluster Per Hour + list_price_usd: 0.1 + default_hours_units: 730 + billing_type: hourly + - sku: B95634 + product: 'OCI Logging Analytics - Active Storage (Free Tier: 10 GB) - Tiered Pricing' + metric: Logging Analytics Storage Unit Per Month + list_price_usd: 372.0 + default_hours_units: 1 + billing_type: monthly + - sku: B91346 + product: Oracle WebLogic Server Enterprise Edition for Oracle Cloud Infrastructure + metric: OCPU Per Hour + list_price_usd: 0.2581 + default_hours_units: 730 + billing_type: hourly + - sku: B91347 + product: Oracle WebLogic Suite for Oracle Cloud Infrastructure + metric: OCPU Per Hour + list_price_usd: 0.4646 + default_hours_units: 730 + billing_type: hourly + - sku: B92599 + product: OCI Data Integration (OCI DI) - Data Processed + metric: Gigabyte of Data Processed Per Hour + list_price_usd: 0.04 + default_hours_units: 730 + billing_type: hourly + - sku: B92598 + product: OCI Data Integration (OCI DI) - Workspace Usage + metric: Workspace Usage Per Hour + list_price_usd: 0.16 + default_hours_units: 730 + billing_type: hourly + - sku: B93306 + product: 'OCI Data Integration (OCI DI) - Pipeline Operator Execution (Free Tier: 30 Execution Hours)' + metric: Execution Hour + list_price_usd: 0.3 + default_hours_units: 730 + billing_type: hourly + - sku: B98217 + product: OCI Cache with Redis - Low Memory (up to 10 GB per node, min 2 GB) + metric: Redis Memory Gigabyte Per Hour + list_price_usd: 0.0194 + default_hours_units: 730 + billing_type: hourly + - sku: B99591 + product: OCI Cache with Redis - High Memory (above 10 GB per node, min 2 GB) + metric: Redis Memory Gigabyte Per Hour + list_price_usd: 0.0136 + default_hours_units: 730 + billing_type: hourly + - sku: B89639 + product: Oracle Integration Cloud Service - Standard + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 0.6452 + default_hours_units: 730 + billing_type: hourly + - sku: B89640 + product: Oracle Integration Cloud Service - Enterprise + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 1.2903 + default_hours_units: 730 + billing_type: hourly + - sku: B109559 + product: Oracle Integration Cloud Service - Healthcare + metric: Messages Pack (5,000 Messages Per Hour) + list_price_usd: 1.9355 + default_hours_units: 730 + billing_type: hourly + ai: + display_name: Oracle Cloud Infrastructure - Artificial Intelligence (AI) + skus: + - sku: B90260 + product: OCI - Oracle Digital Assistant Cloud Service + metric: Requests + list_price_usd: 0.0232 + default_hours_units: 730 + billing_type: hourly + - sku: B94973 + product: 'OCI - Vision: Image Analysis (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B94974 + product: 'OCI - Vision: OCR (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B110617 + product: 'OCI - Vision: Stored Video Analysis (Free Tier: 1,000 Minutes)' + metric: Processed Video Minute + list_price_usd: 0.1 + default_hours_units: 1 + billing_type: per_unit + - sku: B94977 + product: 'OCI - Vision: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B94896 + product: 'OCI - Speech (Free Tier: 5 Transcription Hours)' + metric: Transcription Hour + list_price_usd: 0.35 + default_hours_units: 1 + billing_type: per_unit + - sku: B95917 + product: 'OCI - Language: Custom Inferencing' + metric: 1,000 (1K) Transactions + list_price_usd: 3.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B95919 + product: 'OCI - Language: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B95918 + product: 'OCI - Language: Custom Inferencing - Dedicated (Free Tier: 15 Inferencing Unit Hours)' + metric: Inferencing Unit Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B108711 + product: 'OCI - Language: Dedicated Inferencing - Healthcare' + metric: Inferencing Unit Hour + list_price_usd: 20.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B93423 + product: 'OCI - Language: Pre-trained Inferencing (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B95920 + product: 'OCI - Language: Text Translating (Free Tier: 1,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 10.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B96113 + product: 'OCI - Document Understanding: Custom Training (Free Tier: 15 Training Hours)' + metric: Training Hour + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B96112 + product: 'OCI - Document Understanding: Document Extraction (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 10.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B97194 + product: 'OCI - Document Understanding: Custom Document Extraction (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 30.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B96111 + product: 'OCI - Document Understanding: Document Properties (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 0.25 + default_hours_units: 1 + billing_type: per_unit + - sku: B97193 + product: 'OCI - Document Understanding: Custom Document Properties (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.5 + default_hours_units: 1 + billing_type: per_unit + - sku: B96110 + product: 'OCI - Document Understanding: OCR (Free Tier: 5,000 Transactions)' + metric: 1,000 (1K) Transactions + list_price_usd: 1.0 + default_hours_units: 1 + billing_type: per_unit + - sku: B94282 + product: 'OCI - Data Labeling (Free Tier: 1,000 Annotated Data Records)' + metric: Annotated Data Record + list_price_usd: 0.0002 + default_hours_units: 1 + billing_type: per_unit + genai_cohere: + display_name: Oracle Cloud Infrastructure - Generative AI - Cohere + skus: + - sku: B108077 + product: Oracle Cloud Infrastructure Generative AI - Large Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.0156 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108078 + product: Oracle Cloud Infrastructure Generative AI - Small Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.0009 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108079 + product: Oracle Cloud Infrastructure Generative AI - Embed Cohere + metric: 10,000 (10K) Transactions + list_price_usd: 0.001 + default_hours_units: 1 + billing_type: per_unit + discountable: false + - sku: B108082 + product: Oracle Cloud Infrastructure Generative AI - Large Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 24.0 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108083 + product: Oracle Cloud Infrastructure Generative AI - Small Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 6.5 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B108084 + product: Oracle Cloud Infrastructure Generative AI - Embed Cohere - Dedicated + metric: AI Unit Per Hour + list_price_usd: 10.9 + default_hours_units: 730 + billing_type: hourly + discountable: false + - sku: B111015 + product: Oracle Cloud Infrastructure Generative AI - Cohere Rerank - Dedicated + metric: Cluster Hour + list_price_usd: 10.0 + default_hours_units: 730 + billing_type: hourly + discountable: false + genai_llama: + display_name: Oracle Cloud Infrastructure - Generative AI - Llama + skus: + - sku: B108080 + product: Oracle Cloud Infrastructure Generative AI - Large Meta 70B + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B110517 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 3.1 405B + metric: 10,000 (10K) Transactions + list_price_usd: 0.0267 + default_hours_units: 1 + billing_type: per_unit + - sku: B110679 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 3.2 90B Vision + metric: 10,000 (10K) Transactions + list_price_usd: 0.005 + default_hours_units: 1 + billing_type: per_unit + - sku: B111035 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 4 Scout + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B111036 + product: Oracle Cloud Infrastructure Generative AI - Meta Llama 4 Maverick + metric: 10,000 (10K) Transactions + list_price_usd: 0.0018 + default_hours_units: 1 + billing_type: per_unit + - sku: B108085 + product: Oracle Cloud Infrastructure Generative AI - Large Meta - Dedicated + metric: AI Unit Per Hour + list_price_usd: 12.0 + default_hours_units: 730 + billing_type: hourly + genai_agents: + display_name: Oracle Cloud Infrastructure - Generative AI Agents - RAG + skus: + - sku: B110461 + product: Oracle Cloud Infrastructure Generative AI Agents - Retrieval-Augmented Generation (RAG) + metric: 10,000 (10K) Transactions + list_price_usd: 0.003 + default_hours_units: 1 + billing_type: per_unit + - sku: B110462 + product: Oracle Cloud Infrastructure Generative AI Agents - Knowledge Base Storage + metric: Gigabyte Storage Capacity Per HOUR + list_price_usd: 0.0084 + default_hours_units: 730 + billing_type: hourly + - sku: B110463 + product: Oracle Cloud Infrastructure Generative AI Agents - Data Ingestion + metric: 10,000 (10K) Transactions + list_price_usd: 0.0003 + default_hours_units: 1 + billing_type: per_unit + roving_edge: + display_name: Oracle Roving Edge Infrastructure + skus: + - sku: B92615 + product: Roving Edge Device - Compute Optimized - Ruggedized + metric: Resource Possession Per Day + list_price_usd: 160.0 + default_hours_units: 31 + billing_type: daily + - sku: B93039 + product: Roving Edge Device - Compute Optimized - Standard + metric: Resource Possession Per Day + list_price_usd: 160.0 + default_hours_units: 31 + billing_type: daily + - sku: B95228 + product: Roving Edge Ultra + metric: Resource Possession Per Day + list_price_usd: 45.0 + default_hours_units: 31 + billing_type: daily + - sku: B109492 + product: Roving Edge Device - RED.2 Compute + metric: Resource Possession Per Day + list_price_usd: 80.0 + default_hours_units: 31 + billing_type: daily + - sku: B109493 + product: Roving Edge Device - RED.2 GPU + metric: Resource Possession Per Day + list_price_usd: 100.0 + default_hours_units: 31 + billing_type: daily + - sku: B109494 + product: Roving Edge Device - RED.2.STG Storage + metric: Resource Possession Per Day + list_price_usd: 95.0 + default_hours_units: 31 + billing_type: daily + - sku: B109496 + product: Roving Edge Device - RED.2 Ruggedized Case (add-on) + metric: Resource Possession Per Day + list_price_usd: 10.0 + default_hours_units: 31 + billing_type: daily + - sku: B109495 + product: Roving Edge Device - RED.2 Non-Return Fee + metric: Device + list_price_usd: 25000.0 + default_hours_units: 1 + billing_type: one_time + - sku: B93040 + product: Roving Edge Device - Compute Optimized - Unreturnable/Loss Fee + metric: Device + list_price_usd: 45000.0 + default_hours_units: 1 + billing_type: one_time + - sku: B95229 + product: Roving Edge Ultra - Non-Return or Loss Fee + metric: Device + list_price_usd: 35000.0 + default_hours_units: 1 + billing_type: one_time diff --git a/templates/bom-spec.yaml b/templates/bom-spec.yaml new file mode 100644 index 0000000..195f7ea --- /dev/null +++ b/templates/bom-spec.yaml @@ -0,0 +1,50 @@ +# ────────────────────────────────────────────────────────────────── +# OCI Bill of Materials (BOM) — Spec Template +# ────────────────────────────────────────────────────────────────── +# The AI generates this YAML based on the customer's request, +# selecting only the relevant SKUs from kb/pricing/oci-sku-catalog.yaml. +# The BOM generator tool (tools/oci_bom_gen.py) renders it to .xlsx. +# +# Usage: +# python tools/oci_bom_gen.py --spec this-file.yaml --output bom.xlsx + +bom: + # ── Header ────────────────────────────────────────────────────── + customer_name: "" + project_name: "" + prepared_by: "" + date: "" # ISO format: 2026-03-27 + reference_label: "" # e.g., "ACME-BOM-001" + currency: USD # Primary currency + # realm: "OC1 - Commercial" # Optional: OC1 / OC2 / OC3 / OC9 + # service_type: "Pay as You Go" # Optional: PAYG / UCM / Annual Flex + + # ── Optional currency conversion ──────────────────────────────── + # Enable to add converted columns (e.g., USD → BRL with tax) + conversion: + enabled: false + target_currency: BRL + exchange_rate: 5.75 # 1 USD = X target currency + tax_rate: 0.0 # e.g., 0.1385 for 13.85% ISS+PIS+COFINS + + # ── Line items (only requested services) ──────────────────────── + # Each item must reference a SKU from oci-sku-catalog.yaml. + # Only include services the customer actually needs. + line_items: + - sku: "B97384" # Must match oci-sku-catalog.yaml + qty: 4 # Number of instances/units + # hours_units: 730 # Override catalog default if needed + months: 12 # Contract duration + discount: 0.0 # 0.0 to 1.0 (e.g., 0.30 = 30%) + # custom_label: "" # Optional label + # custom_note: "" # Optional note + + - sku: "B97385" + qty: 64 # 64 GB memory + months: 12 + discount: 0.0 + + # ── Notes (optional) ──────────────────────────────────────────── + notes: + - "Pricing based on Ashburn region (OC1)" + - "Discounts subject to Oracle approval" diff --git a/tools/oci_bom_gen.py b/tools/oci_bom_gen.py new file mode 100644 index 0000000..2eee985 --- /dev/null +++ b/tools/oci_bom_gen.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 +""" +OCI Deal Accelerator — Bill of Materials (BOM) Generator + +Generates a professional .xlsx BOM from a YAML spec, resolving SKUs +against the OCI SKU catalog. Only requested services appear in the output. + +Usage: + python oci_bom_gen.py --spec bom-spec.yaml --output customer-bom.xlsx + python oci_bom_gen.py --spec bom-spec.yaml --output bom.xlsx --catalog kb/pricing/oci-sku-catalog.yaml +""" + +import argparse +import os +import sys +from datetime import date +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import yaml +from openpyxl import Workbook +from openpyxl.styles import ( + Alignment, + Border, + Font, + NamedStyle, + PatternFill, + Side, + numbers, +) +from openpyxl.utils import get_column_letter + + +# ── Oracle Redwood palette ──────────────────────────────────────── +class Colors: + TEAL = "2C5967" + TEAL_LIGHT = "E8F0F2" + DARK_BG = "312D2A" + COPPER = "AA643B" + WHITE = "FFFFFF" + GRAY_100 = "F5F4F2" + GRAY_300 = "D4D1CC" + GRAY_500 = "6F6964" + GRAY_700 = "3A3631" + BLACK = "000000" + + +# ── Column definitions ──────────────────────────────────────────── +# Base columns (always present) +BASE_COLS = [ + ("Part (SKU)", 14), + ("Product / Description", 55), + ("Metric", 32), + ("Unit Price (USD)", 16), + ("Hours/Units", 12), + ("Qty", 10), + ("Months", 10), + ("Discount", 10), + ("Monthly USD\n(w/o discount)", 18), + ("Monthly USD\n(w/ discount)", 18), +] + +# Conversion columns (added when conversion is enabled) +# Placeholders — header text is set dynamically with target currency +CONV_COLS = [ + ("Monthly {cur}\n(w/o tax)", 18), + ("Monthly {cur}\n(w/ tax)", 18), +] + +# Always-last column +TAIL_COLS = [ + ("Cost %", 10), +] + + +class OCIBomGenerator: + """Generate OCI Bill of Materials (.xlsx) with Oracle Redwood styling.""" + + def __init__( + self, + customer: str = "", + project: str = "", + prepared_by: str = "", + bom_date: str = "", + reference_label: str = "", + currency: str = "USD", + realm: str = "", + service_type: str = "", + conversion: Optional[dict] = None, + notes: Optional[list] = None, + ): + self.customer = customer + self.project = project + self.prepared_by = prepared_by + self.bom_date = bom_date or date.today().isoformat() + self.reference_label = reference_label + self.currency = currency + self.realm = realm + self.service_type = service_type + self.conversion = conversion or {"enabled": False} + self.notes = notes or [] + + self.catalog: dict = {} # sku_code -> {product, metric, ...} + self.catalog_meta: dict = {} # top-level catalog metadata + self.category_order: list = [] # ordered category keys + self.category_names: dict = {} # cat_key -> display_name + self.line_items: list = [] # resolved line items + self.wb = Workbook() + + # ── Catalog loading ─────────────────────────────────────────── + + def load_catalog(self, catalog_path: Optional[str] = None): + """Load SKU catalog from YAML. Auto-discovers path if not given.""" + if not catalog_path: + # Auto-discover relative to this script + tools_dir = Path(__file__).resolve().parent + catalog_path = str(tools_dir.parent / "kb" / "pricing" / "oci-sku-catalog.yaml") + + with open(catalog_path, "r") as f: + raw = yaml.safe_load(f) + + self.catalog_meta = { + k: v for k, v in raw.items() if k != "categories" + } + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + self.category_order.append(cat_key) + self.category_names[cat_key] = cat_data.get("display_name", cat_key) + for sku_entry in cat_data.get("skus", []): + sku_code = str(sku_entry["sku"]) + self.catalog[sku_code] = { + **sku_entry, + "sku": sku_code, + "category": cat_key, + } + + # ── Line items ──────────────────────────────────────────────── + + def add_line_item( + self, + sku: str, + qty: float = 0, + hours_units: Optional[float] = None, + months: int = 12, + discount: float = 0.0, + custom_label: str = "", + custom_note: str = "", + ): + """Add a line item, resolving SKU details from catalog.""" + sku = str(sku) + cat_entry = self.catalog.get(sku, {}) + + item = { + "sku": sku, + "product": cat_entry.get("product", f"Unknown SKU ({sku})"), + "metric": cat_entry.get("metric", ""), + "list_price_usd": cat_entry.get("list_price_usd", 0), + "hours_units": hours_units if hours_units is not None else cat_entry.get("default_hours_units", 730), + "qty": qty, + "months": months, + "discount": discount, + "category": cat_entry.get("category", "other"), + "discountable": cat_entry.get("discountable", True), + "billing_type": cat_entry.get("billing_type", "hourly"), + "custom_label": custom_label, + "custom_note": custom_note, + } + self.line_items.append(item) + + # ── Excel generation ────────────────────────────────────────── + + def _col_defs(self) -> List[Tuple[str, int]]: + """Return full column definitions based on conversion setting.""" + cols = list(BASE_COLS) + if self.conversion.get("enabled"): + cur = self.conversion.get("target_currency", "BRL") + cols += [(h.format(cur=cur), w) for h, w in CONV_COLS] + cols += list(TAIL_COLS) + return cols + + def _col_count(self) -> int: + return len(self._col_defs()) + + def _build_sheet(self, ws): + """Build the complete BOM sheet.""" + ws.title = "BOM" + col_defs = self._col_defs() + num_cols = len(col_defs) + conv = self.conversion.get("enabled", False) + + # ── Styles ──────────────────────────────────────────────── + thin_border = Border( + left=Side(style="thin", color=Colors.GRAY_300), + right=Side(style="thin", color=Colors.GRAY_300), + top=Side(style="thin", color=Colors.GRAY_300), + bottom=Side(style="thin", color=Colors.GRAY_300), + ) + header_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.WHITE) + header_fill = PatternFill(start_color=Colors.TEAL, end_color=Colors.TEAL, fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center", wrap_text=True) + + cat_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.TEAL) + cat_fill = PatternFill(start_color=Colors.TEAL_LIGHT, end_color=Colors.TEAL_LIGHT, fill_type="solid") + + data_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + alt_fill = PatternFill(start_color=Colors.GRAY_100, end_color=Colors.GRAY_100, fill_type="solid") + + total_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.DARK_BG) + total_fill = PatternFill(start_color=Colors.GRAY_300, end_color=Colors.GRAY_300, fill_type="solid") + + subtotal_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700) + + # ── Header area ─────────────────────────────────────────── + row = 1 + title_font = Font(name="Segoe UI", size=16, bold=True, color=Colors.TEAL) + ws.cell(row=row, column=1, value="Oracle Investment Proposal").font = title_font + ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=num_cols) + row += 1 + + subtitle_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_500) + ws.cell(row=row, column=1, value=f"as of {self.bom_date}").font = subtitle_font + row += 1 + + meta_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + meta_items = [ + ("Customer", self.customer), + ("Project", self.project), + ("Prepared by", self.prepared_by), + ("Reference", self.reference_label), + ("Currency", self.currency), + ("Realm", self.realm), + ("Service Type", self.service_type), + ] + for label, value in meta_items: + if value: + ws.cell(row=row, column=1, value=f"{label}:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=value).font = meta_font + row += 1 + + if conv: + xr = self.conversion.get("exchange_rate", 1) + tc = self.conversion.get("target_currency", "BRL") + tr = self.conversion.get("tax_rate", 0) + ws.cell(row=row, column=1, value="Exchange Rate:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=f"1 USD = {xr} {tc}").font = meta_font + row += 1 + if tr > 0: + ws.cell(row=row, column=1, value="Tax Rate:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_700 + ) + ws.cell(row=row, column=2, value=f"{tr*100:.2f}%").font = meta_font + row += 1 + + row += 1 # blank row before table + + # ── Column headers ──────────────────────────────────────── + header_row = row + for ci, (col_name, col_width) in enumerate(col_defs, 1): + cell = ws.cell(row=row, column=ci, value=col_name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws.column_dimensions[get_column_letter(ci)].width = col_width + row += 1 + + # ── Group items by category ─────────────────────────────── + items_by_cat: dict[str, list] = {} + for item in self.line_items: + cat = item["category"] + items_by_cat.setdefault(cat, []).append(item) + + # Column index references (1-based) + COL_SKU = 1 + COL_PRODUCT = 2 + COL_METRIC = 3 + COL_PRICE = 4 + COL_HOURS = 5 + COL_QTY = 6 + COL_MONTHS = 7 + COL_DISC = 8 + COL_M_WO = 9 # monthly without discount + COL_M_W = 10 # monthly with discount + if conv: + COL_CONV_WO = 11 # monthly converted without tax + COL_CONV_W = 12 # monthly converted with tax + COL_PCT = 13 # cost proportion + else: + COL_PCT = 11 + + data_start_row = row + cat_subtotal_rows = [] + + for cat_key in self.category_order: + if cat_key not in items_by_cat: + continue + + cat_items = items_by_cat[cat_key] + display_name = self.category_names.get(cat_key, cat_key) + + # Category header row + ws.cell(row=row, column=COL_PRODUCT, value=display_name).font = cat_font + for ci in range(1, num_cols + 1): + ws.cell(row=row, column=ci).fill = cat_fill + ws.cell(row=row, column=ci).border = thin_border + cat_header_row = row + row += 1 + + cat_first_data = row + + for idx, item in enumerate(cat_items): + # Static data + ws.cell(row=row, column=COL_SKU, value=item["sku"]) + ws.cell(row=row, column=COL_PRODUCT, value=item["product"]) + ws.cell(row=row, column=COL_METRIC, value=item["metric"]) + ws.cell(row=row, column=COL_PRICE, value=item["list_price_usd"]) + ws.cell(row=row, column=COL_HOURS, value=item["hours_units"]) + ws.cell(row=row, column=COL_QTY, value=item["qty"]) + ws.cell(row=row, column=COL_MONTHS, value=item["months"]) + ws.cell(row=row, column=COL_DISC, value=item["discount"]) + + # Formula: Monthly w/o discount = Price × Hours × Qty + p = get_column_letter(COL_PRICE) + h = get_column_letter(COL_HOURS) + q = get_column_letter(COL_QTY) + d = get_column_letter(COL_DISC) + ws.cell( + row=row, column=COL_M_WO, + value=f"={p}{row}*{h}{row}*{q}{row}", + ) + + # Formula: Monthly w/ discount = Monthly_wo × (1 - Discount) + mwo = get_column_letter(COL_M_WO) + ws.cell( + row=row, column=COL_M_W, + value=f"={mwo}{row}*(1-{d}{row})", + ) + + # Conversion formulas + if conv: + xr = self.conversion.get("exchange_rate", 1) + tr = self.conversion.get("tax_rate", 0) + mw = get_column_letter(COL_M_W) + # Converted w/o tax + ws.cell( + row=row, column=COL_CONV_WO, + value=f"={mw}{row}*{xr}", + ) + # Converted w/ tax + ws.cell( + row=row, column=COL_CONV_W, + value=f"={get_column_letter(COL_CONV_WO)}{row}*(1+{tr})", + ) + + # Styling per row + is_alt = idx % 2 == 1 + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = data_font + cell.border = thin_border + if is_alt: + cell.fill = alt_fill + cell.alignment = Alignment( + vertical="center", + wrap_text=(ci == COL_PRODUCT), + ) + + # Number formats + ws.cell(row=row, column=COL_PRICE).number_format = '$#,##0.0000' + ws.cell(row=row, column=COL_HOURS).number_format = '#,##0' + ws.cell(row=row, column=COL_QTY).number_format = '#,##0.##' + ws.cell(row=row, column=COL_MONTHS).number_format = '#,##0' + ws.cell(row=row, column=COL_DISC).number_format = '0%' + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + if conv: + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + row += 1 + + # Category subtotal row + cat_last_data = row - 1 + ws.cell(row=row, column=COL_PRODUCT, value=f"Subtotal — {display_name}") + mwo_letter = get_column_letter(COL_M_WO) + mw_letter = get_column_letter(COL_M_W) + ws.cell(row=row, column=COL_M_WO, value=f"=SUM({mwo_letter}{cat_first_data}:{mwo_letter}{cat_last_data})") + ws.cell(row=row, column=COL_M_W, value=f"=SUM({mw_letter}{cat_first_data}:{mw_letter}{cat_last_data})") + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + + if conv: + cwo_letter = get_column_letter(COL_CONV_WO) + cw_letter = get_column_letter(COL_CONV_W) + ws.cell(row=row, column=COL_CONV_WO, value=f"=SUM({cwo_letter}{cat_first_data}:{cwo_letter}{cat_last_data})") + ws.cell(row=row, column=COL_CONV_W, value=f"=SUM({cw_letter}{cat_first_data}:{cw_letter}{cat_last_data})") + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = subtotal_font + cell.border = thin_border + cell.fill = PatternFill(start_color="EDE9E5", end_color="EDE9E5", fill_type="solid") + + cat_subtotal_rows.append(row) + + row += 1 # blank row between categories + + # ── Grand total ─────────────────────────────────────────── + total_row = row + ws.cell(row=row, column=COL_PRODUCT, value="TOTAL").font = total_font + + # Sum of subtotals + mwo_refs = "+".join(f"{get_column_letter(COL_M_WO)}{r}" for r in cat_subtotal_rows) + mw_refs = "+".join(f"{get_column_letter(COL_M_W)}{r}" for r in cat_subtotal_rows) + ws.cell(row=row, column=COL_M_WO, value=f"={mwo_refs}") + ws.cell(row=row, column=COL_M_W, value=f"={mw_refs}") + ws.cell(row=row, column=COL_M_WO).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + + if conv: + cwo_refs = "+".join(f"{get_column_letter(COL_CONV_WO)}{r}" for r in cat_subtotal_rows) + cw_refs = "+".join(f"{get_column_letter(COL_CONV_W)}{r}" for r in cat_subtotal_rows) + ws.cell(row=row, column=COL_CONV_WO, value=f"={cwo_refs}") + ws.cell(row=row, column=COL_CONV_W, value=f"={cw_refs}") + ws.cell(row=row, column=COL_CONV_WO).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + + for ci in range(1, num_cols + 1): + cell = ws.cell(row=row, column=ci) + cell.font = total_font + cell.fill = total_fill + cell.border = thin_border + row += 1 + + # ── ARR (Annual Run Rate) ──────────────────────────────── + arr_row = row + ws.cell(row=row, column=COL_PRODUCT, value="Annual Run Rate (ARR)").font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + mw_total = get_column_letter(COL_M_W) + ws.cell(row=row, column=COL_M_W, value=f"={mw_total}{total_row}*12") + ws.cell(row=row, column=COL_M_W).number_format = '$#,##0.00' + ws.cell(row=row, column=COL_M_W).font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + if conv: + cw_total = get_column_letter(COL_CONV_W) + ws.cell(row=row, column=COL_CONV_W, value=f"={cw_total}{total_row}*12") + ws.cell(row=row, column=COL_CONV_W).number_format = '#,##0.00' + ws.cell(row=row, column=COL_CONV_W).font = Font( + name="Segoe UI", size=11, bold=True, color=Colors.COPPER + ) + for ci in range(1, num_cols + 1): + ws.cell(row=row, column=ci).border = thin_border + row += 2 + + # ── 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 + + # ── Notes ───────────────────────────────────────────────── + if self.notes: + ws.cell(row=row, column=1, value="Notes:").font = Font( + name="Segoe UI", size=10, bold=True, color=Colors.GRAY_500 + ) + row += 1 + for note in self.notes: + ws.cell(row=row, column=1, value=f" - {note}").font = Font( + name="Segoe UI", size=9, color=Colors.GRAY_500 + ) + row += 1 + row += 1 + + # ── Disclaimer ──────────────────────────────────────────── + disclaimer = self.catalog_meta.get("disclaimer", "") + if disclaimer: + ws.cell(row=row, column=1, value="Disclaimer:").font = Font( + name="Segoe UI", size=8, bold=True, color=Colors.GRAY_500 + ) + row += 1 + ws.merge_cells( + start_row=row, start_column=1, + end_row=row, end_column=num_cols, + ) + cell = ws.cell(row=row, column=1, value=disclaimer.strip()) + cell.font = Font(name="Segoe UI", size=8, color=Colors.GRAY_500) + cell.alignment = Alignment(wrap_text=True, vertical="top") + ws.row_dimensions[row].height = 60 + + # ── Print setup ─────────────────────────────────────────── + ws.sheet_properties.pageSetUpPr = None + ws.page_setup.orientation = "landscape" + ws.page_setup.fitToWidth = 1 + ws.page_setup.fitToHeight = 0 + ws.print_title_rows = f"{header_row}:{header_row}" + + # ── AppCA Export ────────────────────────────────────────────── + + def _build_appca_sheets(self): + """Build the two sheets needed for AppCA import: 'Export to AppCA' and 'BOM.C1'.""" + + # ── Styles ──────────────────────────────────────────────── + thin_border = Border( + left=Side(style="thin", color=Colors.GRAY_300), + right=Side(style="thin", color=Colors.GRAY_300), + top=Side(style="thin", color=Colors.GRAY_300), + bottom=Side(style="thin", color=Colors.GRAY_300), + ) + header_font = Font(name="Segoe UI", size=11, bold=True, color=Colors.WHITE) + header_fill = PatternFill(start_color=Colors.TEAL, end_color=Colors.TEAL, fill_type="solid") + header_align = Alignment(horizontal="center", vertical="center", wrap_text=True) + data_font = Font(name="Segoe UI", size=10, color=Colors.GRAY_700) + cat_font = Font(name="Segoe UI", size=10, bold=True, color=Colors.TEAL) + cat_fill = PatternFill(start_color=Colors.TEAL_LIGHT, end_color=Colors.TEAL_LIGHT, fill_type="solid") + + # ── Sheet: BOM.C1 ───────────────────────────────────────── + ws_bom = self.wb.create_sheet("BOM.C1") + + # Row 1: global parameters + ws_bom.cell(row=1, column=6, value="Hours: ").font = Font(name="Segoe UI", size=10, bold=True) + ws_bom.cell(row=1, column=7, value=730) + ws_bom.cell(row=1, column=8, value="Months: ").font = Font(name="Segoe UI", size=10, bold=True) + months_val = self.line_items[0]["months"] if self.line_items else 12 + ws_bom.cell(row=1, column=9, value=months_val) + + # Row 4: title + ws_bom.cell(row=4, column=4, value="Oracle Cloud Infrastructure - Bill of Material (BOM)").font = Font( + name="Segoe UI", size=12, bold=True, color=Colors.TEAL + ) + + # Row 5: column headers (matching original BOM.C1 layout: C=SKU, D=Product, etc.) + bom_headers = [ + (3, "SKU", 14), (4, "Product", 50), (5, "Metric", 30), + (6, "Price List US$", 14), (7, "Hours/Units", 12), + (8, "Qty", 10), (9, "Months", 10), (10, "Discount", 10), + (11, "US$\n(w/o discount)", 16), (12, "US$\n(w/ discount)", 16), + ] + for col, name, width in bom_headers: + cell = ws_bom.cell(row=5, column=col, value=name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws_bom.column_dimensions[get_column_letter(col)].width = width + + # Row 6+: data rows grouped by category + row = 6 + items_by_cat = {} + for item in self.line_items: + cat = item["category"] + items_by_cat.setdefault(cat, []).append(item) + + for cat_key in self.category_order: + if cat_key not in items_by_cat: + continue + + display_name = self.category_names.get(cat_key, cat_key) + + # Category header + ws_bom.cell(row=row, column=4, value=display_name).font = cat_font + for ci in range(3, 13): + ws_bom.cell(row=row, column=ci).fill = cat_fill + ws_bom.cell(row=row, column=ci).border = thin_border + row += 1 + + for item in items_by_cat[cat_key]: + ws_bom.cell(row=row, column=3, value=item["sku"]) + ws_bom.cell(row=row, column=4, value=item["product"]) + ws_bom.cell(row=row, column=5, value=item["metric"]) + ws_bom.cell(row=row, column=6, value=item["list_price_usd"]) + ws_bom.cell(row=row, column=7, value=item["hours_units"]) + ws_bom.cell(row=row, column=8, value=item["qty"]) + ws_bom.cell(row=row, column=9, value=item["months"]) + ws_bom.cell(row=row, column=10, value=item["discount"]) + + # Formulas matching BOM.C1 style + ws_bom.cell(row=row, column=11, + value="=(F{r}*G{r}*H{r})".format(r=row)) + ws_bom.cell(row=row, column=12, + value="=IFERROR(K{r}*(1-J{r}),K{r})".format(r=row)) + + # Formats + ws_bom.cell(row=row, column=6).number_format = '#,##0.0000' + ws_bom.cell(row=row, column=10).number_format = '0.0%' + ws_bom.cell(row=row, column=11).number_format = '$#,##0.00' + ws_bom.cell(row=row, column=12).number_format = '$#,##0.00' + + for ci in range(3, 13): + cell = ws_bom.cell(row=row, column=ci) + cell.font = data_font + cell.border = thin_border + row += 1 + + # ── Sheet: Export to AppCA ───────────────────────────────── + ws_appca = self.wb.create_sheet("Export to AppCA", 0) # first sheet + + # Title + ws_appca.cell(row=1, column=1, value="TEMPLATE BOM - APPCA").font = Font( + name="Segoe UI", size=14, bold=True, color=Colors.TEAL + ) + ws_appca.merge_cells("A1:G1") + + # Headers — AppCA expects: SKU, QTY, HOURS, MONTHS, DISCOUNT, BURSTABLE, STATUS + appca_headers = ["SKU", "QTY", "HOURS", "MONTHS", "DISCOUNT", "BURSTABLE", "STATUS"] + appca_widths = [14, 10, 12, 10, 12, 10, 12] + for ci, (name, width) in enumerate(zip(appca_headers, appca_widths), 1): + cell = ws_appca.cell(row=2, column=ci, value=name) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_align + cell.border = thin_border + ws_appca.column_dimensions[get_column_letter(ci)].width = width + + # Data rows: only items with qty > 0 + appca_row = 3 + for item in self.line_items: + if item["qty"] > 0: + ws_appca.cell(row=appca_row, column=1, value=item["sku"]) + ws_appca.cell(row=appca_row, column=2, value=item["qty"]) + ws_appca.cell(row=appca_row, column=3, value=item["hours_units"]) + ws_appca.cell(row=appca_row, column=4, value=item["months"]) + ws_appca.cell(row=appca_row, column=5, value=item["discount"]) + ws_appca.cell(row=appca_row, column=6, value="No") # BURSTABLE: No unless specified + ws_appca.cell(row=appca_row, column=7, value="Yes") # STATUS: required by AppCA + + ws_appca.cell(row=appca_row, column=5).number_format = '0.0%' + + for ci in range(1, 8): + cell = ws_appca.cell(row=appca_row, column=ci) + cell.font = data_font + cell.border = thin_border + appca_row += 1 + + # ── Public API ──────────────────────────────────────────────── + + def save(self, filepath: str, appca: bool = False): + """Build and save the workbook to an .xlsx file.""" + if appca: + # Remove default empty sheet, build AppCA sheets + default_sheet = self.wb.active + self._build_appca_sheets() + self.wb.remove(default_sheet) + else: + ws = self.wb.active + self._build_sheet(ws) + self.wb.save(filepath) + print(" BOM saved: {}".format(filepath)) + + @classmethod + def from_spec(cls, spec: dict, catalog_path: Optional[str] = None) -> "OCIBomGenerator": + """Build BOM from a YAML specification dict.""" + bom = spec.get("bom", spec) + + gen = cls( + customer=bom.get("customer_name", ""), + project=bom.get("project_name", ""), + prepared_by=bom.get("prepared_by", ""), + bom_date=bom.get("date", ""), + reference_label=bom.get("reference_label", ""), + currency=bom.get("currency", "USD"), + realm=bom.get("realm", ""), + service_type=bom.get("service_type", ""), + conversion=bom.get("conversion"), + notes=bom.get("notes"), + ) + + gen.load_catalog(catalog_path) + + for item in bom.get("line_items", []): + gen.add_line_item( + sku=str(item["sku"]), + qty=item.get("qty", 0), + hours_units=item.get("hours_units"), + months=item.get("months", 12), + discount=item.get("discount", 0.0), + custom_label=item.get("custom_label", ""), + custom_note=item.get("custom_note", ""), + ) + + return gen + + +def main(): + parser = argparse.ArgumentParser( + description="OCI Deal Accelerator — BOM Generator (.xlsx)" + ) + parser.add_argument( + "--spec", required=True, + help="Path to BOM spec YAML file", + ) + parser.add_argument( + "--output", default="bom.xlsx", + help="Output .xlsx file path (default: bom.xlsx)", + ) + parser.add_argument( + "--catalog", default=None, + help="Override path to SKU catalog YAML (default: auto-discover)", + ) + parser.add_argument( + "--appca", action="store_true", + help="Generate AppCA import format (2 sheets: 'Export to AppCA' + 'BOM.C1')", + ) + args = parser.parse_args() + + with open(args.spec, "r") as f: + spec = yaml.safe_load(f) + + gen = OCIBomGenerator.from_spec(spec, catalog_path=args.catalog) + gen.save(args.output, appca=args.appca) + + +if __name__ == "__main__": + main() diff --git a/tools/oci_output.py b/tools/oci_output.py index a421141..1c6501c 100644 --- a/tools/oci_output.py +++ b/tools/oci_output.py @@ -27,6 +27,7 @@ FORMATS = { "full": ["pptx", "drawio", "docx", "xlsx", "pdf"], "doc": ["docx"], "pdf": ["pdf"], + "bom": ["bom"], } GENERATORS = { @@ -62,6 +63,13 @@ GENERATORS = { "method": "from_spec", "description": "Customer-facing PDF (.pdf)", }, + "bom": { + "module": "oci_bom_gen", + "class": "OCIBomGenerator", + "method": "from_spec", + "description": "Bill of Materials (.xlsx)", + "optional": True, + }, } diff --git a/tools/refresh_sku_catalog.py b/tools/refresh_sku_catalog.py new file mode 100644 index 0000000..6667500 --- /dev/null +++ b/tools/refresh_sku_catalog.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +OCI Deal Accelerator — SKU Catalog Refresh Tool + +Fetches current OCI SKU pricing from Oracle's public pricing API and +updates kb/pricing/oci-sku-catalog.yaml with the latest prices. + +API endpoint (anonymous, no auth required): + https://itra.oraclecloud.com/itas/.anon/myservices/api/v1/products + +Usage: + # Refresh full catalog from API + python tools/refresh_sku_catalog.py --refresh + + # Refresh and show what changed + python tools/refresh_sku_catalog.py --refresh --diff + + # Fetch a specific SKU to inspect + python tools/refresh_sku_catalog.py --sku B110627 + + # Export raw API dump to JSON (for debugging) + python tools/refresh_sku_catalog.py --dump oci-skus-raw.json + + # Validate current catalog against API (report stale prices) + python tools/refresh_sku_catalog.py --validate +""" + +import argparse +import json +import os +import sys +import time +from datetime import date +from pathlib import Path + +try: + import requests +except ImportError: + print("Error: 'requests' package required. Install with: pip install requests") + sys.exit(1) + +import yaml + +# ── API Configuration ───────────────────────────────────────────── +# Public Oracle pricing API (no authentication required) +# Documented: https://docs.oracle.com/en-us/iaas/Content/GSG/Tasks/signingup_topic-Estimating_Costs.htm +API_BASE = "https://apexapps.oracle.com/pls/apex/cetools/api/v1/products/" +DEFAULT_CURRENCY = "USD" + +# ── Paths ───────────────────────────────────────────────────────── +TOOLS_DIR = Path(__file__).resolve().parent +CATALOG_PATH = TOOLS_DIR.parent / "kb" / "pricing" / "oci-sku-catalog.yaml" + + +def fetch_all_products(currency="USD", verbose=False): + """Fetch all OCI products from the APEX pricing API.""" + url = "{}?currencyCode={}".format(API_BASE, currency) + if verbose: + print(" Fetching: {}".format(url)) + + resp = requests.get(url, timeout=60) + resp.raise_for_status() + data = resp.json() + + items = data.get("items", []) + if verbose: + print(" Fetched {} products".format(len(items))) + last_updated = data.get("lastUpdated", "unknown") + print(" API last updated: {}".format(last_updated)) + return items + + +def fetch_single_sku(part_number, currency="USD"): + """Fetch a single SKU by part number.""" + url = "{}?partNumber={}¤cyCode={}".format(API_BASE, part_number, currency) + resp = requests.get(url, timeout=15) + resp.raise_for_status() + data = resp.json() + return data.get("items", []) + + +def extract_payg_price(product, currency="USD"): + """Extract the Pay As You Go price from a product's prices structure. + + APEX API format: + currencyCodeLocalizations: [ + { currencyCode: "USD", prices: [ {model: "PAY_AS_YOU_GO", value: 2.9032} ] } + ] + """ + localizations = product.get("currencyCodeLocalizations", []) + for loc in localizations: + if loc.get("currencyCode") == currency: + for price in loc.get("prices", []): + if price.get("model") == "PAY_AS_YOU_GO": + return price.get("value", 0) + # Fallback: take first price in this currency + prices = loc.get("prices", []) + if prices: + return prices[0].get("value", 0) + + # Legacy format fallback (itra API) + for price in product.get("prices", []): + if price.get("model") == "PAY_AS_YOU_GO": + return price.get("value", 0) + + return 0 + + +def load_current_catalog(): + """Load the current YAML catalog and build a SKU lookup.""" + if not CATALOG_PATH.exists(): + return {}, {} + + with open(str(CATALOG_PATH), "r") as f: + raw = yaml.safe_load(f) + + sku_map = {} # sku -> {product, category, ...} + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku_map[str(entry["sku"])] = { + "category": cat_key, + "product": entry.get("product", ""), + "list_price_usd": entry.get("list_price_usd", 0), + } + + return raw, sku_map + + +def refresh_catalog(verbose=False): + """Refresh the catalog YAML with latest API prices.""" + print("Fetching current prices from Oracle API...") + products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose) + + # Build API lookup: partNumber -> product data + api_map = {} + for p in products: + pn = p.get("partNumber", "") + if pn: + api_map[pn] = p + + print(" {} products from API".format(len(api_map))) + + # Load current catalog + raw, sku_map = load_current_catalog() + if not raw: + print("Error: Could not load catalog at {}".format(CATALOG_PATH)) + return + + updated = 0 + not_found = 0 + unchanged = 0 + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku = str(entry["sku"]) + if sku in api_map: + api_price = extract_payg_price(api_map[sku]) + old_price = entry.get("list_price_usd", 0) + + # Skip $0 API prices when catalog has a real price + # (API returns $0 for tiered/free-tier SKUs where + # the base tier is free but usage above free tier costs) + if api_price == 0 and old_price > 0: + if verbose: + print(" {} SKIP: API=$0 but catalog=${} (tiered/free-tier SKU)".format( + sku, old_price + )) + unchanged += 1 + elif abs(api_price - old_price) > 0.0001: + if verbose: + print(" {} price: {} -> {} ({})".format( + sku, old_price, api_price, entry.get("product", "")[:50] + )) + entry["list_price_usd"] = api_price + updated += 1 + else: + unchanged += 1 + + # Also update product name if different + api_name = api_map[sku].get("displayName", "") + if api_name and api_name != entry.get("product", ""): + # Keep our shorter names, but log the difference + pass + + # Update metric from API + api_metric = api_map[sku].get("metricDisplayName", "") + if api_metric and api_metric != entry.get("metric", ""): + pass # Keep our curated metrics + else: + not_found += 1 + if verbose: + print(" {} NOT in API (may be retired): {}".format( + sku, entry.get("product", "")[:50] + )) + + # Update last_verified date + raw["last_verified"] = date.today().isoformat() + + # Write back + with open(str(CATALOG_PATH), "w") as f: + yaml.dump(raw, f, default_flow_style=False, allow_unicode=True, sort_keys=False, width=120) + + print("\nRefresh complete:") + print(" Updated: {}".format(updated)) + print(" Unchanged: {}".format(unchanged)) + print(" Not in API (possibly retired): {}".format(not_found)) + print(" Catalog saved: {}".format(CATALOG_PATH)) + + +def validate_catalog(verbose=False): + """Compare current catalog prices against API and report differences.""" + print("Validating catalog against 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") + return + + diffs = [] + missing = [] + + categories = raw.get("categories", {}) + for cat_key, cat_data in categories.items(): + for entry in cat_data.get("skus", []): + sku = str(entry["sku"]) + if sku in api_map: + api_price = extract_payg_price(api_map[sku]) + cat_price = entry.get("list_price_usd", 0) + # Skip $0 API prices for tiered/free-tier SKUs + if api_price == 0 and cat_price > 0: + continue + if abs(api_price - cat_price) > 0.0001: + diffs.append({ + "sku": sku, + "product": entry.get("product", "")[:60], + "catalog_price": cat_price, + "api_price": api_price, + "diff_pct": ((api_price - cat_price) / cat_price * 100) if cat_price else 0, + }) + else: + missing.append(sku) + + if diffs: + print("\nPrice differences found ({}):\n".format(len(diffs))) + print("{:<10} {:<60} {:>12} {:>12} {:>8}".format( + "SKU", "Product", "Catalog", "API", "Diff %" + )) + print("-" * 102) + for d in diffs: + print("{:<10} {:<60} ${:>10.4f} ${:>10.4f} {:>7.1f}%".format( + d["sku"], d["product"], d["catalog_price"], d["api_price"], d["diff_pct"] + )) + print("\nRun with --refresh to update the catalog.") + else: + print("\nAll prices match the API. Catalog is up to date.") + + if missing: + print("\n{} SKUs not found in API (possibly retired):".format(len(missing))) + for s in missing[:10]: + print(" - {}".format(s)) + if len(missing) > 10: + print(" ... and {} more".format(len(missing) - 10)) + + +def inspect_sku(part_number): + """Fetch and display a single SKU from the API.""" + print("Fetching SKU {} from API...".format(part_number)) + items = fetch_single_sku(part_number) + if not items: + print(" SKU not found in API") + return + + for item in items: + print("\n Part Number: {}".format(item.get("partNumber", ""))) + print(" Display Name: {}".format(item.get("displayName", ""))) + print(" Category: {}".format(item.get("serviceCategory", "N/A"))) + print(" Metric: {}".format(item.get("metricName", "N/A"))) + print(" Prices:") + for loc in item.get("currencyCodeLocalizations", []): + cc = loc.get("currencyCode", "?") + for price in loc.get("prices", []): + print(" - {} {} : {}".format( + cc, price.get("model", "?"), price.get("value", "?") + )) + + +def dump_all(output_path, verbose=False): + """Dump raw API response to JSON file.""" + products = fetch_all_products(currency=DEFAULT_CURRENCY, verbose=verbose) + with open(output_path, "w") as f: + json.dump(products, f, indent=2) + print("Dumped {} products to {}".format(len(products), output_path)) + + +def main(): + parser = argparse.ArgumentParser( + description="OCI Deal Accelerator — SKU Catalog Refresh Tool" + ) + parser.add_argument("--refresh", action="store_true", + help="Refresh catalog prices from Oracle API") + parser.add_argument("--validate", action="store_true", + help="Validate catalog prices against API") + parser.add_argument("--sku", type=str, + help="Inspect a single SKU from the API") + parser.add_argument("--dump", type=str, metavar="FILE", + help="Dump raw API data to JSON file") + parser.add_argument("--diff", action="store_true", + help="Show detailed price changes during refresh") + parser.add_argument("-v", "--verbose", action="store_true", + help="Verbose output") + args = parser.parse_args() + + if args.sku: + inspect_sku(args.sku) + elif args.refresh: + refresh_catalog(verbose=args.verbose or args.diff) + elif args.validate: + validate_catalog(verbose=args.verbose) + elif args.dump: + dump_all(args.dump, verbose=args.verbose) + else: + parser.print_help() + + +if __name__ == "__main__": + main()