website/hub/services/admin/pricing.py

342 lines
10 KiB
Python
Raw Normal View History

2025-05-26 11:33:04 +02:00
"""
Admin classes for pricing models including compute plans, storage plans, and VSHN AppCat pricing
"""
2025-01-27 14:58:23 +01:00
from django.contrib import admin
2025-01-27 15:14:58 +01:00
from django.utils.html import format_html
2025-03-07 14:58:51 +01:00
from adminsortable2.admin import SortableAdminMixin
2025-05-20 14:26:31 +02:00
from import_export.admin import ImportExportModelAdmin
from import_export import resources
from import_export.fields import Field
from import_export.widgets import ForeignKeyWidget
2025-03-07 14:58:51 +01:00
2025-05-26 11:33:04 +02:00
from ..models import (
2025-05-20 11:26:52 +02:00
ComputePlan,
2025-05-23 17:09:02 +02:00
ComputePlanGroup,
2025-05-20 15:27:45 +02:00
ComputePlanPrice,
2025-05-26 11:33:04 +02:00
CloudProvider,
2025-05-22 16:34:15 +02:00
StoragePlan,
StoragePlanPrice,
2025-05-20 15:27:45 +02:00
VSHNAppCatBaseFee,
VSHNAppCatPrice,
VSHNAppCatUnitRate,
2025-05-26 11:33:04 +02:00
ProgressiveDiscountModel,
DiscountTier,
2025-05-27 17:07:55 +02:00
ExternalPricePlans,
2025-01-28 08:53:00 +01:00
)
2025-01-27 15:23:50 +01:00
2025-05-26 11:33:04 +02:00
class ComputePlanPriceInline(admin.TabularInline):
"""Inline admin for ComputePlanPrice model"""
2025-05-26 11:33:04 +02:00
model = ComputePlanPrice
extra = 1
2025-05-26 11:33:04 +02:00
fields = ("currency", "amount")
2025-01-27 14:58:23 +01:00
2025-05-20 15:27:45 +02:00
class ComputePlanItemInline(admin.TabularInline):
2025-05-26 11:33:04 +02:00
"""Inline admin for ComputePlan model"""
2025-05-20 11:26:52 +02:00
model = ComputePlan
extra = 1
2025-05-20 15:27:45 +02:00
fields = ("name", "vcpus", "ram", "active", "valid_from", "valid_to")
2025-05-20 11:26:52 +02:00
2025-05-23 17:09:02 +02:00
@admin.register(ComputePlanGroup)
class ComputePlanGroupAdmin(SortableAdminMixin, admin.ModelAdmin):
2025-05-26 11:33:04 +02:00
"""Admin configuration for ComputePlanGroup model"""
2025-05-23 17:09:02 +02:00
list_display = ("name", "node_label", "compute_plans_count")
search_fields = ("name", "description", "node_label")
ordering = ("order",)
def compute_plans_count(self, obj):
2025-05-26 11:33:04 +02:00
"""Display count of compute plans in this group"""
2025-05-23 17:09:02 +02:00
return obj.compute_plans.count()
compute_plans_count.short_description = "Compute Plans"
2025-05-20 14:26:31 +02:00
class ComputePlanResource(resources.ModelResource):
2025-05-26 11:33:04 +02:00
"""Import/Export resource for ComputePlan model"""
2025-05-20 14:26:31 +02:00
cloud_provider = Field(
column_name="cloud_provider",
attribute="cloud_provider",
widget=ForeignKeyWidget(CloudProvider, "name"),
)
2025-05-23 17:09:02 +02:00
group = Field(
column_name="group",
attribute="group",
widget=ForeignKeyWidget(ComputePlanGroup, "name"),
)
2025-05-22 16:34:15 +02:00
prices = Field(column_name="prices", attribute=None)
2025-05-20 14:26:31 +02:00
class Meta:
model = ComputePlan
skip_unchanged = True
report_skipped = False
import_id_fields = ["name"]
fields = (
"name",
"vcpus",
"ram",
"cpu_mem_ratio",
"cloud_provider",
2025-05-23 17:09:02 +02:00
"group",
2025-05-20 15:27:45 +02:00
"active",
2025-05-22 16:34:15 +02:00
"term",
2025-05-20 15:27:45 +02:00
"valid_from",
"valid_to",
2025-05-22 16:34:15 +02:00
"prices",
2025-05-20 14:26:31 +02:00
)
2025-05-22 16:34:15 +02:00
def dehydrate_prices(self, compute_plan):
2025-05-26 11:33:04 +02:00
"""Export prices in a custom format"""
2025-05-22 16:34:15 +02:00
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):
2025-05-26 11:33:04 +02:00
"""Handle many-to-many relationships during import"""
2025-05-22 16:34:15 +02:00
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
)
2025-05-20 14:26:31 +02:00
2025-05-20 11:26:52 +02:00
@admin.register(ComputePlan)
2025-05-20 14:26:31 +02:00
class ComputePlansAdmin(ImportExportModelAdmin):
2025-05-26 11:33:04 +02:00
"""Admin configuration for ComputePlan model with import/export functionality"""
2025-05-20 14:26:31 +02:00
resource_class = ComputePlanResource
2025-05-20 15:27:45 +02:00
list_display = (
"name",
"cloud_provider",
2025-05-23 17:09:02 +02:00
"group",
2025-05-20 15:27:45 +02:00
"vcpus",
"ram",
2025-05-22 16:34:15 +02:00
"term",
2025-05-20 15:27:45 +02:00
"display_prices",
"active",
)
2025-05-23 17:09:02 +02:00
search_fields = ("name", "cloud_provider__name", "group__name")
list_filter = ("active", "cloud_provider", "group")
2025-05-20 11:26:52 +02:00
ordering = ("name",)
2025-05-20 15:27:45 +02:00
inlines = [ComputePlanPriceInline]
def display_prices(self, obj):
2025-05-26 11:33:04 +02:00
"""Display formatted prices for the list view"""
2025-05-20 15:27:45 +02:00
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)"
class VSHNAppCatBaseFeeInline(admin.TabularInline):
2025-05-26 11:33:04 +02:00
"""Inline admin for VSHNAppCatBaseFee model"""
2025-05-20 15:27:45 +02:00
model = VSHNAppCatBaseFee
extra = 1
fields = ("currency", "amount")
class VSHNAppCatUnitRateInline(admin.TabularInline):
2025-05-26 11:33:04 +02:00
"""Inline admin for VSHNAppCatUnitRate model"""
2025-05-20 15:27:45 +02:00
model = VSHNAppCatUnitRate
extra = 1
fields = ("currency", "service_level", "amount")
2025-05-22 16:52:34 +02:00
class DiscountTierInline(admin.TabularInline):
2025-05-26 11:33:04 +02:00
"""Inline admin for DiscountTier model"""
2025-05-22 16:52:34 +02:00
model = DiscountTier
extra = 1
2025-05-23 16:37:03 +02:00
fields = ("min_units", "max_units", "discount_percent")
ordering = ("min_units",)
2025-05-22 16:52:34 +02:00
@admin.register(ProgressiveDiscountModel)
class ProgressiveDiscountModelAdmin(admin.ModelAdmin):
2025-05-26 11:33:04 +02:00
"""Admin configuration for ProgressiveDiscountModel"""
2025-05-22 16:52:34 +02:00
list_display = ("name", "description", "active")
search_fields = ("name", "description")
inlines = [DiscountTierInline]
2025-05-20 15:27:45 +02:00
@admin.register(VSHNAppCatPrice)
class VSHNAppCatPriceAdmin(admin.ModelAdmin):
2025-05-26 11:33:04 +02:00
"""Admin configuration for VSHNAppCatPrice model"""
2025-05-20 15:27:45 +02:00
list_display = (
"service",
"variable_unit",
2025-05-22 16:34:15 +02:00
"term",
2025-05-22 16:52:34 +02:00
"discount_model",
2025-05-20 15:27:45 +02:00
"admin_display_base_fees",
"admin_display_unit_rates",
)
2025-05-22 16:52:34 +02:00
list_filter = ("variable_unit", "service", "discount_model")
2025-05-20 15:27:45 +02:00
search_fields = ("service__name",)
inlines = [VSHNAppCatBaseFeeInline, VSHNAppCatUnitRateInline]
def admin_display_base_fees(self, obj):
2025-05-26 11:33:04 +02:00
"""Display base fees in admin list view"""
2025-05-20 15:27:45 +02:00
fees = obj.base_fees.all()
if not fees:
return "No base fees"
return format_html(
"<br>".join([f"{fee.amount} {fee.currency}" for fee in fees])
)
admin_display_base_fees.short_description = "Base Fees"
def admin_display_unit_rates(self, obj):
2025-05-26 11:33:04 +02:00
"""Display unit rates in admin list view"""
2025-05-20 15:27:45 +02:00
rates = obj.unit_rates.all()
if not rates:
return "No unit rates"
return format_html(
"<br>".join(
[
f"{rate.amount} {rate.currency} ({rate.get_service_level_display()})"
for rate in rates
]
)
)
admin_display_unit_rates.short_description = "Unit Rates"
2025-05-22 16:34:15 +02:00
class StoragePlanPriceInline(admin.TabularInline):
2025-05-26 11:33:04 +02:00
"""Inline admin for StoragePlanPrice model"""
2025-05-22 16:34:15 +02:00
model = StoragePlanPrice
extra = 1
fields = ("currency", "amount")
class StoragePlanResource(resources.ModelResource):
2025-05-26 11:33:04 +02:00
"""Import/Export resource for StoragePlan model"""
2025-05-22 16:34:15 +02:00
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):
2025-05-26 11:33:04 +02:00
"""Export prices in a custom format"""
2025-05-22 16:34:15 +02:00
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):
2025-05-26 11:33:04 +02:00
"""Handle many-to-many relationships during import"""
2025-05-22 16:34:15 +02:00
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):
2025-05-26 11:33:04 +02:00
"""Admin configuration for StoragePlan model with import/export functionality"""
2025-05-22 16:34:15 +02:00
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):
2025-05-26 11:33:04 +02:00
"""Display formatted prices for the list view"""
2025-05-22 16:34:15 +02:00
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)"
2025-05-27 17:07:55 +02:00
@admin.register(ExternalPricePlans)
class ExternalPricePlansAdmin(admin.ModelAdmin):
"""Admin configuration for ExternalPricePlans model"""
list_display = (
"plan_name",
"cloud_provider",
"service",
"currency",
"amount",
"display_compare_to_count",
"date_retrieved",
)
list_filter = ("cloud_provider", "service", "currency", "term")
search_fields = ("plan_name", "cloud_provider__name", "service__name")
ordering = ("cloud_provider", "service", "plan_name")
# Configure many-to-many field display
filter_horizontal = ("compare_to",)
def display_compare_to_count(self, obj):
"""Display count of compute plans this external price compares to"""
count = obj.compare_to.count()
if count == 0:
return "No comparisons"
return f"{count} plan{'s' if count != 1 else ''}"
display_compare_to_count.short_description = "Compare To"