reworked price list view
This commit is contained in:
parent
d39ff91a74
commit
c3d20fda7b
3 changed files with 220 additions and 145 deletions
|
@ -483,7 +483,7 @@ class StoragePlan(models.Model):
|
|||
def get_price(self, currency_code: str):
|
||||
try:
|
||||
return self.prices.get(currency=currency_code).amount
|
||||
except ComputePlanPrice.DoesNotExist:
|
||||
except StoragePlanPrice.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -1,94 +1,81 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Compute Plan Price Comparison</h1>
|
||||
{% block title %}Complete Price List{% endblock %}
|
||||
|
||||
{% for plan_data in plans_data %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2>{{ plan_data.plan.name }}</h2>
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">Complete Price List - All Service Variants</h1>
|
||||
|
||||
{% if pricing_data_by_service_level %}
|
||||
{% for service_level, pricing_data in pricing_data_by_service_level.items %}
|
||||
<div class="mb-5">
|
||||
<h2 class="mb-3">{{ service_level }}</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Cloud Provider</th>
|
||||
<th>Service</th>
|
||||
<th>Compute Plan</th>
|
||||
<th>vCPUs</th>
|
||||
<th>RAM (GB)</th>
|
||||
<th>CPU/Memory Ratio</th>
|
||||
<th>Term</th>
|
||||
<th>Currency</th>
|
||||
<th>Compute Plan Price</th>
|
||||
<th>Variable Unit</th>
|
||||
<th>Units</th>
|
||||
<th>Replica Enforce</th>
|
||||
<th>SLA Base</th>
|
||||
<th>SLA Per Unit</th>
|
||||
<th>SLA Price</th>
|
||||
<th>Discount Model</th>
|
||||
<th class="table-warning">Final Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in pricing_data %}
|
||||
<tr>
|
||||
<td>{{ row.cloud_provider }}</td>
|
||||
<td>{{ row.service }}</td>
|
||||
<td>{{ row.compute_plan }}</td>
|
||||
<td>{{ row.vcpus }}</td>
|
||||
<td>{{ row.ram }}</td>
|
||||
<td>{{ row.cpu_mem_ratio }}</td>
|
||||
<td>{{ row.term }}</td>
|
||||
<td>{{ row.currency }}</td>
|
||||
<td>{{ row.compute_plan_price|floatformat:2 }}</td>
|
||||
<td>{{ row.variable_unit }}</td>
|
||||
<td>{{ row.units }}</td>
|
||||
<td>{{ row.replica_enforce }}</td>
|
||||
<td>{{ row.sla_base|floatformat:2 }}</td>
|
||||
<td>{{ row.sla_per_unit|floatformat:4 }}</td>
|
||||
<td>{{ row.sla_price|floatformat:2 }}</td>
|
||||
<td>
|
||||
{% if row.has_discount %}
|
||||
{{ row.discount_model }}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="table-warning fw-bold">{{ row.final_price|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-muted"><strong>{{ pricing_data|length }}</strong> variants for {{ service_level }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<h4>No pricing data available</h4>
|
||||
<p>Please ensure you have active compute plans with prices and VSHNAppCat price configurations.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>Plan Details</h3>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Cloud Provider:</th>
|
||||
<td>{{ plan_data.plan.cloud_provider.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>vCPUs:</th>
|
||||
<td>{{ plan_data.plan.vcpus }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>RAM:</th>
|
||||
<td>{{ plan_data.plan.ram }} GB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>CPU/Memory Ratio:</th>
|
||||
<td>{{ plan_data.plan.cpu_mem_ratio }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Term:</th>
|
||||
<td>{{ plan_data.plan.get_term_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Compute Plan Prices:</th>
|
||||
<td>
|
||||
{% for price in plan_data.plan.prices.all %}
|
||||
{{ price.amount }} {{ price.currency }}<br>
|
||||
{% empty %}
|
||||
No prices set
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Calculated AppCat Prices</h3>
|
||||
{% if plan_data.calculated_prices %}
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Variable Unit</th>
|
||||
<th>Service Level</th>
|
||||
<th>Units</th>
|
||||
<th>Plan Term</th>
|
||||
<th>Service Term</th>
|
||||
<th>Currency</th>
|
||||
<th>Final Price</th>
|
||||
<th>Discount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for price in plan_data.calculated_prices %}
|
||||
<tr>
|
||||
<td>{{ price.service }}</td>
|
||||
<td>{{ price.variable_unit }}</td>
|
||||
<td>{{ price.service_level }}</td>
|
||||
<td>{{ price.units }}</td>
|
||||
<td>{{ price.plan_term }}</td>
|
||||
<td>{{ price.service_term }}</td>
|
||||
<td>{{ price.currency }}</td>
|
||||
<td>{{ price.price }}</td>
|
||||
<td>
|
||||
{% if price.discount_model %}
|
||||
{{ price.discount_model.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No AppCat prices calculated for this plan.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="alert alert-info">
|
||||
No compute plans found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,86 +1,174 @@
|
|||
from django.shortcuts import render
|
||||
from hub.services.models import ComputePlan, VSHNAppCatPrice, VSHNAppCatUnitRate
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from hub.services.models import ComputePlan, VSHNAppCatPrice
|
||||
|
||||
|
||||
def natural_sort_key(name):
|
||||
"""Extract numeric part from compute plan name for natural sorting"""
|
||||
match = re.search(r"compute-std-(\d+)", name)
|
||||
return int(match.group(1)) if match else 0
|
||||
|
||||
|
||||
def pricelist(request):
|
||||
# Get all compute plans and app catalog prices
|
||||
# Get all active compute plans with their pricing data
|
||||
compute_plans = (
|
||||
ComputePlan.objects.all()
|
||||
ComputePlan.objects.filter(active=True)
|
||||
.select_related("cloud_provider")
|
||||
.prefetch_related("prices")
|
||||
.order_by("cloud_provider__name")
|
||||
)
|
||||
|
||||
# Sort compute plans naturally by provider and plan number
|
||||
compute_plans = sorted(
|
||||
compute_plans, key=lambda x: (x.cloud_provider.name, natural_sort_key(x.name))
|
||||
)
|
||||
|
||||
# Get all VSHNAppCat pricing configurations
|
||||
appcat_prices = (
|
||||
VSHNAppCatPrice.objects.all()
|
||||
.select_related("service", "discount_model")
|
||||
.prefetch_related("base_fees", "unit_rates")
|
||||
.prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
|
||||
.order_by("service__name")
|
||||
)
|
||||
|
||||
plans_data = []
|
||||
pricing_data_by_service_level = defaultdict(list)
|
||||
|
||||
# Track processed combinations to avoid duplicates
|
||||
processed_combinations = set()
|
||||
|
||||
for plan in compute_plans:
|
||||
plan_data = {"plan": plan, "calculated_prices": []}
|
||||
# Get all currencies available for this compute plan
|
||||
plan_currencies = set(plan.prices.values_list("currency", flat=True))
|
||||
|
||||
for price_config in appcat_prices:
|
||||
# Get all service levels for this price config
|
||||
service_levels = (
|
||||
VSHNAppCatUnitRate.objects.filter(vshn_appcat_price_config=price_config)
|
||||
.values_list("service_level", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# Determine number of units based on variable_unit
|
||||
if price_config.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
|
||||
for appcat_price in appcat_prices:
|
||||
# Determine units based on variable unit type (RAM or CPU)
|
||||
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
|
||||
units = int(plan.ram)
|
||||
elif price_config.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
|
||||
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
|
||||
units = int(plan.vcpus)
|
||||
else:
|
||||
continue # Skip other unit type as we don't know yet how to handle them
|
||||
continue
|
||||
|
||||
# Get all currencies used in base fees
|
||||
currencies = price_config.base_fees.values_list(
|
||||
"currency", flat=True
|
||||
# Get currencies available for base fees and unit rates
|
||||
base_fee_currencies = set(
|
||||
appcat_price.base_fees.values_list("currency", flat=True)
|
||||
)
|
||||
|
||||
# Get all distinct service levels for this pricing config
|
||||
service_levels = appcat_price.unit_rates.values_list(
|
||||
"service_level", flat=True
|
||||
).distinct()
|
||||
|
||||
# Calculate prices for all combinations
|
||||
for service_level in service_levels:
|
||||
for currency in currencies:
|
||||
final_price = price_config.calculate_final_price(
|
||||
currency_code=currency,
|
||||
service_level=service_level,
|
||||
number_of_units=units,
|
||||
# Get currencies available for this specific service level
|
||||
unit_rate_currencies = set(
|
||||
appcat_price.unit_rates.filter(
|
||||
service_level=service_level
|
||||
).values_list("currency", flat=True)
|
||||
)
|
||||
|
||||
# Find currencies that exist in ALL three places: plan, base fee, and unit rate
|
||||
matching_currencies = plan_currencies.intersection(
|
||||
base_fee_currencies
|
||||
).intersection(unit_rate_currencies)
|
||||
|
||||
# Skip if no common currencies found
|
||||
if not matching_currencies:
|
||||
continue
|
||||
|
||||
# Process each matching currency
|
||||
for currency in matching_currencies:
|
||||
# Create unique combination key to prevent duplicates
|
||||
combination_key = (
|
||||
plan.cloud_provider.name,
|
||||
plan.name,
|
||||
appcat_price.service.name,
|
||||
service_level,
|
||||
currency,
|
||||
)
|
||||
|
||||
if final_price is not None:
|
||||
service_level_display = dict(
|
||||
VSHNAppCatPrice.ServiceLevel.choices
|
||||
)[service_level]
|
||||
# Skip if this combination was already processed
|
||||
if combination_key in processed_combinations:
|
||||
continue
|
||||
|
||||
# Include discount model information
|
||||
discount_info = None
|
||||
if (
|
||||
price_config.discount_model
|
||||
and price_config.discount_model.active
|
||||
):
|
||||
discount_info = {
|
||||
"name": price_config.discount_model.name,
|
||||
"description": price_config.discount_model.description,
|
||||
}
|
||||
processed_combinations.add(combination_key)
|
||||
|
||||
plan_data["calculated_prices"].append(
|
||||
{
|
||||
"service": price_config.service.name,
|
||||
"variable_unit": price_config.get_variable_unit_display(),
|
||||
"service_level": service_level_display,
|
||||
"units": units,
|
||||
"currency": currency,
|
||||
"price": final_price,
|
||||
"plan_term": plan.get_term_display(),
|
||||
"service_term": price_config.get_term_display(),
|
||||
"discount_model": discount_info,
|
||||
}
|
||||
# Get pricing data for this currency - skip if any is missing
|
||||
compute_plan_price = plan.get_price(currency)
|
||||
if compute_plan_price is None:
|
||||
continue
|
||||
|
||||
base_fee = appcat_price.get_base_fee(currency)
|
||||
if base_fee is None:
|
||||
continue
|
||||
|
||||
unit_rate = appcat_price.get_unit_rate(currency, service_level)
|
||||
if unit_rate is None:
|
||||
continue
|
||||
|
||||
# Calculate replica enforcement based on service level
|
||||
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
|
||||
replica_enforce = appcat_price.ha_replica_min
|
||||
else:
|
||||
replica_enforce = 1
|
||||
|
||||
total_units = units * replica_enforce
|
||||
|
||||
# Apply discount model if available and active
|
||||
if (
|
||||
appcat_price.discount_model
|
||||
and appcat_price.discount_model.active
|
||||
):
|
||||
discounted_price = (
|
||||
appcat_price.discount_model.calculate_discount(
|
||||
unit_rate, total_units
|
||||
)
|
||||
)
|
||||
sla_price = base_fee + discounted_price
|
||||
else:
|
||||
# Standard pricing without discount
|
||||
sla_price = base_fee + (total_units * unit_rate)
|
||||
|
||||
plans_data.append(plan_data)
|
||||
# Calculate final price (compute + SLA)
|
||||
final_price = compute_plan_price + sla_price
|
||||
|
||||
context = {"plans_data": plans_data}
|
||||
# Get human-readable service level name
|
||||
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
|
||||
service_level
|
||||
]
|
||||
|
||||
# Add row to the appropriate service level group
|
||||
pricing_data_by_service_level[service_level_display].append(
|
||||
{
|
||||
"cloud_provider": plan.cloud_provider.name,
|
||||
"service": appcat_price.service.name,
|
||||
"compute_plan": plan.name,
|
||||
"vcpus": plan.vcpus,
|
||||
"ram": plan.ram,
|
||||
"cpu_mem_ratio": plan.cpu_mem_ratio,
|
||||
"term": plan.get_term_display(),
|
||||
"currency": currency,
|
||||
"compute_plan_price": compute_plan_price,
|
||||
"variable_unit": appcat_price.get_variable_unit_display(),
|
||||
"units": units,
|
||||
"replica_enforce": replica_enforce,
|
||||
"service_level": service_level_display,
|
||||
"sla_base": base_fee,
|
||||
"sla_per_unit": unit_rate,
|
||||
"sla_price": sla_price,
|
||||
"final_price": final_price,
|
||||
"discount_model": (
|
||||
appcat_price.discount_model.name
|
||||
if appcat_price.discount_model
|
||||
else None
|
||||
),
|
||||
"has_discount": bool(
|
||||
appcat_price.discount_model
|
||||
and appcat_price.discount_model.active
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
context = {"pricing_data_by_service_level": dict(pricing_data_by_service_level)}
|
||||
return render(request, "services/pricelist.html", context)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue