2025-05-23 15:56:11 +02:00
|
|
|
import re
|
2025-05-27 17:07:55 +02:00
|
|
|
|
|
|
|
from django.shortcuts import render
|
2025-05-23 15:56:11 +02:00
|
|
|
from collections import defaultdict
|
2025-05-30 13:43:02 +02:00
|
|
|
from hub.services.models import (
|
|
|
|
ComputePlan,
|
|
|
|
VSHNAppCatPrice,
|
|
|
|
ExternalPricePlans,
|
|
|
|
StoragePlan,
|
|
|
|
)
|
2025-05-26 15:03:39 +02:00
|
|
|
from django.contrib.admin.views.decorators import staff_member_required
|
2025-05-27 17:07:55 +02:00
|
|
|
from django.db import models
|
2025-05-23 15:56:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
def natural_sort_key(name):
|
2025-05-23 17:09:02 +02:00
|
|
|
"""Extract numeric part from compute plan name for natural sorting"""
|
2025-05-23 15:56:11 +02:00
|
|
|
match = re.search(r"compute-std-(\d+)", name)
|
|
|
|
return int(match.group(1)) if match else 0
|
2025-05-22 16:34:15 +02:00
|
|
|
|
|
|
|
|
2025-05-27 17:07:55 +02:00
|
|
|
def get_external_price_comparisons(plan, appcat_price, currency, service_level):
|
|
|
|
"""Get external price comparisons for a specific compute plan and service"""
|
|
|
|
try:
|
2025-05-30 13:36:09 +02:00
|
|
|
# Filter by service level if external price has one set, ignore currency for comparison
|
2025-05-27 17:07:55 +02:00
|
|
|
external_prices = ExternalPricePlans.objects.filter(
|
2025-05-30 13:36:09 +02:00
|
|
|
compare_to=plan, service=appcat_price.service
|
2025-05-27 17:07:55 +02:00
|
|
|
).select_related("cloud_provider")
|
|
|
|
|
|
|
|
# Filter by service level if the external price has it configured
|
|
|
|
if service_level:
|
|
|
|
external_prices = external_prices.filter(
|
|
|
|
models.Q(service_level=service_level)
|
|
|
|
| models.Q(service_level__isnull=True)
|
|
|
|
)
|
|
|
|
|
|
|
|
return external_prices
|
|
|
|
except Exception:
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
2025-05-26 15:03:39 +02:00
|
|
|
@staff_member_required
|
2025-05-23 08:49:08 +02:00
|
|
|
def pricelist(request):
|
2025-05-23 17:09:02 +02:00
|
|
|
"""Generate comprehensive price list grouped by compute plan groups and service levels"""
|
2025-05-26 15:36:20 +02:00
|
|
|
# Get filter parameters from request
|
2025-05-23 17:12:05 +02:00
|
|
|
show_discount_details = request.GET.get("discount_details", "").lower() == "true"
|
2025-05-27 17:07:55 +02:00
|
|
|
show_price_comparison = request.GET.get("price_comparison", "").lower() == "true"
|
2025-05-26 15:36:20 +02:00
|
|
|
filter_cloud_provider = request.GET.get("cloud_provider", "")
|
|
|
|
filter_service = request.GET.get("service", "")
|
|
|
|
filter_compute_plan_group = request.GET.get("compute_plan_group", "")
|
|
|
|
filter_service_level = request.GET.get("service_level", "")
|
2025-05-23 17:12:05 +02:00
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Fetch all active compute plans with related data
|
2025-05-22 16:34:15 +02:00
|
|
|
compute_plans = (
|
2025-05-23 15:56:11 +02:00
|
|
|
ComputePlan.objects.filter(active=True)
|
2025-05-23 17:09:02 +02:00
|
|
|
.select_related("cloud_provider", "group")
|
2025-05-22 16:34:15 +02:00
|
|
|
.prefetch_related("prices")
|
2025-05-23 17:09:02 +02:00
|
|
|
.order_by("group__order", "group__name", "cloud_provider__name")
|
2025-05-23 15:56:11 +02:00
|
|
|
)
|
|
|
|
|
2025-05-26 15:36:20 +02:00
|
|
|
# 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)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Apply natural sorting for compute plan names
|
2025-05-23 15:56:11 +02:00
|
|
|
compute_plans = sorted(
|
2025-05-23 17:09:02 +02:00
|
|
|
compute_plans,
|
|
|
|
key=lambda x: (
|
|
|
|
x.group.order if x.group else 999, # No group plans at the end
|
|
|
|
x.group.name if x.group else "ZZZ",
|
|
|
|
x.cloud_provider.name,
|
|
|
|
natural_sort_key(x.name),
|
|
|
|
),
|
2025-05-22 16:34:15 +02:00
|
|
|
)
|
2025-05-23 15:56:11 +02:00
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Fetch all appcat price configurations
|
2025-05-22 16:34:15 +02:00
|
|
|
appcat_prices = (
|
|
|
|
VSHNAppCatPrice.objects.all()
|
2025-05-23 08:49:08 +02:00
|
|
|
.select_related("service", "discount_model")
|
2025-05-23 15:56:11 +02:00
|
|
|
.prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
|
|
|
|
.order_by("service__name")
|
2025-05-22 16:34:15 +02:00
|
|
|
)
|
|
|
|
|
2025-05-26 15:36:20 +02:00
|
|
|
# Apply service filter
|
|
|
|
if filter_service:
|
|
|
|
appcat_prices = appcat_prices.filter(service__name=filter_service)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list))
|
2025-05-23 15:56:11 +02:00
|
|
|
processed_combinations = set()
|
2025-05-22 16:34:15 +02:00
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Generate pricing combinations for each compute plan and service
|
2025-05-22 16:34:15 +02:00
|
|
|
for plan in compute_plans:
|
2025-05-23 15:56:11 +02:00
|
|
|
plan_currencies = set(plan.prices.values_list("currency", flat=True))
|
2025-05-22 16:34:15 +02:00
|
|
|
|
2025-05-23 15:56:11 +02:00
|
|
|
for appcat_price in appcat_prices:
|
2025-05-23 17:09:02 +02:00
|
|
|
# Determine units based on variable unit type
|
2025-05-23 15:56:11 +02:00
|
|
|
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
|
2025-05-22 16:34:15 +02:00
|
|
|
units = int(plan.ram)
|
2025-05-23 15:56:11 +02:00
|
|
|
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
|
2025-05-22 16:34:15 +02:00
|
|
|
units = int(plan.vcpus)
|
|
|
|
else:
|
2025-05-23 15:56:11 +02:00
|
|
|
continue
|
|
|
|
|
|
|
|
base_fee_currencies = set(
|
|
|
|
appcat_price.base_fees.values_list("currency", flat=True)
|
|
|
|
)
|
2025-05-22 16:34:15 +02:00
|
|
|
|
2025-05-23 15:56:11 +02:00
|
|
|
service_levels = appcat_price.unit_rates.values_list(
|
|
|
|
"service_level", flat=True
|
2025-05-22 16:34:15 +02:00
|
|
|
).distinct()
|
|
|
|
|
2025-05-26 15:36:20 +02:00
|
|
|
# Apply service level filter
|
|
|
|
if filter_service_level:
|
|
|
|
service_levels = [
|
|
|
|
sl
|
|
|
|
for sl in service_levels
|
|
|
|
if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl]
|
|
|
|
== filter_service_level
|
|
|
|
]
|
|
|
|
|
2025-05-22 16:34:15 +02:00
|
|
|
for service_level in service_levels:
|
2025-05-23 15:56:11 +02:00
|
|
|
unit_rate_currencies = set(
|
|
|
|
appcat_price.unit_rates.filter(
|
|
|
|
service_level=service_level
|
|
|
|
).values_list("currency", flat=True)
|
|
|
|
)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Find currencies that exist across all pricing components
|
2025-05-23 15:56:11 +02:00
|
|
|
matching_currencies = plan_currencies.intersection(
|
|
|
|
base_fee_currencies
|
|
|
|
).intersection(unit_rate_currencies)
|
|
|
|
|
|
|
|
if not matching_currencies:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for currency in matching_currencies:
|
|
|
|
combination_key = (
|
|
|
|
plan.cloud_provider.name,
|
|
|
|
plan.name,
|
|
|
|
appcat_price.service.name,
|
|
|
|
service_level,
|
|
|
|
currency,
|
2025-05-22 16:34:15 +02:00
|
|
|
)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Skip if combination already processed
|
2025-05-23 15:56:11 +02:00
|
|
|
if combination_key in processed_combinations:
|
|
|
|
continue
|
|
|
|
|
|
|
|
processed_combinations.add(combination_key)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Get pricing components
|
2025-05-23 15:56:11 +02:00
|
|
|
compute_plan_price = plan.get_price(currency)
|
|
|
|
base_fee = appcat_price.get_base_fee(currency)
|
|
|
|
unit_rate = appcat_price.get_unit_rate(currency, service_level)
|
2025-05-23 17:09:02 +02:00
|
|
|
|
|
|
|
# Skip if any pricing component is missing
|
|
|
|
if any(
|
|
|
|
price is None
|
|
|
|
for price in [compute_plan_price, base_fee, unit_rate]
|
|
|
|
):
|
2025-05-23 15:56:11 +02:00
|
|
|
continue
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Calculate replica enforcement based on service level
|
2025-05-23 15:56:11 +02:00
|
|
|
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
|
|
|
|
replica_enforce = appcat_price.ha_replica_min
|
|
|
|
else:
|
|
|
|
replica_enforce = 1
|
|
|
|
|
|
|
|
total_units = units * replica_enforce
|
2025-05-23 16:37:03 +02:00
|
|
|
standard_sla_price = base_fee + (total_units * unit_rate)
|
2025-05-23 15:56:11 +02:00
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Apply discount if available
|
2025-05-23 16:37:03 +02:00
|
|
|
discount_breakdown = None
|
2025-05-23 15:56:11 +02:00
|
|
|
if (
|
|
|
|
appcat_price.discount_model
|
|
|
|
and appcat_price.discount_model.active
|
|
|
|
):
|
|
|
|
discounted_price = (
|
|
|
|
appcat_price.discount_model.calculate_discount(
|
|
|
|
unit_rate, total_units
|
|
|
|
)
|
2025-05-22 16:34:15 +02:00
|
|
|
)
|
2025-05-23 15:56:11 +02:00
|
|
|
sla_price = base_fee + discounted_price
|
2025-05-23 16:37:03 +02:00
|
|
|
discount_savings = standard_sla_price - sla_price
|
|
|
|
discount_percentage = (
|
|
|
|
(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
|
|
|
|
)
|
|
|
|
)
|
2025-05-23 15:56:11 +02:00
|
|
|
else:
|
2025-05-23 16:37:03 +02:00
|
|
|
sla_price = standard_sla_price
|
|
|
|
discounted_price = total_units * unit_rate
|
|
|
|
discount_savings = 0
|
|
|
|
discount_percentage = 0
|
2025-05-23 15:56:11 +02:00
|
|
|
|
|
|
|
final_price = compute_plan_price + sla_price
|
|
|
|
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
|
|
|
|
service_level
|
|
|
|
]
|
|
|
|
|
2025-05-27 17:07:55 +02:00
|
|
|
# Get external price comparisons if enabled
|
|
|
|
external_comparisons = []
|
|
|
|
if show_price_comparison:
|
|
|
|
external_prices = get_external_price_comparisons(
|
|
|
|
plan, appcat_price, currency, service_level
|
|
|
|
)
|
|
|
|
for ext_price in external_prices:
|
2025-05-30 13:36:09 +02:00
|
|
|
# Calculate price difference using external price currency
|
|
|
|
difference = ext_price.amount - final_price
|
|
|
|
ratio = (
|
|
|
|
ext_price.amount / final_price if final_price > 0 else 0
|
2025-05-27 17:07:55 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
external_comparisons.append(
|
|
|
|
{
|
|
|
|
"plan_name": ext_price.plan_name,
|
2025-05-30 13:36:09 +02:00
|
|
|
"provider": ext_price.cloud_provider.name,
|
2025-05-27 17:07:55 +02:00
|
|
|
"description": ext_price.description,
|
2025-05-30 13:36:09 +02:00
|
|
|
"amount": ext_price.amount,
|
|
|
|
"currency": ext_price.currency, # Use external price currency
|
2025-05-27 17:07:55 +02:00
|
|
|
"vcpus": ext_price.vcpus,
|
|
|
|
"ram": ext_price.ram,
|
|
|
|
"storage": ext_price.storage,
|
|
|
|
"replicas": ext_price.replicas,
|
2025-05-30 13:36:09 +02:00
|
|
|
"difference": difference,
|
|
|
|
"ratio": ratio,
|
|
|
|
"source": ext_price.source,
|
|
|
|
"date_retrieved": ext_price.date_retrieved,
|
2025-05-27 17:07:55 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
group_name = plan.group.name if plan.group else "No Group"
|
|
|
|
|
2025-05-30 13:43:02 +02:00
|
|
|
# Get storage plans for this cloud provider
|
|
|
|
storage_plans = StoragePlan.objects.filter(
|
|
|
|
cloud_provider=plan.cloud_provider
|
|
|
|
).prefetch_related("prices")
|
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Add pricing data to the grouped structure
|
|
|
|
pricing_data_by_group_and_service_level[group_name][
|
|
|
|
service_level_display
|
|
|
|
].append(
|
2025-05-23 15:56:11 +02:00
|
|
|
{
|
|
|
|
"cloud_provider": plan.cloud_provider.name,
|
|
|
|
"service": appcat_price.service.name,
|
|
|
|
"compute_plan": plan.name,
|
2025-05-23 17:09:02 +02:00
|
|
|
"compute_plan_group": group_name,
|
|
|
|
"compute_plan_group_description": (
|
|
|
|
plan.group.description if plan.group else ""
|
|
|
|
),
|
|
|
|
"compute_plan_group_node_label": (
|
|
|
|
plan.group.node_label if plan.group else ""
|
|
|
|
),
|
2025-05-30 13:43:02 +02:00
|
|
|
"storage_plans": storage_plans,
|
2025-05-23 15:56:11 +02:00
|
|
|
"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,
|
2025-05-23 16:37:03 +02:00
|
|
|
"total_units": total_units,
|
2025-05-23 15:56:11 +02:00
|
|
|
"service_level": service_level_display,
|
|
|
|
"sla_base": base_fee,
|
|
|
|
"sla_per_unit": unit_rate,
|
|
|
|
"sla_price": sla_price,
|
2025-05-23 16:37:03 +02:00
|
|
|
"standard_sla_price": standard_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,
|
2025-05-23 15:56:11 +02:00
|
|
|
"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
|
|
|
|
),
|
2025-05-27 17:07:55 +02:00
|
|
|
"external_comparisons": external_comparisons,
|
2025-05-23 15:56:11 +02:00
|
|
|
}
|
|
|
|
)
|
2025-05-22 16:34:15 +02:00
|
|
|
|
2025-05-23 17:09:02 +02:00
|
|
|
# Order groups correctly, placing "No Group" last
|
|
|
|
ordered_groups_intermediate = {}
|
|
|
|
all_group_names = list(pricing_data_by_group_and_service_level.keys())
|
|
|
|
|
|
|
|
if "No Group" in all_group_names:
|
|
|
|
all_group_names.remove("No Group")
|
|
|
|
all_group_names.append("No Group")
|
|
|
|
|
|
|
|
for group_name_key in all_group_names:
|
|
|
|
ordered_groups_intermediate[group_name_key] = (
|
|
|
|
pricing_data_by_group_and_service_level[group_name_key]
|
|
|
|
)
|
|
|
|
|
|
|
|
# Convert defaultdicts to regular dicts for the template
|
|
|
|
final_context_data = {}
|
|
|
|
for group_key, service_levels_dict in ordered_groups_intermediate.items():
|
|
|
|
final_context_data[group_key] = {
|
|
|
|
sl_key: list(plans_list)
|
|
|
|
for sl_key, plans_list in service_levels_dict.items()
|
|
|
|
}
|
|
|
|
|
2025-05-26 15:36:20 +02:00
|
|
|
# Get filter options for dropdowns
|
|
|
|
all_cloud_providers = (
|
|
|
|
ComputePlan.objects.filter(active=True)
|
|
|
|
.values_list("cloud_provider__name", flat=True)
|
|
|
|
.distinct()
|
|
|
|
.order_by("cloud_provider__name")
|
|
|
|
)
|
|
|
|
all_services = (
|
|
|
|
VSHNAppCatPrice.objects.values_list("service__name", flat=True)
|
|
|
|
.distinct()
|
|
|
|
.order_by("service__name")
|
|
|
|
)
|
|
|
|
all_compute_plan_groups = list(
|
|
|
|
ComputePlan.objects.filter(active=True, group__isnull=False)
|
|
|
|
.values_list("group__name", flat=True)
|
|
|
|
.distinct()
|
|
|
|
.order_by("group__name")
|
|
|
|
)
|
|
|
|
all_compute_plan_groups.append("No Group") # Add option for plans without groups
|
|
|
|
all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices]
|
|
|
|
|
2025-05-23 17:12:05 +02:00
|
|
|
context = {
|
|
|
|
"pricing_data_by_group_and_service_level": final_context_data,
|
|
|
|
"show_discount_details": show_discount_details,
|
2025-05-27 17:07:55 +02:00
|
|
|
"show_price_comparison": show_price_comparison,
|
2025-05-26 15:36:20 +02:00
|
|
|
"filter_cloud_provider": filter_cloud_provider,
|
|
|
|
"filter_service": filter_service,
|
|
|
|
"filter_compute_plan_group": filter_compute_plan_group,
|
|
|
|
"filter_service_level": filter_service_level,
|
|
|
|
"all_cloud_providers": all_cloud_providers,
|
|
|
|
"all_services": all_services,
|
|
|
|
"all_compute_plan_groups": all_compute_plan_groups,
|
|
|
|
"all_service_levels": all_service_levels,
|
2025-05-23 17:12:05 +02:00
|
|
|
}
|
2025-05-22 16:34:15 +02:00
|
|
|
return render(request, "services/pricelist.html", context)
|