Merge branch 'feature/6561/show-multiple-line-items' into 'master'
Feature/6561/show multiple line items See merge request ungleich-public/dynamicweb!697
This commit is contained in:
commit
3c4494f35c
5 changed files with 169 additions and 70 deletions
|
@ -473,6 +473,51 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model):
|
||||||
('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'),
|
('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def amount_in_chf(self):
|
||||||
|
"""
|
||||||
|
Returns amount in chf. The amount in this model is in cents (as in
|
||||||
|
Stripe). Hence we multiply it by 0.01 to obtain the result
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.amount * 0.01
|
||||||
|
|
||||||
|
def unit_amount_in_chf(self):
|
||||||
|
"""
|
||||||
|
Returns unit amount in chf. If its 0, we obtain it from amount and
|
||||||
|
quantity.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.unit_amount == 0:
|
||||||
|
return round((self.amount / self.quantity) * 0.01, 2)
|
||||||
|
else:
|
||||||
|
return self.unit_amount * 0.01
|
||||||
|
|
||||||
|
def get_item_detail_str(self):
|
||||||
|
"""
|
||||||
|
Returns line item html string representation
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
item_detail = ""
|
||||||
|
if self.metadata is not None and len(self.metadata) > 0:
|
||||||
|
try:
|
||||||
|
vm_dict = json.loads(self.metadata)
|
||||||
|
item_detail = "VM ID: {}<br/>".format(vm_dict["VM_ID"])
|
||||||
|
except ValueError as ve:
|
||||||
|
logger.error(
|
||||||
|
"Could not parse VM in metadata {}. Detail {}".format(
|
||||||
|
self.metadata, str(ve)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vm_conf = StripeUtils.get_vm_config_from_stripe_id(
|
||||||
|
self.stripe_plan.stripe_plan_id
|
||||||
|
)
|
||||||
|
item_detail += ("<b>Cores</b>: {}<br/><b>RAM</b>: {} GB<br/>"
|
||||||
|
"<b>SSD</b>: {} GB<br/>").format(
|
||||||
|
vm_conf['cores'], int(float(vm_conf['ram'])), vm_conf['ssd']
|
||||||
|
)
|
||||||
|
return item_detail
|
||||||
|
|
||||||
class VMDetail(models.Model):
|
class VMDetail(models.Model):
|
||||||
user = models.ForeignKey(CustomUser)
|
user = models.ForeignKey(CustomUser)
|
||||||
|
|
|
@ -113,3 +113,16 @@
|
||||||
.dcl-place-order-text {
|
.dcl-place-order-text {
|
||||||
color: #808080;
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.border_bottom td {
|
||||||
|
border-bottom:1pt solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.grand-total-padding td {
|
||||||
|
padding-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -89,80 +89,91 @@
|
||||||
<div>
|
<div>
|
||||||
<h4>{% trans "Invoice summary" %}</h4>
|
<h4>{% trans "Invoice summary" %}</h4>
|
||||||
{% if vm %}
|
{% if vm %}
|
||||||
<p>
|
{% if line_items %}
|
||||||
<strong>{% trans "Product" %}:</strong>
|
<table>
|
||||||
{% if vm.name %}
|
<tr><th style="width: 35%">Product</th><th style="width: 20%">Period</th><th style="text-align: center; width: 10%">Qty</th><th align="center" style="width: 10%; text-align: center;">Unit Price</th><th style="width: 10%; text-align: right;">Total</th></tr>
|
||||||
{{ vm.name }}
|
{% for line_item in line_items %}
|
||||||
{% endif %}
|
<tr class="border_bottom"><td>{% if line_item.description|length > 0 %}{{line_item.description}}{% else %}{{line_item.get_item_detail_str|safe}}{% endif %}</td><td>{{ line_item.period_start | date:'Y-m-d' }} — {{ line_item.period_end | date:'Y-m-d' }}</td><td align="center">{{line_item.quantity}}</td><td align="center">{{line_item.unit_amount_in_chf}}</td><td align="right">{{line_item.amount_in_chf}}</td></tr>
|
||||||
</p>
|
|
||||||
<div class="row">
|
{% endfor %}
|
||||||
<div class="col-sm-6">
|
<tr class="grand-total-padding"><td colspan="4">Grand Total</td><td align="right">{{total_in_chf}}</td></tr>
|
||||||
{% if period_start %}
|
</table>
|
||||||
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
<span>{% trans "Period" %}: </span>
|
<strong>{% trans "Product" %}:</strong>
|
||||||
<span>
|
{% if vm.name %}
|
||||||
<span class="locale_date"
|
{{ vm.name }}
|
||||||
data-format="YYYY/MM/DD">{{ period_start|date:'Y-m-d h:i a' }}</span> - <span
|
|
||||||
class="locale_date" data-format="YYYY/MM/DD">{{ period_end|date:'Y-m-d h:i a' }}</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>
|
|
||||||
<span>{% trans "Cores" %}: </span>
|
|
||||||
{% if vm.cores %}
|
|
||||||
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
|
|
||||||
{% else %}
|
|
||||||
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div class="row">
|
||||||
<span>{% trans "Memory" %}: </span>
|
<div class="col-sm-6">
|
||||||
<strong class="pull-right">{{vm.memory}} GB</strong>
|
{% if period_start %}
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<span>{% trans "Period" %}: </span>
|
||||||
<span>{% trans "Disk space" %}: </span>
|
<span>
|
||||||
<strong class="pull-right">{{vm.disk_size}} GB</strong>
|
<span class="locale_date"
|
||||||
</p>
|
data-format="YYYY/MM/DD">{{ period_start|date:'Y-m-d h:i a' }}</span> - <span
|
||||||
</div>
|
class="locale_date" data-format="YYYY/MM/DD">{{ period_end|date:'Y-m-d h:i a' }}</span>
|
||||||
<div class="col-sm-12">
|
</span>
|
||||||
<hr class="thin-hr">
|
</p>
|
||||||
</div>
|
{% endif %}
|
||||||
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
<p>
|
||||||
<div class="col-sm-6">
|
<span>{% trans "Cores" %}: </span>
|
||||||
<div class="subtotal-price">
|
{% if vm.cores %}
|
||||||
{% if vm.vat > 0 %}
|
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
|
||||||
<p>
|
{% else %}
|
||||||
<strong>{% trans "Subtotal" %} </strong>
|
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
||||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}}
|
{% endif %}
|
||||||
CHF</strong>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<span>{% trans "Memory" %}: </span>
|
||||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
|
<strong class="pull-right">{{vm.memory}} GB</strong>
|
||||||
</small>
|
</p>
|
||||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
<p>
|
||||||
</p>
|
<span>{% trans "Disk space" %}: </span>
|
||||||
{% endif %}
|
<strong class="pull-right">{{vm.disk_size}} GB</strong>
|
||||||
{% if vm.discount.amount > 0 %}
|
</p>
|
||||||
<p class="text-primary">
|
</div>
|
||||||
{%trans "Discount" as discount_name %}
|
<div class="col-sm-12">
|
||||||
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
<hr class="thin-hr">
|
||||||
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
</div>
|
||||||
</p>
|
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="subtotal-price">
|
||||||
|
{% if vm.vat > 0 %}
|
||||||
|
<p>
|
||||||
|
<strong>{% trans "Subtotal" %} </strong>
|
||||||
|
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}}
|
||||||
|
CHF</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
|
||||||
|
</small>
|
||||||
|
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if vm.discount.amount > 0 %}
|
||||||
|
<p class="text-primary">
|
||||||
|
{%trans "Discount" as discount_name %}
|
||||||
|
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
||||||
|
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<hr class="thin-hr">
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<p class="total-price">
|
||||||
|
<strong>{% trans "Total" %} </strong>
|
||||||
|
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %}
|
||||||
|
CHF</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<hr class="thin-hr">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="col-sm-6">
|
|
||||||
<p class="total-price">
|
|
||||||
<strong>{% trans "Total" %} </strong>
|
|
||||||
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %}
|
|
||||||
CHF</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
<strong>{% trans "Product" %}:</strong>
|
<strong>{% trans "Product" %}:</strong>
|
||||||
|
|
|
@ -1284,8 +1284,8 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
|
||||||
# fallback to get it from the infrastructure
|
# fallback to get it from the infrastructure
|
||||||
try:
|
try:
|
||||||
manager = OpenNebulaManager(
|
manager = OpenNebulaManager(
|
||||||
email=self.request.email,
|
email=self.request.user.email,
|
||||||
password=self.request.password
|
password=self.request.user.password
|
||||||
)
|
)
|
||||||
vm = manager.get_vm(vm_id)
|
vm = manager.get_vm(vm_id)
|
||||||
context['vm'] = VirtualMachineSerializer(vm).data
|
context['vm'] = VirtualMachineSerializer(vm).data
|
||||||
|
@ -1322,6 +1322,9 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
|
||||||
context['total_in_chf'] = obj.total_in_chf()
|
context['total_in_chf'] = obj.total_in_chf()
|
||||||
context['invoice_number'] = obj.invoice_number
|
context['invoice_number'] = obj.invoice_number
|
||||||
context['discount_on_stripe'] = obj.discount_in_chf()
|
context['discount_on_stripe'] = obj.discount_in_chf()
|
||||||
|
if obj.lines_data_count > 1:
|
||||||
|
# special case, we pass the details of each of the line items
|
||||||
|
context['line_items'] = obj.hostingbilllineitem_set.all()
|
||||||
return context
|
return context
|
||||||
else:
|
else:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import stripe
|
import stripe
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from datacenterlight.models import StripePlan
|
from datacenterlight.models import StripePlan
|
||||||
|
@ -376,6 +377,32 @@ class StripeUtils(object):
|
||||||
else:
|
else:
|
||||||
return stripe_plan_id_string
|
return stripe_plan_id_string
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_vm_config_from_stripe_id(stripe_id):
|
||||||
|
"""
|
||||||
|
Given a string like "dcl-v1-cpu-2-ram-5gb-ssd-10gb" return different
|
||||||
|
configuration params as a dict
|
||||||
|
|
||||||
|
:param stripe_id|str
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r'^dcl-v(\d+)-cpu-(\d+)-ram-(\d+\.?\d*)gb-ssd-(\d+)gb-?(\d*\.?\d*)(chf)?$')
|
||||||
|
match_res = pattern.match(stripe_id)
|
||||||
|
if match_res is not None:
|
||||||
|
price = None
|
||||||
|
try:
|
||||||
|
price=match_res.group(5)
|
||||||
|
except IndexError as ie:
|
||||||
|
logger.debug("Did not find price in {}".format(stripe_id))
|
||||||
|
return {
|
||||||
|
'version': match_res.group(1),
|
||||||
|
'cores': match_res.group(2),
|
||||||
|
'ram': match_res.group(3),
|
||||||
|
'ssd': match_res.group(4),
|
||||||
|
'price': price
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_stripe_plan_name(cpu, memory, disk_size, price):
|
def get_stripe_plan_name(cpu, memory, disk_size, price):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue