improved pricelist view

This commit is contained in:
Tobias Brunner 2025-06-20 09:30:51 +02:00
parent 3f3b9da992
commit a8f204dcb4
No known key found for this signature in database
2 changed files with 271 additions and 34 deletions

View file

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% load math_tags %}
{% block title %}Complete Price List{% endblock %} {% block title %}Complete Price List{% endblock %}
@ -51,6 +52,69 @@
.servala-row { .servala-row {
border-bottom: 2px solid #007bff; border-bottom: 2px solid #007bff;
} }
/* Price calculation breakdown styling */
.price-breakdown-header {
background: linear-gradient(135deg, #28a745, #20b2aa);
}
.compute-plan-col {
background-color: rgba(13, 110, 253, 0.1);
border-right: 2px solid #0d6efd;
}
.sla-base-col {
background-color: rgba(111, 66, 193, 0.1);
border-right: 2px solid #6f42c1;
}
.sla-units-col {
background-color: rgba(253, 126, 20, 0.1);
border-right: 2px solid #fd7e14;
}
.mandatory-addons-col {
background-color: rgba(220, 53, 69, 0.1);
border-right: 2px solid #dc3545;
}
.total-sla-col {
background-color: rgba(25, 135, 84, 0.2);
border-right: 3px solid #198754;
}
/* Mathematical operator styling */
.math-operator {
font-size: 1.2em;
font-weight: bold;
color: #666;
padding: 0 5px;
}
/* Price calculation formula helper */
.price-formula {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 10px;
margin-bottom: 20px;
font-family: monospace;
text-align: center;
}
.price-formula .formula-part {
display: inline-block;
padding: 2px 8px;
margin: 0 5px;
border-radius: 3px;
font-weight: bold;
}
.price-formula .compute-part { background-color: rgba(13, 110, 253, 0.2); color: #0d6efd; }
.price-formula .sla-base-part { background-color: rgba(111, 66, 193, 0.2); color: #6f42c1; }
.price-formula .sla-units-part { background-color: rgba(253, 126, 20, 0.2); color: #fd7e14; }
.price-formula .addons-part { background-color: rgba(220, 53, 69, 0.2); color: #dc3545; }
.price-formula .equals-part { background-color: rgba(25, 135, 84, 0.2); color: #198754; }
</style> </style>
{% endblock %} {% endblock %}
@ -60,6 +124,61 @@
<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>
<!-- Pricing Model Explanation -->
<div class="card mb-4">
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#pricingExplanation" aria-expanded="false" aria-controls="pricingExplanation" style="cursor: pointer;">
<h5 class="mb-0">
<i class="bi bi-info-circle me-2"></i>How Our Pricing Works
<small class="text-muted ms-2">(Click to expand)</small>
</h5>
</div>
<div class="collapse" id="pricingExplanation">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Price Components</h6>
<ul class="list-unstyled">
<li class="mb-2">
<span class="badge" style="background-color: #0d6efd;">Compute Plan Price</span>
<span class="ms-2">Base infrastructure cost for CPU, memory, and storage</span>
</li>
<li class="mb-2">
<span class="badge" style="background-color: #6f42c1;">SLA Base</span>
<span class="ms-2">Fixed cost for the service level agreement</span>
</li>
<li class="mb-2">
<span class="badge" style="background-color: #fd7e14;">Units × SLA Per Unit</span>
<span class="ms-2">Variable cost based on scale/usage</span>
</li>
<li class="mb-2">
<span class="badge" style="background-color: #dc3545;">Mandatory Add-ons</span>
<span class="ms-2">Required additional services (backup, monitoring, etc.)</span>
</li>
</ul>
</div>
<div class="col-md-6">
<h6>Final Price Formula</h6>
<div class="bg-light p-3 rounded">
<code>
<span style="color: #0d6efd;">Compute Plan Price</span> +
<span style="color: #6f42c1;">SLA Base</span> +
<span style="color: #fd7e14;">(Units × SLA Per Unit)</span> +
<span style="color: #dc3545;">Mandatory Add-ons</span> =
<strong style="color: #198754;">Final Price</strong>
</code>
</div>
<p class="mt-3 mb-0">
<small class="text-muted">
This transparent pricing model ensures you understand exactly what you're paying for.
The table below breaks down each component for every service variant we offer.
</small>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Filter Form --> <!-- Filter Form -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
@ -278,32 +397,49 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- Price Calculation Formula Helper -->
<div class="price-formula">
<strong>Final Price Calculation:</strong><br>
<span class="formula-part compute-part">Compute Plan Price</span>
<span class="math-operator">+</span>
<span class="formula-part sla-base-part">SLA Base</span>
<span class="math-operator">+</span>
<span class="formula-part sla-units-part">Units × SLA Per Unit</span>
<span class="math-operator">+</span>
<span class="formula-part addons-part">Mandatory Add-ons</span>
<span class="math-operator">=</span>
<span class="formula-part equals-part">Final Price</span>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-bordered table-sm pricing-table"> <table class="table table-striped table-bordered table-sm pricing-table">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>Compute Plan</th> <th rowspan="2">Compute Plan</th>
<th>Cloud Provider</th> <th rowspan="2">Cloud Provider</th>
<th>vCPUs</th> <th rowspan="2">vCPUs</th>
<th>RAM (GB)</th> <th rowspan="2">RAM (GB)</th>
<th>Term</th> <th rowspan="2">Term</th>
<th>Currency</th> <th rowspan="2">Currency</th>
<th>Compute Plan Price</th> <th colspan="5" class="text-center" style="background-color: #198754 !important;">Price Calculation Breakdown</th>
<th>Units</th>
<th>SLA Base</th>
<th>SLA Per Unit</th>
<th>SLA Price</th>
{% if show_addon_details %} {% if show_addon_details %}
<th>Add-ons</th> <th rowspan="2">Add-ons</th>
{% endif %} {% endif %}
{% if show_discount_details %} {% if show_discount_details %}
<th>Discount Model</th> <th rowspan="2">Discount Model</th>
<th>Discount Details</th> <th rowspan="2">Discount Details</th>
{% endif %} {% endif %}
{% if show_price_comparison %} {% if show_price_comparison %}
<th>External Comparisons</th> <th rowspan="2">External Comparisons</th>
{% endif %} {% endif %}
<th class="final-price-header">Final Price</th> <th rowspan="2" class="final-price-header">Final Price</th>
</tr>
<tr>
<th style="background-color: #0d6efd; color: white;">Compute Plan Price</th>
<th style="background-color: #6f42c1; color: white;">SLA Base</th>
<th style="background-color: #fd7e14; color: white;">Units × SLA Per Unit</th>
<th style="background-color: #dc3545; color: white;">Mandatory Add-ons</th>
<th style="background-color: #198754; color: white;">= Total SLA Price</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -315,11 +451,44 @@
<td>{{ row.ram }}</td> <td>{{ row.ram }}</td>
<td>{{ row.term }}</td> <td>{{ row.term }}</td>
<td>{{ row.currency }}</td> <td>{{ row.currency }}</td>
<td>{{ row.compute_plan_price|floatformat:2 }}</td> <!-- Price Calculation Breakdown -->
<td>{{ row.units }}</td> <td class="text-center" style="background-color: rgba(13, 110, 253, 0.1);">
<td>{{ row.sla_base|floatformat:2 }}</td> <span class="fw-bold">{{ row.compute_plan_price|floatformat:2 }}</span>
<td>{{ row.sla_per_unit|floatformat:4 }}</td> </td>
<td>{{ row.sla_price|floatformat:2 }}</td> <td class="text-center" style="background-color: rgba(111, 66, 193, 0.1);">
<span class="fw-bold">{{ row.sla_base|floatformat:2 }}</span>
</td>
<td class="text-center" style="background-color: rgba(253, 126, 20, 0.1);">
<span class="fw-bold">{{ row.units|floatformat:0 }} × {{ row.sla_per_unit|floatformat:4 }}</span><br>
<small class="text-muted">= {{ row.units|multiply:row.sla_per_unit|floatformat:2 }}</small>
</td>
<td class="text-center" style="background-color: rgba(220, 53, 69, 0.1);">
{% if row.mandatory_addons %}
{% for addon in row.mandatory_addons %}
<div class="mb-1">
{% if addon.addon_type == "Unit Rate" %}
<strong>{{ addon.name }}</strong><br>
<span class="fw-bold">{{ row.units|floatformat:0 }} × {{ addon.price|floatformat:4 }}</span><br>
<small class="text-muted">= {{ row.units|multiply:addon.price|floatformat:2 }}</small>
{% elif addon.addon_type == "Base Fee" %}
<strong>{{ addon.name }}</strong><br>
<span class="fw-bold">{{ addon.price|floatformat:2 }}</span>
{% else %}
<strong>{{ addon.name }}</strong><br>
<span class="fw-bold">{{ addon.price|floatformat:2 }}</span>
{% endif %}
</div>
{% if not forloop.last %}<hr class="my-1">{% endif %}
{% endfor %}
{% else %}
<span class="text-muted">n/a</span>
{% endif %}
</td>
<td class="text-center fw-bold" style="background-color: rgba(25, 135, 84, 0.2);">
{% with addon_total=row.mandatory_addons|calculate_addon_total:row.units %}
{{ row.sla_price|add_float:addon_total|floatformat:2 }}
{% endwith %}
</td>
{% if show_addon_details %} {% if show_addon_details %}
<td> <td>
{% if row.mandatory_addons or row.optional_addons %} {% if row.mandatory_addons or row.optional_addons %}
@ -415,6 +584,7 @@
</td> </td>
<td class="text-muted">{{ row.term }}</td> <td class="text-muted">{{ row.term }}</td>
<td class="text-muted">{{ comparison.currency }}</td> <td class="text-muted">{{ comparison.currency }}</td>
<!-- Price breakdown columns for external comparisons -->
<td class="text-muted">-</td> <td class="text-muted">-</td>
<td class="text-muted">-</td> <td class="text-muted">-</td>
<td class="text-muted">-</td> <td class="text-muted">-</td>
@ -462,7 +632,7 @@
{# Price Chart #} {# Price Chart #}
<div class="price-chart mt-3"> <div class="price-chart mt-3">
<h5 class="text-muted">Price Chart - Units vs Final Price</h5> <h5 class="text-muted">Price Breakdown Chart - Units vs Price Components</h5>
<div style="height: 400px;"> <div style="height: 400px;">
<canvas id="chart-{{ group_name|slugify }}-{{ service_level|slugify }}" width="400" height="200"></canvas> <canvas id="chart-{{ group_name|slugify }}-{{ service_level|slugify }}" width="400" height="200"></canvas>
</div> </div>
@ -517,9 +687,7 @@ document.addEventListener('DOMContentLoaded', function() {
const priceComparisonCheckbox = document.getElementById('price_comparison'); const priceComparisonCheckbox = document.getElementById('price_comparison');
priceComparisonCheckbox.addEventListener('change', function() { priceComparisonCheckbox.addEventListener('change', function() {
filterForm.submit(); filterForm.submit();
}); }); // Chart data for each service level
// Chart data for each service level
{% for group_name, service_levels in pricing_data_by_group_and_service_level.items %} {% for group_name, service_levels in pricing_data_by_group_and_service_level.items %}
{% for service_level, pricing_data in service_levels.items %} {% for service_level, pricing_data in service_levels.items %}
{% if pricing_data %} {% if pricing_data %}
@ -536,18 +704,42 @@ document.addEventListener('DOMContentLoaded', function() {
fill: false fill: false
}, },
{ {
label: 'SLA Price', label: 'Compute Plan Price',
data: [{% for row in pricing_data %}{{ row.sla_price|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}], data: [{% for row in pricing_data %}{{ row.compute_plan_price|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}],
borderColor: 'rgb(255, 99, 132)', borderColor: 'rgb(13, 110, 253)',
backgroundColor: 'rgba(255, 99, 132, 0.2)', backgroundColor: 'rgba(13, 110, 253, 0.2)',
tension: 0.1, tension: 0.1,
fill: false fill: false
}, },
{ {
label: 'Compute Plan Price', label: 'SLA Base',
data: [{% for row in pricing_data %}{{ row.compute_plan_price|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}], data: [{% for row in pricing_data %}{{ row.sla_base|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}],
borderColor: 'rgb(54, 162, 235)', borderColor: 'rgb(111, 66, 193)',
backgroundColor: 'rgba(54, 162, 235, 0.2)', backgroundColor: 'rgba(111, 66, 193, 0.2)',
tension: 0.1,
fill: false
},
{
label: 'Units × SLA Per Unit',
data: [{% for row in pricing_data %}{{ row.units|multiply:row.sla_per_unit|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}],
borderColor: 'rgb(253, 126, 20)',
backgroundColor: 'rgba(253, 126, 20, 0.2)',
tension: 0.1,
fill: false
},
{
label: 'Mandatory Add-ons',
data: [{% for row in pricing_data %}{{ row.mandatory_addons|calculate_addon_total:row.units|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}],
borderColor: 'rgb(220, 53, 69)',
backgroundColor: 'rgba(220, 53, 69, 0.2)',
tension: 0.1,
fill: false
},
{
label: 'Total SLA Price',
data: [{% for row in pricing_data %}{{ row.sla_price|floatformat:2 }}{% if not forloop.last %}, {% endif %}{% endfor %}],
borderColor: 'rgb(25, 135, 84)',
backgroundColor: 'rgba(25, 135, 84, 0.2)',
tension: 0.1, tension: 0.1,
fill: false fill: false
} }
@ -580,7 +772,7 @@ document.addEventListener('DOMContentLoaded', function() {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: '{{ group_name }} - {{ service_level }} Pricing' text: '{{ group_name }} - {{ service_level }} Price Breakdown'
}, },
legend: { legend: {
display: true display: true

View file

@ -0,0 +1,45 @@
from django import template
register = template.Library()
@register.filter(name="multiply")
def multiply(value, arg):
"""Multiply two numbers in Django templates"""
try:
return float(value) * float(arg)
except (ValueError, TypeError):
return 0
@register.filter(name="add_float")
def add_float(value, arg):
"""Add two numbers in Django templates"""
try:
return float(value) + float(arg)
except (ValueError, TypeError):
return 0
@register.filter(name="sum_addon_prices")
def sum_addon_prices(addons):
"""Sum the prices of addons"""
try:
return sum(addon.price for addon in addons)
except (AttributeError, TypeError):
return 0
@register.filter(name="calculate_addon_total")
def calculate_addon_total(addons, units):
"""Calculate total cost of addons based on their type and units"""
try:
total = 0
for addon in addons:
if addon.addon_type == "Unit Rate":
total += float(addon.price) * float(units)
else: # Base Fee or other types
total += float(addon.price)
return total
except (AttributeError, TypeError, ValueError):
return 0