continued work on price model
This commit is contained in:
parent
6f41c8c344
commit
a6a15150ea
10 changed files with 500 additions and 1 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
@ -1,6 +1,6 @@
|
||||||
This is a Django project which uses SQLite as the database.
|
This is a Django project which uses SQLite as the database.
|
||||||
Follow the Django conventions and best practices, use the Django ORM and define the fields with appropriate types
|
Follow the Django conventions and best practices, use the Django ORM and define the fields with appropriate types
|
||||||
Use class-based views and follow the Django conventions for naming and structuring views.
|
Use function-based views and follow the Django conventions for naming and structuring views.
|
||||||
Templates use the Django template language and Bootstrap 5 CSS and JavaScript for styling.
|
Templates use the Django template language and Bootstrap 5 CSS and JavaScript for styling.
|
||||||
The main Django app is in hub/services, ignore the app hub/broker.
|
The main Django app is in hub/services, ignore the app hub/broker.
|
||||||
Docker specific code is in the folder docker/.
|
Docker specific code is in the folder docker/.
|
||||||
|
|
|
@ -19,6 +19,8 @@ from .models import (
|
||||||
ReusableText,
|
ReusableText,
|
||||||
Service,
|
Service,
|
||||||
ServiceOffering,
|
ServiceOffering,
|
||||||
|
StoragePlan,
|
||||||
|
StoragePlanPrice,
|
||||||
VSHNAppCatBaseFee,
|
VSHNAppCatBaseFee,
|
||||||
VSHNAppCatPrice,
|
VSHNAppCatPrice,
|
||||||
VSHNAppCatUnitRate,
|
VSHNAppCatUnitRate,
|
||||||
|
@ -208,6 +210,7 @@ class ComputePlanResource(resources.ModelResource):
|
||||||
attribute="cloud_provider",
|
attribute="cloud_provider",
|
||||||
widget=ForeignKeyWidget(CloudProvider, "name"),
|
widget=ForeignKeyWidget(CloudProvider, "name"),
|
||||||
)
|
)
|
||||||
|
prices = Field(column_name="prices", attribute=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ComputePlan
|
model = ComputePlan
|
||||||
|
@ -221,10 +224,35 @@ class ComputePlanResource(resources.ModelResource):
|
||||||
"cpu_mem_ratio",
|
"cpu_mem_ratio",
|
||||||
"cloud_provider",
|
"cloud_provider",
|
||||||
"active",
|
"active",
|
||||||
|
"term",
|
||||||
"valid_from",
|
"valid_from",
|
||||||
"valid_to",
|
"valid_to",
|
||||||
|
"prices",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def dehydrate_prices(self, compute_plan):
|
||||||
|
prices = compute_plan.prices.all()
|
||||||
|
if not prices:
|
||||||
|
return ""
|
||||||
|
return "|".join([f"{p.currency} {p.amount}" for p in prices])
|
||||||
|
|
||||||
|
def save_m2m(self, instance, row, *args, **kwargs):
|
||||||
|
super().save_m2m(instance, row, *args, **kwargs)
|
||||||
|
|
||||||
|
# Handle prices
|
||||||
|
if "prices" in row and row["prices"]:
|
||||||
|
# Clear existing prices first
|
||||||
|
instance.prices.all().delete()
|
||||||
|
|
||||||
|
# Create new prices
|
||||||
|
price_entries = row["prices"].split("|")
|
||||||
|
for entry in price_entries:
|
||||||
|
if " " in entry:
|
||||||
|
currency, amount = entry.split(" ")
|
||||||
|
ComputePlanPrice.objects.create(
|
||||||
|
compute_plan=instance, currency=currency, amount=amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ComputePlan)
|
@admin.register(ComputePlan)
|
||||||
class ComputePlansAdmin(ImportExportModelAdmin):
|
class ComputePlansAdmin(ImportExportModelAdmin):
|
||||||
|
@ -234,6 +262,7 @@ class ComputePlansAdmin(ImportExportModelAdmin):
|
||||||
"cloud_provider",
|
"cloud_provider",
|
||||||
"vcpus",
|
"vcpus",
|
||||||
"ram",
|
"ram",
|
||||||
|
"term",
|
||||||
"display_prices",
|
"display_prices",
|
||||||
"active",
|
"active",
|
||||||
)
|
)
|
||||||
|
@ -268,6 +297,7 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"service",
|
"service",
|
||||||
"variable_unit",
|
"variable_unit",
|
||||||
|
"term",
|
||||||
"admin_display_base_fees",
|
"admin_display_base_fees",
|
||||||
"admin_display_unit_rates",
|
"admin_display_unit_rates",
|
||||||
)
|
)
|
||||||
|
@ -299,3 +329,80 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
|
|
||||||
admin_display_unit_rates.short_description = "Unit Rates"
|
admin_display_unit_rates.short_description = "Unit Rates"
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePlanPriceInline(admin.TabularInline):
|
||||||
|
model = StoragePlanPrice
|
||||||
|
extra = 1
|
||||||
|
fields = ("currency", "amount")
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePlanResource(resources.ModelResource):
|
||||||
|
cloud_provider = Field(
|
||||||
|
column_name="cloud_provider",
|
||||||
|
attribute="cloud_provider",
|
||||||
|
widget=ForeignKeyWidget(CloudProvider, "name"),
|
||||||
|
)
|
||||||
|
prices = Field(column_name="prices", attribute=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = StoragePlan
|
||||||
|
skip_unchanged = True
|
||||||
|
report_skipped = False
|
||||||
|
import_id_fields = ["name"]
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
"cloud_provider",
|
||||||
|
"term",
|
||||||
|
"unit",
|
||||||
|
"valid_from",
|
||||||
|
"valid_to",
|
||||||
|
"prices",
|
||||||
|
)
|
||||||
|
|
||||||
|
def dehydrate_prices(self, storage_plan):
|
||||||
|
prices = storage_plan.prices.all()
|
||||||
|
if not prices:
|
||||||
|
return ""
|
||||||
|
return "|".join([f"{p.currency} {p.amount}" for p in prices])
|
||||||
|
|
||||||
|
def save_m2m(self, instance, row, *args, **kwargs):
|
||||||
|
super().save_m2m(instance, row, *args, **kwargs)
|
||||||
|
|
||||||
|
# Handle prices
|
||||||
|
if "prices" in row and row["prices"]:
|
||||||
|
# Clear existing prices first
|
||||||
|
instance.prices.all().delete()
|
||||||
|
|
||||||
|
# Create new prices
|
||||||
|
price_entries = row["prices"].split("|")
|
||||||
|
for entry in price_entries:
|
||||||
|
if " " in entry:
|
||||||
|
currency, amount = entry.split(" ")
|
||||||
|
StoragePlanPrice.objects.create(
|
||||||
|
storage_plan=instance, currency=currency, amount=amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StoragePlan)
|
||||||
|
class StoragePlanAdmin(ImportExportModelAdmin):
|
||||||
|
resource_class = StoragePlanResource
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"cloud_provider",
|
||||||
|
"term",
|
||||||
|
"unit",
|
||||||
|
"display_prices",
|
||||||
|
)
|
||||||
|
search_fields = ("name", "cloud_provider__name")
|
||||||
|
list_filter = ("cloud_provider",)
|
||||||
|
ordering = ("name",)
|
||||||
|
inlines = [StoragePlanPriceInline]
|
||||||
|
|
||||||
|
def display_prices(self, obj):
|
||||||
|
prices = obj.prices.all()
|
||||||
|
if not prices:
|
||||||
|
return "No prices set"
|
||||||
|
return format_html("<br>".join([f"{p.amount} {p.currency}" for p in prices]))
|
||||||
|
|
||||||
|
display_prices.short_description = "Prices (Amount Currency)"
|
||||||
|
|
87
hub/services/migrations/0024_storageplan_storageplanprice.py
Normal file
87
hub/services/migrations/0024_storageplan_storageplanprice.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# Generated by Django 5.2 on 2025-05-22 14:13
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("services", "0023_alter_computeplan_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="StoragePlan",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200)),
|
||||||
|
("valid_from", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("valid_to", models.DateTimeField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"cloud_provider",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="storage_plans",
|
||||||
|
to="services.cloudprovider",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["name"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="StoragePlanPrice",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"currency",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("CHF", "Swiss Franc"),
|
||||||
|
("EUR", "Euro"),
|
||||||
|
("USD", "US Dollar"),
|
||||||
|
],
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"amount",
|
||||||
|
models.DecimalField(
|
||||||
|
decimal_places=2,
|
||||||
|
help_text="Price in the specified currency, excl. VAT",
|
||||||
|
max_digits=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"storage_plan",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="prices",
|
||||||
|
to="services.storageplan",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["currency"],
|
||||||
|
"unique_together": {("storage_plan", "currency")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Generated by Django 5.2 on 2025-05-22 14:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("services", "0024_storageplan_storageplanprice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="computeplan",
|
||||||
|
name="term",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("MTH", "per Month (30d)"),
|
||||||
|
("DAY", "per Day"),
|
||||||
|
("HR", "per Hour"),
|
||||||
|
("MIN", "per Minute"),
|
||||||
|
],
|
||||||
|
default="MTH",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="storageplan",
|
||||||
|
name="term",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("MTH", "per Month (30d)"),
|
||||||
|
("DAY", "per Day"),
|
||||||
|
("HR", "per Hour"),
|
||||||
|
("MIN", "per Minute"),
|
||||||
|
],
|
||||||
|
default="MTH",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="storageplan",
|
||||||
|
name="unit",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("GIB", "GiB"), ("MIB", "MiB"), ("CPU", "vCPU")],
|
||||||
|
default="GIB",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="vshnappcatprice",
|
||||||
|
name="term",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("MTH", "per Month (30d)"),
|
||||||
|
("DAY", "per Day"),
|
||||||
|
("HR", "per Hour"),
|
||||||
|
("MIN", "per Minute"),
|
||||||
|
],
|
||||||
|
default="MTH",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="storageplan",
|
||||||
|
unique_together={
|
||||||
|
("cloud_provider", "term", "unit", "valid_from", "valid_to")
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -362,6 +362,19 @@ class Currency(models.TextChoices):
|
||||||
USD = "USD", "US Dollar"
|
USD = "USD", "US Dollar"
|
||||||
|
|
||||||
|
|
||||||
|
class Term(models.TextChoices):
|
||||||
|
MTH = "MTH", "per Month (30d)"
|
||||||
|
DAY = "DAY", "per Day"
|
||||||
|
HR = "HR", "per Hour"
|
||||||
|
MIN = "MIN", "per Minute"
|
||||||
|
|
||||||
|
|
||||||
|
class Unit(models.TextChoices):
|
||||||
|
GIB = "GIB", "GiB"
|
||||||
|
MIB = "MIB", "MiB"
|
||||||
|
CPU = "CPU", "vCPU"
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
@ -392,6 +405,11 @@ class ComputePlan(models.Model):
|
||||||
help_text="vCPU to Memory ratio. How much vCPU per GiB RAM is available?"
|
help_text="vCPU to Memory ratio. How much vCPU per GiB RAM is available?"
|
||||||
)
|
)
|
||||||
active = models.BooleanField(default=True, help_text="Is the plan active?")
|
active = models.BooleanField(default=True, help_text="Is the plan active?")
|
||||||
|
term = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
default=Term.MTH,
|
||||||
|
choices=Term.choices,
|
||||||
|
)
|
||||||
|
|
||||||
cloud_provider = models.ForeignKey(
|
cloud_provider = models.ForeignKey(
|
||||||
CloudProvider, on_delete=models.CASCADE, related_name="compute_plans"
|
CloudProvider, on_delete=models.CASCADE, related_name="compute_plans"
|
||||||
|
@ -413,6 +431,61 @@ class ComputePlan(models.Model):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePlanPrice(models.Model):
|
||||||
|
storage_plan = models.ForeignKey(
|
||||||
|
"StoragePlan", on_delete=models.CASCADE, related_name="prices"
|
||||||
|
)
|
||||||
|
currency = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
choices=Currency.choices,
|
||||||
|
)
|
||||||
|
amount = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
help_text="Price in the specified currency, excl. VAT",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("storage_plan", "currency")
|
||||||
|
ordering = ["currency"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.storage_plan.name} - {self.amount} {self.currency}"
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePlan(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
cloud_provider = models.ForeignKey(
|
||||||
|
CloudProvider, on_delete=models.CASCADE, related_name="storage_plans"
|
||||||
|
)
|
||||||
|
term = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
default=Term.MTH,
|
||||||
|
choices=Term.choices,
|
||||||
|
)
|
||||||
|
unit = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
default=Unit.GIB,
|
||||||
|
choices=Unit.choices,
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_from = models.DateTimeField(blank=True, null=True)
|
||||||
|
valid_to = models.DateTimeField(blank=True, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("cloud_provider", "term", "unit", "valid_from", "valid_to")
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_price(self, currency_code: str):
|
||||||
|
try:
|
||||||
|
return self.prices.get(currency=currency_code).amount
|
||||||
|
except ComputePlanPrice.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class VSHNAppCatBaseFee(models.Model):
|
class VSHNAppCatBaseFee(models.Model):
|
||||||
vshn_appcat_price_config = models.ForeignKey(
|
vshn_appcat_price_config = models.ForeignKey(
|
||||||
"VSHNAppCatPrice", on_delete=models.CASCADE, related_name="base_fees"
|
"VSHNAppCatPrice", on_delete=models.CASCADE, related_name="base_fees"
|
||||||
|
@ -459,6 +532,11 @@ class VSHNAppCatPrice(models.Model):
|
||||||
ha_replica_max = models.IntegerField(
|
ha_replica_max = models.IntegerField(
|
||||||
default=1, help_text="Maximum supported replicas"
|
default=1, help_text="Maximum supported replicas"
|
||||||
)
|
)
|
||||||
|
term = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
default=Term.MTH,
|
||||||
|
choices=Term.choices,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.service.name} - {self.get_variable_unit_display()} based pricing"
|
return f"{self.service.name} - {self.get_variable_unit_display()} based pricing"
|
||||||
|
|
78
hub/services/templates/services/pricelist.html
Normal file
78
hub/services/templates/services/pricelist.html
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Compute Plan Price Comparison</h1>
|
||||||
|
|
||||||
|
{% for plan_data in plans_data %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ plan_data.plan.name }}</h2>
|
||||||
|
</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>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>Currency</th>
|
||||||
|
<th>Final Price</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.currency }}</td>
|
||||||
|
<td>{{ price.price }}</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 %}
|
||||||
|
{% endblock %}
|
|
@ -22,4 +22,9 @@ urlpatterns = [
|
||||||
path("contact/thank-you/", views.thank_you, name="thank_you"),
|
path("contact/thank-you/", views.thank_you, name="thank_you"),
|
||||||
path("contact-form/", views.contact_form, name="contact_form"),
|
path("contact-form/", views.contact_form, name="contact_form"),
|
||||||
path("subscribe/", views.subscribe, name="subscribe"),
|
path("subscribe/", views.subscribe, name="subscribe"),
|
||||||
|
path(
|
||||||
|
"pricelist/",
|
||||||
|
views.compute_plan_price_comparison,
|
||||||
|
name="pricelist",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,3 +5,4 @@ from .providers import *
|
||||||
from .services import *
|
from .services import *
|
||||||
from .pages import *
|
from .pages import *
|
||||||
from .subscriptions import *
|
from .subscriptions import *
|
||||||
|
from .pricelist import *
|
||||||
|
|
72
hub/services/views/pricelist.py
Normal file
72
hub/services/views/pricelist.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from hub.services.models import ComputePlan, VSHNAppCatPrice, VSHNAppCatUnitRate
|
||||||
|
|
||||||
|
|
||||||
|
def compute_plan_price_comparison(request):
|
||||||
|
# Get all compute plans and app catalog prices
|
||||||
|
compute_plans = (
|
||||||
|
ComputePlan.objects.all()
|
||||||
|
.select_related("cloud_provider")
|
||||||
|
.prefetch_related("prices")
|
||||||
|
)
|
||||||
|
appcat_prices = (
|
||||||
|
VSHNAppCatPrice.objects.all()
|
||||||
|
.select_related("service")
|
||||||
|
.prefetch_related("base_fees", "unit_rates")
|
||||||
|
)
|
||||||
|
|
||||||
|
plans_data = []
|
||||||
|
|
||||||
|
for plan in compute_plans:
|
||||||
|
plan_data = {"plan": plan, "calculated_prices": []}
|
||||||
|
|
||||||
|
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:
|
||||||
|
units = int(plan.ram)
|
||||||
|
elif price_config.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
|
||||||
|
|
||||||
|
# Get all currencies used in base fees
|
||||||
|
currencies = price_config.base_fees.values_list(
|
||||||
|
"currency", 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
if final_price is not None:
|
||||||
|
service_level_display = dict(
|
||||||
|
VSHNAppCatPrice.ServiceLevel.choices
|
||||||
|
)[service_level]
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
plans_data.append(plan_data)
|
||||||
|
|
||||||
|
context = {"plans_data": plans_data}
|
||||||
|
return render(request, "services/pricelist.html", context)
|
|
@ -246,6 +246,7 @@ JAZZMIN_SETTINGS = {
|
||||||
],
|
],
|
||||||
"show_sidebar": True,
|
"show_sidebar": True,
|
||||||
"navigation_expanded": True,
|
"navigation_expanded": True,
|
||||||
|
"hide_apps": ["hub.broker"],
|
||||||
}
|
}
|
||||||
|
|
||||||
IMPORT_EXPORT_FORMATS = [CSV]
|
IMPORT_EXPORT_FORMATS = [CSV]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue