Compare commits

..

No commits in common. "96b667dd756aa1792885e1aacdb6544994ad6f66" and "fbca67ef66b394c388190f469ee4492fda4bf96a" have entirely different histories.

2 changed files with 281 additions and 135 deletions

View file

@ -477,13 +477,8 @@
</thead> </thead>
<tbody> <tbody>
{% for row in pricing_data %} {% for row in pricing_data %}
<tr class="servala-row {% if not row.is_active %}text-muted opacity-50{% endif %} {% if show_price_comparison and row.external_comparisons or row.internal_comparisons %}has-comparisons{% endif %}"> <tr class="servala-row {% if show_price_comparison and row.external_comparisons or row.internal_comparisons %}has-comparisons{% endif %}">
<td> <td>{{ row.compute_plan }}</td>
{{ row.compute_plan }}
{% if not row.is_active %}
<span class="badge bg-secondary ms-1" title="This compute plan is not active and not available for new public offerings.">Inactive plan</span>
{% endif %}
</td>
<td>{{ row.cloud_provider }}</td> <td>{{ row.cloud_provider }}</td>
<td>{{ row.vcpus }}</td> <td>{{ row.vcpus }}</td>
<td>{{ row.ram }}</td> <td>{{ row.ram }}</td>

View file

@ -2,16 +2,20 @@ import re
from django.shortcuts import render from django.shortcuts import render
from collections import defaultdict from collections import defaultdict
from hub.services.models.pricing import ComputePlan, StoragePlan, ExternalPricePlans, VSHNAppCatPrice from hub.services.models import (
ComputePlan,
VSHNAppCatPrice,
ExternalPricePlans,
StoragePlan,
)
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.db import models from django.db import models
def natural_sort_key(obj): def natural_sort_key(name):
"""Extract numeric parts for natural sorting (works for any plan name)""" """Extract numeric part from compute plan name for natural sorting"""
name = obj.name if hasattr(obj, 'name') else str(obj) match = re.search(r"compute-std-(\d+)", name)
parts = re.split(r"(\d+)", name) return int(match.group(1)) if match else 0
return [int(part) if part.isdigit() else part for part in parts]
def get_external_price_comparisons(plan, appcat_price, currency, service_level): def get_external_price_comparisons(plan, appcat_price, currency, service_level):
@ -120,7 +124,7 @@ def get_internal_cloud_provider_comparisons(
@staff_member_required @staff_member_required
def pricelist(request): def pricelist(request):
"""Generate comprehensive price list grouped by compute plan groups and service levels (optimized)""" """Generate comprehensive price list grouped by compute plan groups and service levels"""
# Get filter parameters from request # Get filter parameters from request
show_discount_details = request.GET.get("discount_details", "").lower() == "true" show_discount_details = request.GET.get("discount_details", "").lower() == "true"
show_addon_details = request.GET.get("addon_details", "").lower() == "true" show_addon_details = request.GET.get("addon_details", "").lower() == "true"
@ -130,47 +134,45 @@ def pricelist(request):
filter_compute_plan_group = request.GET.get("compute_plan_group", "") filter_compute_plan_group = request.GET.get("compute_plan_group", "")
filter_service_level = request.GET.get("service_level", "") filter_service_level = request.GET.get("service_level", "")
# Fetch all compute plans (active and inactive) with related data # Fetch all active compute plans with related data
compute_plans_qs = ComputePlan.objects.all() compute_plans = (
if filter_cloud_provider: ComputePlan.objects.filter(active=True)
compute_plans_qs = compute_plans_qs.filter(cloud_provider__name=filter_cloud_provider)
if filter_compute_plan_group:
if filter_compute_plan_group == "No Group":
compute_plans_qs = compute_plans_qs.filter(group__isnull=True)
else:
compute_plans_qs = compute_plans_qs.filter(group__name=filter_compute_plan_group)
compute_plans = list(
compute_plans_qs
.select_related("cloud_provider", "group") .select_related("cloud_provider", "group")
.prefetch_related("prices") .prefetch_related("prices")
.order_by("group__order", "group__name", "cloud_provider__name", "name") .order_by("group__order", "group__name", "cloud_provider__name")
) )
# Restore natural sorting of compute plan names
# Apply compute plan filters
if filter_cloud_provider:
compute_plans = compute_plans.filter(cloud_provider__name=filter_cloud_provider)
if filter_compute_plan_group:
if filter_compute_plan_group == "No Group":
compute_plans = compute_plans.filter(group__isnull=True)
else:
compute_plans = compute_plans.filter(group__name=filter_compute_plan_group)
# Apply natural sorting for compute plan names
compute_plans = sorted( compute_plans = sorted(
compute_plans, compute_plans,
key=lambda p: ( key=lambda x: (
p.group.order if p.group else 999, x.group.order if x.group else 999, # No group plans at the end
p.group.name if p.group else "ZZZ", x.group.name if x.group else "ZZZ",
natural_sort_key(p), x.cloud_provider.name,
natural_sort_key(x.name),
), ),
) )
# Fetch all appcat price configurations (prefetch addons) # Fetch all appcat price configurations
appcat_prices_qs = ( appcat_prices = (
VSHNAppCatPrice.objects.all() VSHNAppCatPrice.objects.all()
.select_related("service", "discount_model") .select_related("service", "discount_model")
.prefetch_related("base_fees", "unit_rates", "discount_model__tiers", "addons") .prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
.order_by("service__name") .order_by("service__name")
) )
if filter_service:
appcat_prices_qs = appcat_prices_qs.filter(service__name=filter_service)
appcat_prices = list(appcat_prices_qs)
# Prefetch all storage plans for all cloud providers and build a lookup # Apply service filter
all_storage_plans = StoragePlan.objects.all().prefetch_related("prices") if filter_service:
storage_plans_by_provider = defaultdict(list) appcat_prices = appcat_prices.filter(service__name=filter_service)
for sp in all_storage_plans:
storage_plans_by_provider[sp.cloud_provider_id].append(sp)
pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list)) pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list))
processed_combinations = set() processed_combinations = set()
@ -178,6 +180,7 @@ def pricelist(request):
# Generate pricing combinations for each compute plan and service # Generate pricing combinations for each compute plan and service
for plan in compute_plans: for plan in compute_plans:
plan_currencies = set(plan.prices.values_list("currency", flat=True)) plan_currencies = set(plan.prices.values_list("currency", flat=True))
for appcat_price in appcat_prices: for appcat_price in appcat_prices:
# Determine units based on variable unit type # Determine units based on variable unit type
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM: if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
@ -186,22 +189,39 @@ def pricelist(request):
units = int(plan.vcpus) units = int(plan.vcpus)
else: else:
continue continue
base_fee_currencies = set(appcat_price.base_fees.values_list("currency", flat=True))
service_levels = appcat_price.unit_rates.values_list("service_level", flat=True).distinct() base_fee_currencies = set(
appcat_price.base_fees.values_list("currency", flat=True)
)
service_levels = appcat_price.unit_rates.values_list(
"service_level", flat=True
).distinct()
# Apply service level filter # Apply service level filter
if filter_service_level: if filter_service_level:
service_levels = [ service_levels = [
sl for sl in service_levels sl
if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl] == filter_service_level for sl in service_levels
if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl]
== filter_service_level
] ]
for service_level in service_levels: for service_level in service_levels:
unit_rate_currencies = set( unit_rate_currencies = set(
appcat_price.unit_rates.filter(service_level=service_level).values_list("currency", flat=True) appcat_price.unit_rates.filter(
service_level=service_level
).values_list("currency", flat=True)
) )
# Find currencies that exist across all pricing components # Find currencies that exist across all pricing components
matching_currencies = plan_currencies.intersection(base_fee_currencies).intersection(unit_rate_currencies) matching_currencies = plan_currencies.intersection(
base_fee_currencies
).intersection(unit_rate_currencies)
if not matching_currencies: if not matching_currencies:
continue continue
for currency in matching_currencies: for currency in matching_currencies:
combination_key = ( combination_key = (
plan.cloud_provider.name, plan.cloud_provider.name,
@ -210,35 +230,63 @@ def pricelist(request):
service_level, service_level,
currency, currency,
) )
# Skip if combination already processed
if combination_key in processed_combinations: if combination_key in processed_combinations:
continue continue
processed_combinations.add(combination_key) processed_combinations.add(combination_key)
# Get pricing components # Get pricing components
compute_plan_price = plan.get_price(currency) compute_plan_price = plan.get_price(currency)
base_fee = appcat_price.get_base_fee(currency, service_level) base_fee = appcat_price.get_base_fee(currency, service_level)
unit_rate = appcat_price.get_unit_rate(currency, service_level) unit_rate = appcat_price.get_unit_rate(currency, service_level)
if any(price is None for price in [compute_plan_price, base_fee, unit_rate]):
# Skip if any pricing component is missing
if any(
price is None
for price in [compute_plan_price, base_fee, unit_rate]
):
continue continue
# Calculate replica enforcement based on service level # Calculate replica enforcement based on service level
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED: if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
replica_enforce = appcat_price.ha_replica_min replica_enforce = appcat_price.ha_replica_min
else: else:
replica_enforce = 1 replica_enforce = 1
total_units = units * replica_enforce total_units = units * replica_enforce
standard_sla_price = base_fee + (total_units * unit_rate) standard_sla_price = base_fee + (total_units * unit_rate)
# Apply discount if available # Apply discount if available
discount_breakdown = None discount_breakdown = None
if appcat_price.discount_model and appcat_price.discount_model.active: if (
discounted_price = appcat_price.discount_model.calculate_discount(unit_rate, total_units) 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 sla_price = base_fee + discounted_price
discount_savings = standard_sla_price - sla_price discount_savings = standard_sla_price - sla_price
discount_percentage = (discount_savings / standard_sla_price) * 100 if standard_sla_price > 0 else 0 discount_percentage = (
discount_breakdown = appcat_price.discount_model.get_discount_breakdown(unit_rate, total_units) (discount_savings / standard_sla_price) * 100
if standard_sla_price > 0
else 0
)
discount_breakdown = (
appcat_price.discount_model.get_discount_breakdown(
unit_rate, total_units
)
)
else: else:
sla_price = standard_sla_price sla_price = standard_sla_price
discounted_price = total_units * unit_rate discounted_price = total_units * unit_rate
discount_savings = 0 discount_savings = 0
discount_percentage = 0 discount_percentage = 0
# Calculate final price using the model method to ensure consistency # Calculate final price using the model method to ensure consistency
price_calculation = appcat_price.calculate_final_price( price_calculation = appcat_price.calculate_final_price(
currency_code=currency, currency_code=currency,
@ -246,22 +294,60 @@ def pricelist(request):
number_of_units=total_units, number_of_units=total_units,
addon_ids=None, # This will include only mandatory addons addon_ids=None, # This will include only mandatory addons
) )
if price_calculation is None: if price_calculation is None:
continue continue
# Calculate base service price (without addons) for display purposes # Calculate base service price (without addons) for display purposes
base_sla_price = base_fee + (total_units * unit_rate) base_sla_price = base_fee + (total_units * unit_rate)
# Extract addon information from the calculation (use prefetched addons)
# Apply discount if available
discount_breakdown = None
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
discount_savings = base_sla_price - sla_price
discount_percentage = (
(discount_savings / base_sla_price) * 100
if base_sla_price > 0
else 0
)
discount_breakdown = (
appcat_price.discount_model.get_discount_breakdown(
unit_rate, total_units
)
)
else:
sla_price = base_sla_price
discounted_price = total_units * unit_rate
discount_savings = 0
discount_percentage = 0
# Extract addon information from the calculation
mandatory_addons = [] mandatory_addons = []
optional_addons = [] optional_addons = []
all_addons = [a for a in appcat_price.addons.all() if a.active]
# Get all addons to separate mandatory from optional
all_addons = appcat_price.addons.filter(active=True)
for addon in all_addons: for addon in all_addons:
addon_price = None addon_price = None
if addon.addon_type == "BF": # Base Fee if addon.addon_type == "BF": # Base Fee
addon_price = addon.get_price(currency, service_level) addon_price = addon.get_price(currency, service_level)
elif addon.addon_type == "UR": # Unit Rate elif addon.addon_type == "UR": # Unit Rate
addon_price_per_unit = addon.get_price(currency, service_level) addon_price_per_unit = addon.get_price(
currency, service_level
)
if addon_price_per_unit: if addon_price_per_unit:
addon_price = addon_price_per_unit * total_units addon_price = addon_price_per_unit * total_units
addon_info = { addon_info = {
"id": addon.id, "id": addon.id,
"name": addon.name, "name": addon.name,
@ -270,103 +356,165 @@ def pricelist(request):
"addon_type": addon.get_addon_type_display(), "addon_type": addon.get_addon_type_display(),
"price": addon_price, "price": addon_price,
} }
if addon.mandatory: if addon.mandatory:
mandatory_addons.append(addon_info) mandatory_addons.append(addon_info)
else: else:
optional_addons.append(addon_info) optional_addons.append(addon_info)
# Use the calculated total price which includes mandatory addons
service_price_with_addons = price_calculation["total_price"] service_price_with_addons = price_calculation["total_price"]
final_price = compute_plan_price + service_price_with_addons final_price = compute_plan_price + service_price_with_addons
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices).get(service_level, service_level) service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
# Get external/internal price comparisons if enabled (unchanged, but could be optimized further) service_level
]
# Get external price comparisons if enabled
external_comparisons = [] external_comparisons = []
internal_comparisons = [] internal_comparisons = []
if show_price_comparison: if show_price_comparison:
external_prices = get_external_price_comparisons(plan, appcat_price, currency, service_level) # Get external price comparisons
external_prices = get_external_price_comparisons(
plan, appcat_price, currency, service_level
)
for ext_price in external_prices: for ext_price in external_prices:
# Calculate price difference using external price currency
difference = ext_price.amount - final_price difference = ext_price.amount - final_price
ratio = ext_price.amount / final_price if final_price > 0 else 0 ratio = (
external_comparisons.append({ ext_price.amount / final_price if final_price > 0 else 0
"plan_name": ext_price.plan_name, )
"provider": ext_price.cloud_provider.name,
"description": ext_price.description, external_comparisons.append(
"amount": ext_price.amount, {
"currency": ext_price.currency, "plan_name": ext_price.plan_name,
"vcpus": ext_price.vcpus, "provider": ext_price.cloud_provider.name,
"ram": ext_price.ram, "description": ext_price.description,
"storage": ext_price.storage, "amount": ext_price.amount,
"replicas": ext_price.replicas, "currency": ext_price.currency, # Use external price currency
"difference": difference, "vcpus": ext_price.vcpus,
"ratio": ratio, "ram": ext_price.ram,
"source": ext_price.source, "storage": ext_price.storage,
"date_retrieved": ext_price.date_retrieved, "replicas": ext_price.replicas,
"is_internal": False, "difference": difference,
}) "ratio": ratio,
internal_price_comparisons = get_internal_cloud_provider_comparisons(plan, appcat_price, currency, service_level) "source": ext_price.source,
"date_retrieved": ext_price.date_retrieved,
"is_internal": False,
}
)
# Get internal cloud provider comparisons
internal_price_comparisons = (
get_internal_cloud_provider_comparisons(
plan, appcat_price, currency, service_level
)
)
for int_price in internal_price_comparisons: for int_price in internal_price_comparisons:
# Calculate price difference
difference = int_price["final_price"] - final_price difference = int_price["final_price"] - final_price
ratio = int_price["final_price"] / final_price if final_price > 0 else 0 ratio = (
internal_comparisons.append({ int_price["final_price"] / final_price
"plan_name": int_price["plan_name"], if final_price > 0
"provider": int_price["provider"], else 0
"description": f"Same specs with {int_price['provider']}", )
"amount": int_price["final_price"],
"currency": int_price["currency"], internal_comparisons.append(
"vcpus": int_price["vcpus"], {
"ram": int_price["ram"], "plan_name": int_price["plan_name"],
"group_name": int_price["group_name"], "provider": int_price["provider"],
"compute_plan_price": int_price["compute_plan_price"], "description": f"Same specs with {int_price['provider']}",
"service_price": int_price["service_price"], "amount": int_price["final_price"],
"difference": difference, "currency": int_price["currency"],
"ratio": ratio, "vcpus": int_price["vcpus"],
"is_internal": True, "ram": int_price["ram"],
}) "group_name": int_price["group_name"],
"compute_plan_price": int_price[
"compute_plan_price"
],
"service_price": int_price["service_price"],
"difference": difference,
"ratio": ratio,
"is_internal": True,
}
)
group_name = plan.group.name if plan.group else "No Group" group_name = plan.group.name if plan.group else "No Group"
# Use prefetched storage plans
storage_plans = storage_plans_by_provider.get(plan.cloud_provider_id, []) # Get storage plans for this cloud provider
pricing_data_by_group_and_service_level[group_name][service_level_display].append({ storage_plans = StoragePlan.objects.filter(
"cloud_provider": plan.cloud_provider.name, cloud_provider=plan.cloud_provider
"service": appcat_price.service.name, ).prefetch_related("prices")
"compute_plan": plan.name,
"compute_plan_group": group_name, # Add pricing data to the grouped structure
"compute_plan_group_description": (plan.group.description if plan.group else ""), pricing_data_by_group_and_service_level[group_name][
"compute_plan_group_node_label": (plan.group.node_label if plan.group else ""), service_level_display
"storage_plans": storage_plans, ].append(
"vcpus": plan.vcpus, {
"ram": plan.ram, "cloud_provider": plan.cloud_provider.name,
"cpu_mem_ratio": plan.cpu_mem_ratio, "service": appcat_price.service.name,
"term": plan.get_term_display(), "compute_plan": plan.name,
"currency": currency, "compute_plan_group": group_name,
"compute_plan_price": compute_plan_price, "compute_plan_group_description": (
"variable_unit": appcat_price.get_variable_unit_display(), plan.group.description if plan.group else ""
"units": units, ),
"replica_enforce": replica_enforce, "compute_plan_group_node_label": (
"total_units": total_units, plan.group.node_label if plan.group else ""
"service_level": service_level_display, ),
"sla_base": base_fee, "storage_plans": storage_plans,
"sla_per_unit": unit_rate, "vcpus": plan.vcpus,
"sla_price": service_price_with_addons, "ram": plan.ram,
"standard_sla_price": base_sla_price, "cpu_mem_ratio": plan.cpu_mem_ratio,
"discounted_sla_price": (base_fee + discounted_price if appcat_price.discount_model and appcat_price.discount_model.active else None), "term": plan.get_term_display(),
"discount_savings": discount_savings, "currency": currency,
"discount_percentage": discount_percentage, "compute_plan_price": compute_plan_price,
"discount_breakdown": discount_breakdown, "variable_unit": appcat_price.get_variable_unit_display(),
"final_price": final_price, "units": units,
"discount_model": (appcat_price.discount_model.name if appcat_price.discount_model else None), "replica_enforce": replica_enforce,
"has_discount": bool(appcat_price.discount_model and appcat_price.discount_model.active), "total_units": total_units,
"external_comparisons": external_comparisons, "service_level": service_level_display,
"internal_comparisons": internal_comparisons, "sla_base": base_fee,
"mandatory_addons": mandatory_addons, "sla_per_unit": unit_rate,
"optional_addons": optional_addons, "sla_price": service_price_with_addons,
"is_active": plan.active, "standard_sla_price": base_sla_price,
}) "discounted_sla_price": (
base_fee + discounted_price
if appcat_price.discount_model
and appcat_price.discount_model.active
else None
),
"discount_savings": discount_savings,
"discount_percentage": discount_percentage,
"discount_breakdown": discount_breakdown,
"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
),
"external_comparisons": external_comparisons,
"internal_comparisons": internal_comparisons,
"mandatory_addons": mandatory_addons,
"optional_addons": optional_addons,
}
)
# Order groups correctly, placing "No Group" last # Order groups correctly, placing "No Group" last
ordered_groups_intermediate = {} ordered_groups_intermediate = {}
all_group_names = list(pricing_data_by_group_and_service_level.keys()) all_group_names = list(pricing_data_by_group_and_service_level.keys())
if "No Group" in all_group_names: if "No Group" in all_group_names:
all_group_names.remove("No Group") all_group_names.remove("No Group")
all_group_names.append("No Group") all_group_names.append("No Group")
for group_name_key in all_group_names: for group_name_key in all_group_names:
ordered_groups_intermediate[group_name_key] = pricing_data_by_group_and_service_level[group_name_key] ordered_groups_intermediate[group_name_key] = (
pricing_data_by_group_and_service_level[group_name_key]
)
# Convert defaultdicts to regular dicts for the template # Convert defaultdicts to regular dicts for the template
final_context_data = {} final_context_data = {}
for group_key, service_levels_dict in ordered_groups_intermediate.items(): for group_key, service_levels_dict in ordered_groups_intermediate.items():
@ -374,9 +522,10 @@ def pricelist(request):
sl_key: list(plans_list) sl_key: list(plans_list)
for sl_key, plans_list in service_levels_dict.items() for sl_key, plans_list in service_levels_dict.items()
} }
# Get filter options for dropdowns (include all providers/groups from all plans, not just active)
# Get filter options for dropdowns
all_cloud_providers = ( all_cloud_providers = (
ComputePlan.objects.all() ComputePlan.objects.filter(active=True)
.values_list("cloud_provider__name", flat=True) .values_list("cloud_provider__name", flat=True)
.distinct() .distinct()
.order_by("cloud_provider__name") .order_by("cloud_provider__name")
@ -387,18 +536,20 @@ def pricelist(request):
.order_by("service__name") .order_by("service__name")
) )
all_compute_plan_groups = list( all_compute_plan_groups = list(
ComputePlan.objects.filter(group__isnull=False) ComputePlan.objects.filter(active=True, group__isnull=False)
.values_list("group__name", flat=True) .values_list("group__name", flat=True)
.distinct() .distinct()
.order_by("group__name") .order_by("group__name")
) )
all_compute_plan_groups.append("No Group") # Add option for plans without groups all_compute_plan_groups.append("No Group") # Add option for plans without groups
all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices] all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices]
# If no filter is specified, select the first available provider/service by default # If no filter is specified, select the first available provider/service by default
if not filter_cloud_provider and all_cloud_providers: if not filter_cloud_provider and all_cloud_providers:
filter_cloud_provider = all_cloud_providers[0] filter_cloud_provider = all_cloud_providers[0]
if not filter_service and all_services: if not filter_service and all_services:
filter_service = all_services[0] filter_service = all_services[0]
context = { context = {
"pricing_data_by_group_and_service_level": final_context_data, "pricing_data_by_group_and_service_level": final_context_data,
"show_discount_details": show_discount_details, "show_discount_details": show_discount_details,