compute plan grouping
This commit is contained in:
parent
3896636f9b
commit
19b36b9a2c
7 changed files with 284 additions and 93 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
@ -10,4 +10,6 @@ GitLab CI is used as the main CI/CD system.
|
||||||
The project uses Astral uv to manage the Pythong project, dependencies and the venv.
|
The project uses Astral uv to manage the Pythong project, dependencies and the venv.
|
||||||
Execute Django with `uv run --extra dev manage.py`.
|
Execute Django with `uv run --extra dev manage.py`.
|
||||||
|
|
||||||
|
Always add comments to the code to describe what's happening.
|
||||||
|
|
||||||
Answers should be short and concise, and should not include any unnecessary comments or explanations, but be clear on which file a code block should be placed in.
|
Answers should be short and concise, and should not include any unnecessary comments or explanations, but be clear on which file a code block should be placed in.
|
|
@ -10,6 +10,7 @@ from .models import (
|
||||||
Category,
|
Category,
|
||||||
CloudProvider,
|
CloudProvider,
|
||||||
ComputePlan,
|
ComputePlan,
|
||||||
|
ComputePlanGroup,
|
||||||
ComputePlanPrice,
|
ComputePlanPrice,
|
||||||
ConsultingPartner,
|
ConsultingPartner,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
@ -206,12 +207,29 @@ class ComputePlanPriceInline(admin.TabularInline):
|
||||||
fields = ("currency", "amount")
|
fields = ("currency", "amount")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ComputePlanGroup)
|
||||||
|
class ComputePlanGroupAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||||
|
list_display = ("name", "node_label", "compute_plans_count")
|
||||||
|
search_fields = ("name", "description", "node_label")
|
||||||
|
ordering = ("order",)
|
||||||
|
|
||||||
|
def compute_plans_count(self, obj):
|
||||||
|
return obj.compute_plans.count()
|
||||||
|
|
||||||
|
compute_plans_count.short_description = "Compute Plans"
|
||||||
|
|
||||||
|
|
||||||
class ComputePlanResource(resources.ModelResource):
|
class ComputePlanResource(resources.ModelResource):
|
||||||
cloud_provider = Field(
|
cloud_provider = Field(
|
||||||
column_name="cloud_provider",
|
column_name="cloud_provider",
|
||||||
attribute="cloud_provider",
|
attribute="cloud_provider",
|
||||||
widget=ForeignKeyWidget(CloudProvider, "name"),
|
widget=ForeignKeyWidget(CloudProvider, "name"),
|
||||||
)
|
)
|
||||||
|
group = Field(
|
||||||
|
column_name="group",
|
||||||
|
attribute="group",
|
||||||
|
widget=ForeignKeyWidget(ComputePlanGroup, "name"),
|
||||||
|
)
|
||||||
prices = Field(column_name="prices", attribute=None)
|
prices = Field(column_name="prices", attribute=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -225,6 +243,7 @@ class ComputePlanResource(resources.ModelResource):
|
||||||
"ram",
|
"ram",
|
||||||
"cpu_mem_ratio",
|
"cpu_mem_ratio",
|
||||||
"cloud_provider",
|
"cloud_provider",
|
||||||
|
"group",
|
||||||
"active",
|
"active",
|
||||||
"term",
|
"term",
|
||||||
"valid_from",
|
"valid_from",
|
||||||
|
@ -262,14 +281,15 @@ class ComputePlansAdmin(ImportExportModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
"cloud_provider",
|
"cloud_provider",
|
||||||
|
"group",
|
||||||
"vcpus",
|
"vcpus",
|
||||||
"ram",
|
"ram",
|
||||||
"term",
|
"term",
|
||||||
"display_prices",
|
"display_prices",
|
||||||
"active",
|
"active",
|
||||||
)
|
)
|
||||||
search_fields = ("name", "cloud_provider__name")
|
search_fields = ("name", "cloud_provider__name", "group__name")
|
||||||
list_filter = ("active", "cloud_provider")
|
list_filter = ("active", "cloud_provider", "group")
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
inlines = [ComputePlanPriceInline]
|
inlines = [ComputePlanPriceInline]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 5.2 on 2025-05-23 14:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("services", "0027_alter_category_options_alter_discounttier_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ComputePlanGroup",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200)),
|
||||||
|
("description", models.TextField(blank=True)),
|
||||||
|
(
|
||||||
|
"node_label",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Kubernetes node label for this group", max_length=100
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="computeplan",
|
||||||
|
name="group",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Group this compute plan belongs to",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="compute_plans",
|
||||||
|
to="services.computeplangroup",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 5.2 on 2025-05-23 14:52
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("services", "0028_computeplangroup_computeplan_group"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="computeplangroup",
|
||||||
|
options={"ordering": ["order", "name"]},
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,6 +5,21 @@ from .providers import CloudProvider
|
||||||
from .services import Service
|
from .services import Service
|
||||||
|
|
||||||
|
|
||||||
|
class ComputePlanGroup(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
node_label = models.CharField(
|
||||||
|
max_length=100, help_text="Kubernetes node label for this group"
|
||||||
|
)
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["order", "name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class ComputePlanPrice(models.Model):
|
class ComputePlanPrice(models.Model):
|
||||||
compute_plan = models.ForeignKey(
|
compute_plan = models.ForeignKey(
|
||||||
"ComputePlan", on_delete=models.CASCADE, related_name="prices"
|
"ComputePlan", on_delete=models.CASCADE, related_name="prices"
|
||||||
|
@ -45,6 +60,15 @@ class ComputePlan(models.Model):
|
||||||
CloudProvider, on_delete=models.CASCADE, related_name="compute_plans"
|
CloudProvider, on_delete=models.CASCADE, related_name="compute_plans"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group = models.ForeignKey(
|
||||||
|
ComputePlanGroup,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="compute_plans",
|
||||||
|
help_text="Group this compute plan belongs to",
|
||||||
|
)
|
||||||
|
|
||||||
valid_from = models.DateTimeField(blank=True, null=True)
|
valid_from = models.DateTimeField(blank=True, null=True)
|
||||||
valid_to = models.DateTimeField(blank=True, null=True)
|
valid_to = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
|
|
@ -8,84 +8,110 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="mb-4">Complete Price List - All Service Variants</h1>
|
<h1 class="mb-4">Complete Price List - All Service Variants</h1>
|
||||||
|
|
||||||
{% if pricing_data_by_service_level %}
|
{% if pricing_data_by_group_and_service_level %}
|
||||||
{% for service_level, pricing_data in pricing_data_by_service_level.items %}
|
{% for group_name, service_levels in pricing_data_by_group_and_service_level.items %}
|
||||||
<div class="mb-5">
|
<div class="mb-5 border rounded p-3">
|
||||||
<h2 class="mb-3">{{ service_level }}</h2>
|
<h2 class="mb-3 text-primary">{{ group_name }}</h2>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped table-bordered table-sm">
|
{# Display group description and node_label from first available plan #}
|
||||||
<thead class="table-dark">
|
{% for service_level, pricing_data in service_levels.items %}
|
||||||
<tr>
|
{% if pricing_data and forloop.first %}
|
||||||
<th>Cloud Provider</th>
|
{% with pricing_data.0 as representative_plan %}
|
||||||
<th>Service</th>
|
{% if representative_plan.compute_plan_group_description %}
|
||||||
<th>Compute Plan</th>
|
<p class="text-muted mb-2"><strong>Description:</strong> {{ representative_plan.compute_plan_group_description }}</p>
|
||||||
<th>vCPUs</th>
|
{% endif %}
|
||||||
<th>RAM (GB)</th>
|
{% if representative_plan.compute_plan_group_node_label %}
|
||||||
<th>CPU/Memory Ratio</th>
|
<p class="text-muted mb-3"><strong>Node Label:</strong> <code>{{ representative_plan.compute_plan_group_node_label }}</code></p>
|
||||||
<th>Term</th>
|
{% endif %}
|
||||||
<th>Currency</th>
|
{% endwith %}
|
||||||
<th>Compute Plan Price</th>
|
{% endif %}
|
||||||
<th>Variable Unit</th>
|
{% endfor %}
|
||||||
<th>Units</th>
|
|
||||||
<th>Replica Enforce</th>
|
{% for service_level, pricing_data in service_levels.items %}
|
||||||
<th>SLA Base</th>
|
<div class="mb-4">
|
||||||
<th>SLA Per Unit</th>
|
<h3 class="mb-3 text-secondary">{{ service_level }}</h3>
|
||||||
<th>SLA Price</th>
|
{% if pricing_data %}
|
||||||
<th>Discount Model</th>
|
<div class="table-responsive">
|
||||||
<th>Discount Details</th>
|
<table class="table table-striped table-bordered table-sm">
|
||||||
<th class="table-warning">Final Price</th>
|
<thead class="table-dark">
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<th>Cloud Provider</th>
|
||||||
<tbody>
|
<th>Service</th>
|
||||||
{% for row in pricing_data %}
|
<th>Compute Plan</th>
|
||||||
<tr>
|
<th>vCPUs</th>
|
||||||
<td>{{ row.cloud_provider }}</td>
|
<th>RAM (GB)</th>
|
||||||
<td>{{ row.service }}</td>
|
<th>CPU/Memory Ratio</th>
|
||||||
<td>{{ row.compute_plan }}</td>
|
<th>Term</th>
|
||||||
<td>{{ row.vcpus }}</td>
|
<th>Currency</th>
|
||||||
<td>{{ row.ram }}</td>
|
<th>Compute Plan Price</th>
|
||||||
<td>{{ row.cpu_mem_ratio }}</td>
|
<th>Variable Unit</th>
|
||||||
<td>{{ row.term }}</td>
|
<th>Units</th>
|
||||||
<td>{{ row.currency }}</td>
|
<th>Replica Enforce</th>
|
||||||
<td>{{ row.compute_plan_price|floatformat:2 }}</td>
|
<th>SLA Base</th>
|
||||||
<td>{{ row.variable_unit }}</td>
|
<th>SLA Per Unit</th>
|
||||||
<td>{{ row.units }}</td>
|
<th>SLA Price</th>
|
||||||
<td>{{ row.replica_enforce }}</td>
|
<th>Discount Model</th>
|
||||||
<td>{{ row.sla_base|floatformat:2 }}</td>
|
<th>Discount Details</th>
|
||||||
<td>{{ row.sla_per_unit|floatformat:4 }}</td>
|
<th class="table-warning">Final Price</th>
|
||||||
<td>{{ row.sla_price|floatformat:2 }}</td>
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
{% if row.has_discount %}
|
<tbody>
|
||||||
{{ row.discount_model }}
|
{% for row in pricing_data %}
|
||||||
{% else %}
|
<tr>
|
||||||
None
|
<td>{{ row.cloud_provider }}</td>
|
||||||
{% endif %}
|
<td>{{ row.service }}</td>
|
||||||
</td>
|
<td>{{ row.compute_plan }}</td>
|
||||||
<td>
|
<td>{{ row.vcpus }}</td>
|
||||||
{% if row.has_discount %}
|
<td>{{ row.ram }}</td>
|
||||||
<small class="text-muted">
|
<td>{{ row.cpu_mem_ratio }}</td>
|
||||||
<strong>Total Units:</strong> {{ row.total_units }}<br>
|
<td>{{ row.term }}</td>
|
||||||
<strong>Standard Price:</strong> {{ row.standard_sla_price|floatformat:2 }}<br>
|
<td>{{ row.currency }}</td>
|
||||||
<strong>Discounted Price:</strong> {{ row.discounted_sla_price|floatformat:2 }}<br>
|
<td>{{ row.compute_plan_price|floatformat:2 }}</td>
|
||||||
<strong>Savings:</strong> {{ row.discount_savings|floatformat:2 }} ({{ row.discount_percentage|floatformat:1 }}%)<br>
|
<td>{{ row.variable_unit }}</td>
|
||||||
{% if row.discount_breakdown %}
|
<td>{{ row.units }}</td>
|
||||||
<strong>Breakdown:</strong><br>
|
<td>{{ row.replica_enforce }}</td>
|
||||||
{% for tier in row.discount_breakdown %}
|
<td>{{ row.sla_base|floatformat:2 }}</td>
|
||||||
{{ tier.tier_range }} units: {{ tier.units }} × {{ tier.rate|floatformat:4 }} = {{ tier.subtotal|floatformat:2 }}<br>
|
<td>{{ row.sla_per_unit|floatformat:4 }}</td>
|
||||||
{% endfor %}
|
<td>{{ row.sla_price|floatformat:2 }}</td>
|
||||||
{% endif %}
|
<td>
|
||||||
</small>
|
{% if row.has_discount %}
|
||||||
{% else %}
|
{{ row.discount_model }}
|
||||||
<small class="text-muted">No discount applied</small>
|
{% else %}
|
||||||
{% endif %}
|
None
|
||||||
</td>
|
{% endif %}
|
||||||
<td class="table-warning fw-bold">{{ row.final_price|floatformat:2 }}</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
{% endfor %}
|
{% if row.has_discount %}
|
||||||
</tbody>
|
<small class="text-muted">
|
||||||
</table>
|
<strong>Total Units:</strong> {{ row.total_units }}<br>
|
||||||
</div>
|
<strong>Standard Price:</strong> {{ row.standard_sla_price|floatformat:2 }}<br>
|
||||||
<p class="text-muted"><strong>{{ pricing_data|length }}</strong> variants for {{ service_level }}</p>
|
<strong>Discounted Price:</strong> {{ row.discounted_sla_price|floatformat:2 }}<br>
|
||||||
|
<strong>Savings:</strong> {{ row.discount_savings|floatformat:2 }} ({{ row.discount_percentage|floatformat:1 }}%)<br>
|
||||||
|
{% if row.discount_breakdown %}
|
||||||
|
<strong>Breakdown:</strong><br>
|
||||||
|
{% for tier in row.discount_breakdown %}
|
||||||
|
{{ tier.tier_range }} units: {{ tier.units }} × {{ tier.rate|floatformat:4 }} = {{ tier.subtotal|floatformat:2 }}<br>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">No discount applied</small>
|
||||||
|
{% 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 }} in {{ group_name }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No pricing variants available for {{ service_level }} in {{ group_name }}.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted">No service levels with pricing data found for group: {{ group_name }}.</p>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -5,22 +5,33 @@ from hub.services.models import ComputePlan, VSHNAppCatPrice
|
||||||
|
|
||||||
|
|
||||||
def natural_sort_key(name):
|
def natural_sort_key(name):
|
||||||
|
"""Extract numeric part from compute plan name for natural sorting"""
|
||||||
match = re.search(r"compute-std-(\d+)", name)
|
match = re.search(r"compute-std-(\d+)", name)
|
||||||
return int(match.group(1)) if match else 0
|
return int(match.group(1)) if match else 0
|
||||||
|
|
||||||
|
|
||||||
def pricelist(request):
|
def pricelist(request):
|
||||||
|
"""Generate comprehensive price list grouped by compute plan groups and service levels"""
|
||||||
|
# Fetch all active compute plans with related data
|
||||||
compute_plans = (
|
compute_plans = (
|
||||||
ComputePlan.objects.filter(active=True)
|
ComputePlan.objects.filter(active=True)
|
||||||
.select_related("cloud_provider")
|
.select_related("cloud_provider", "group")
|
||||||
.prefetch_related("prices")
|
.prefetch_related("prices")
|
||||||
.order_by("cloud_provider__name")
|
.order_by("group__order", "group__name", "cloud_provider__name")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Apply natural sorting for compute plan names
|
||||||
compute_plans = sorted(
|
compute_plans = sorted(
|
||||||
compute_plans, key=lambda x: (x.cloud_provider.name, natural_sort_key(x.name))
|
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),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fetch all appcat price configurations
|
||||||
appcat_prices = (
|
appcat_prices = (
|
||||||
VSHNAppCatPrice.objects.all()
|
VSHNAppCatPrice.objects.all()
|
||||||
.select_related("service", "discount_model")
|
.select_related("service", "discount_model")
|
||||||
|
@ -28,13 +39,15 @@ def pricelist(request):
|
||||||
.order_by("service__name")
|
.order_by("service__name")
|
||||||
)
|
)
|
||||||
|
|
||||||
pricing_data_by_service_level = defaultdict(list)
|
pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list))
|
||||||
processed_combinations = set()
|
processed_combinations = set()
|
||||||
|
|
||||||
|
# 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
|
||||||
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
|
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
|
||||||
units = int(plan.ram)
|
units = int(plan.ram)
|
||||||
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
|
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
|
||||||
|
@ -57,6 +70,7 @@ def pricelist(request):
|
||||||
).values_list("currency", flat=True)
|
).values_list("currency", flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Find currencies that exist across all pricing components
|
||||||
matching_currencies = plan_currencies.intersection(
|
matching_currencies = plan_currencies.intersection(
|
||||||
base_fee_currencies
|
base_fee_currencies
|
||||||
).intersection(unit_rate_currencies)
|
).intersection(unit_rate_currencies)
|
||||||
|
@ -73,23 +87,25 @@ def pricelist(request):
|
||||||
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
|
||||||
compute_plan_price = plan.get_price(currency)
|
compute_plan_price = plan.get_price(currency)
|
||||||
if compute_plan_price is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
base_fee = appcat_price.get_base_fee(currency)
|
base_fee = appcat_price.get_base_fee(currency)
|
||||||
if base_fee is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
unit_rate = appcat_price.get_unit_rate(currency, service_level)
|
unit_rate = appcat_price.get_unit_rate(currency, service_level)
|
||||||
if unit_rate is None:
|
|
||||||
|
# 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
|
||||||
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:
|
||||||
|
@ -98,6 +114,7 @@ def pricelist(request):
|
||||||
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
|
||||||
discount_breakdown = None
|
discount_breakdown = None
|
||||||
if (
|
if (
|
||||||
appcat_price.discount_model
|
appcat_price.discount_model
|
||||||
|
@ -131,11 +148,23 @@ def pricelist(request):
|
||||||
service_level
|
service_level
|
||||||
]
|
]
|
||||||
|
|
||||||
pricing_data_by_service_level[service_level_display].append(
|
group_name = plan.group.name if plan.group else "No Group"
|
||||||
|
|
||||||
|
# Add pricing data to the grouped structure
|
||||||
|
pricing_data_by_group_and_service_level[group_name][
|
||||||
|
service_level_display
|
||||||
|
].append(
|
||||||
{
|
{
|
||||||
"cloud_provider": plan.cloud_provider.name,
|
"cloud_provider": plan.cloud_provider.name,
|
||||||
"service": appcat_price.service.name,
|
"service": appcat_price.service.name,
|
||||||
"compute_plan": plan.name,
|
"compute_plan": plan.name,
|
||||||
|
"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 ""
|
||||||
|
),
|
||||||
"vcpus": plan.vcpus,
|
"vcpus": plan.vcpus,
|
||||||
"ram": plan.ram,
|
"ram": plan.ram,
|
||||||
"cpu_mem_ratio": plan.cpu_mem_ratio,
|
"cpu_mem_ratio": plan.cpu_mem_ratio,
|
||||||
|
@ -173,5 +202,26 @@ def pricelist(request):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {"pricing_data_by_service_level": dict(pricing_data_by_service_level)}
|
# 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {"pricing_data_by_group_and_service_level": final_context_data}
|
||||||
return render(request, "services/pricelist.html", context)
|
return render(request, "services/pricelist.html", context)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue