
diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html
new file mode 100644
index 00000000..e63f25a7
--- /dev/null
+++ b/hosting/templates/hosting/invoice_detail.html
@@ -0,0 +1,224 @@
+{% extends "hosting/base_short.html" %}
+{% load staticfiles bootstrap3 humanize i18n custom_tags %}
+
+
+{% block content %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% if not error %}
+
+
+
+ {% blocktrans with page_header_text=page_header_text|default:"Invoice" %}{{page_header_text}}{% endblocktrans %}
+
+ {% if invoice %}
+
+
+
+
+ {% endif %}
+
+
+ {% if invoice %}
+
+ {% trans "Invoice #" %} {{invoice.invoice_number}}
+
+ {% endif %}
+
+ {% trans "Date" %}:
+
+ {% if invoice %}
+ {{invoice.paid_at|date:'Y-m-d h:i a'}}
+ {% else %}
+ {% now "Y-m-d h:i a" %}
+ {% endif %}
+
+
+ {% if invoice and vm %}
+
+ {% trans "Status" %}:
+
+ {% if vm.terminated_at %}
+ {% trans "Terminated" %}
+ {% elif invoice.order.status == 'Approved' %}
+ {% trans "Approved" %}
+ {% else %}
+ {% trans "Declined" %}
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+ {% trans "Billed to" %}:
+
+ {% if invoice.order %}
+ {{invoice.customer.user.name}}
+ {{invoice.order.billing_address.street_address}},
+ {{invoice.order.billing_address.postal_code}}
+ {{invoice.order.billing_address.city}},
+ {{invoice.order.billing_address.country}}
+ {% endif %}
+
+
+
+
+
+
{% trans "Payment method" %}:
+
+ {% if invoice.order %}
+ {{invoice.order.cc_brand}} {% trans "ending in" %} ****
+ {{invoice.order.last4}}
+ {{invoice.customer.user.email}}
+ {% endif %}
+
+
+
+
+
{% trans "Invoice summary" %}
+ {% if vm %}
+
+ {% trans "Product" %}:
+ {% if vm.name %}
+ {{ vm.name }}
+ {% endif %}
+
+
+
+ {% if period_start %}
+
+ {% trans "Period" %}:
+
+ {{ period_start|date:'Y-m-d h:i a' }} - {{ period_end|date:'Y-m-d h:i a' }}
+
+
+ {% endif %}
+
+ {% trans "Cores" %}:
+ {% if vm.cores %}
+ {{vm.cores|floatformat}}
+ {% else %}
+ {{vm.cpu|floatformat}}
+ {% endif %}
+
+
+ {% trans "Memory" %}:
+ {{vm.memory}} GB
+
+
+ {% trans "Disk space" %}:
+ {{vm.disk_size}} GB
+
+
+
+
+
+ {% if vm.vat > 0 or vm.discount.amount > 0 %}
+
+
+ {% if vm.vat > 0 %}
+
+ {% trans "Subtotal" %}
+ {{vm.price|floatformat:2|intcomma}}
+ CHF
+
+
+ {% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
+
+ {{vm.vat|floatformat:2|intcomma}} CHF
+
+ {% endif %}
+ {% if vm.discount.amount > 0 %}
+
+ {%trans "Discount" as discount_name %}
+ {{ vm.discount.name|default:discount_name }}
+ - {{ vm.discount.amount }} CHF
+
+ {% endif %}
+
+
+
+
+
+ {% endif %}
+
+
+ {% trans "Total" %}
+ {% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %}
+ CHF
+
+
+
+ {% else %}
+
+ {% trans "Product" %}:
+ {{ product_name }}
+
+
+
+
+ {% trans "Amount" %}:
+ {{total_in_chf|floatformat:2|intcomma}}
+ CHF
+
+ {% if invoice.order.generic_payment_description %}
+
+ {% trans "Description" %}:
+ {{invoice.order.generic_payment_description}}
+
+ {% endif %}
+ {% if invoice.order.subscription_id %}
+
+ {% trans "Recurring" %}:
+ {{invoice.order.created_at|date:'d'|ordinal}}
+ {% trans "of every month" %}
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
+{%endblock%}
+
+{% block js_extra %}
+{% if invoice.order %}
+
+
+
+
+{% endif %}
+{% endblock js_extra %}
diff --git a/hosting/templates/hosting/invoices.html b/hosting/templates/hosting/invoices.html
new file mode 100644
index 00000000..f2486111
--- /dev/null
+++ b/hosting/templates/hosting/invoices.html
@@ -0,0 +1,61 @@
+{% extends "hosting/base_short.html" %}
+{% load staticfiles bootstrap3 humanize i18n custom_tags %}
+
+{% block content %}
+
+
+
{% trans "My Bills" %}
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+
+ {% trans "VM ID" %} |
+ {% trans "IP Address" %} |
+ {% trans "Period" %} |
+ {% trans "Amount" %} |
+ |
+
+
+
+ {% for invoice in invoices %}
+
+ {{ invoice.order.vm_id }} |
+ {{ ips|get_value_from_dict:invoice.invoice_number|join:" " }} |
+ {% with line_items|get_value_from_dict:invoice.invoice_number as line_items_to_show %}
+ {{ line_items_to_show.0.period_start | date:'Y-m-d' }} — {{ line_items_to_show.0.period_end | date:'Y-m-d' }} |
+ {% endwith %}
+ {{ invoice.total_in_chf|floatformat:2|intcomma }} |
+
+ {% trans 'See Invoice' %}
+ |
+
+ {% endfor %}
+
+
+
+ {% if is_paginated %}
+
+ {% endif %}
+
+{% endblock %}
diff --git a/hosting/urls.py b/hosting/urls.py
index 32ef8400..a3579f06 100644
--- a/hosting/urls.py
+++ b/hosting/urls.py
@@ -8,7 +8,8 @@ from .views import (
MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView,
HostingPricingView, CreateVirtualMachinesView, HostingBillListView,
HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView,
- SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView
+ SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView,
+ InvoiceListView, InvoiceDetailView
)
@@ -22,10 +23,13 @@ urlpatterns = [
url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
url(r'settings/?$', SettingsView.as_view(), name='settings'),
url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
+ url(r'invoices/?$', InvoiceListView.as_view(), name='invoices'),
url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(),
name='order-confirmation'),
url(r'orders/(?P
\d+)/?$', OrdersHostingDetailView.as_view(),
name='orders'),
+ url(r'invoice/(?P[-\w]+)/?$', InvoiceDetailView.as_view(),
+ name='invoices'),
url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
url(r'bills/(?P\d+)/?$', HostingBillDetailView.as_view(),
name='bills'),
diff --git a/hosting/views.py b/hosting/views.py
index 32de4e54..fe13ff21 100644
--- a/hosting/views.py
+++ b/hosting/views.py
@@ -61,7 +61,7 @@ from .forms import (
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
- GenericProduct
+ GenericProduct, MonthlyHostingBill, HostingBillLineItem
)
logger = logging.getLogger(__name__)
@@ -83,6 +83,19 @@ class DashboardView(LoginRequiredMixin, View):
@method_decorator(decorators)
def get(self, request, *args, **kwargs):
context = self.get_context_data()
+ context['has_invoices'] = False
+ try:
+ bills = []
+ if hasattr(self.request.user, 'stripecustomer'):
+ bills = MonthlyHostingBill.objects.filter(
+ customer=self.request.user.stripecustomer
+ )
+ if len(bills) > 0:
+ context['has_invoices'] = True
+ except MonthlyHostingBill.DoesNotExist as dne:
+ logger.error("{}'s monthly hosting bill not imported ?".format(
+ self.request.user.email
+ ))
return render(request, self.template_name, context)
@@ -1146,6 +1159,180 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
return super(OrdersHostingListView, self).get(request, *args, **kwargs)
+class InvoiceListView(LoginRequiredMixin, ListView):
+ template_name = "hosting/invoices.html"
+ login_url = reverse_lazy('hosting:login')
+ context_object_name = "invoices"
+ paginate_by = 10
+ ordering = '-created'
+
+ def get_context_data(self, **kwargs):
+ context = super(InvoiceListView, self).get_context_data(**kwargs)
+ if ('user_email' in self.request.GET
+ and self.request.user.email == settings.ADMIN_EMAIL):
+ user_email = self.request.GET['user_email']
+ logger.debug(
+ "user_email = {}".format(user_email)
+ )
+ try:
+ cu = CustomUser.objects.get(email=user_email)
+ except CustomUser.DoesNotExist as dne:
+ logger.debug("User does not exist")
+ cu = self.request.user
+ mhbs = MonthlyHostingBill.objects.filter(customer__user=cu)
+ else:
+ mhbs = MonthlyHostingBill.objects.filter(
+ customer__user=self.request.user
+ )
+ ips_dict = {}
+ line_items_dict = {}
+ for mhb in mhbs:
+ try:
+ vm_detail = VMDetail.objects.get(vm_id=mhb.order.vm_id)
+ ips_dict[mhb.invoice_number] = [vm_detail.ipv6, vm_detail.ipv4]
+ line_items_dict[mhb.invoice_number] = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb)
+ except VMDetail.DoesNotExist as dne:
+ ips_dict[mhb.invoice_number] = ['--']
+ logger.debug("VMDetail for {} doesn't exist".format(
+ mhb.order.vm_id
+ ))
+ context['line_items'] = line_items_dict
+ context['ips'] = ips_dict
+ return context
+
+ def get_queryset(self):
+ user = self.request.user
+ if ('user_email' in self.request.GET
+ and self.request.user.email == settings.ADMIN_EMAIL):
+ user_email = self.request.GET['user_email']
+ logger.debug(
+ "user_email = {}".format(user_email)
+ )
+ try:
+ cu = CustomUser.objects.get(email=user_email)
+ except CustomUser.DoesNotExist as dne:
+ logger.debug("User does not exist")
+ cu = self.request.user
+ self.queryset = MonthlyHostingBill.objects.filter(customer__user=cu)
+ else:
+ self.queryset = MonthlyHostingBill.objects.filter(
+ customer__user=self.request.user
+ )
+ return super(InvoiceListView, self).get_queryset()
+
+ @method_decorator(decorators)
+ def get(self, request, *args, **kwargs):
+ return super(InvoiceListView, self).get(request, *args, **kwargs)
+
+
+class InvoiceDetailView(LoginRequiredMixin, DetailView):
+ template_name = "hosting/invoice_detail.html"
+ context_object_name = "invoice"
+ login_url = reverse_lazy('hosting:login')
+ permission_required = ['view_monthlyhostingbill']
+ # model = MonthlyHostingBill
+
+ def get_object(self, queryset=None):
+ invoice_id = self.kwargs.get('invoice_id')
+ try:
+ invoice_obj = MonthlyHostingBill.objects.get(
+ invoice_number=invoice_id
+ )
+ logger.debug("Found MHB for id {invoice_id}".format(
+ invoice_id=invoice_id
+ ))
+ if self.request.user.has_perm(
+ self.permission_required[0], invoice_obj
+ ) or self.request.user.email == settings.ADMIN_EMAIL:
+ logger.debug("User has permission to invoice_obj")
+ else:
+ logger.error("User does not have permission to access")
+ invoice_obj = None
+ except MonthlyHostingBill.DoesNotExist as dne:
+ logger.debug("MHB not found for id {invoice_id}".format(
+ invoice_id=invoice_id
+ ))
+ invoice_obj = None
+ return invoice_obj
+
+ def get_context_data(self, **kwargs):
+ # Get context
+ context = super(InvoiceDetailView, self).get_context_data(**kwargs)
+ obj = self.get_object()
+
+ if obj is not None:
+ vm_id = obj.get_vm_id()
+ try:
+ # Try to get vm details from database
+ vm_detail = VMDetail.objects.get(vm_id=vm_id)
+ context['vm'] = vm_detail.__dict__
+ context['vm']['name'] = '{}-{}'.format(
+ context['vm']['configuration'], context['vm']['vm_id'])
+ price, vat, vat_percent, discount = get_vm_price_with_vat(
+ cpu=context['vm']['cores'],
+ ssd_size=context['vm']['disk_size'],
+ memory=context['vm']['memory'],
+ pricing_name=(obj.order.vm_pricing.name
+ if obj.order.vm_pricing else 'default')
+ )
+ context['vm']['vat'] = vat
+ context['vm']['price'] = price
+ context['vm']['discount'] = discount
+ context['vm']['vat_percent'] = vat_percent
+ context['vm']['total_price'] = price + vat - discount['amount']
+ except VMDetail.DoesNotExist:
+ # fallback to get it from the infrastructure
+ try:
+ manager = OpenNebulaManager(
+ email=self.request.email,
+ password=self.request.password
+ )
+ vm = manager.get_vm(vm_id)
+ context['vm'] = VirtualMachineSerializer(vm).data
+ price, vat, vat_percent, discount = get_vm_price_with_vat(
+ cpu=context['vm']['cores'],
+ ssd_size=context['vm']['disk_size'],
+ memory=context['vm']['memory'],
+ pricing_name=(obj.order.vm_pricing.name
+ if obj.order.vm_pricing else 'default')
+ )
+ context['vm']['vat'] = vat
+ context['vm']['price'] = price
+ context['vm']['discount'] = discount
+ context['vm']['vat_percent'] = vat_percent
+ context['vm']['total_price'] = (
+ price + vat - discount['amount']
+ )
+ except WrongIdError:
+ logger.error("WrongIdError while accessing "
+ "invoice {}".format(obj.invoice_id))
+ messages.error(
+ self.request,
+ _('The VM you are looking for is unavailable at the '
+ 'moment. Please contact Data Center Light support.')
+ )
+ self.kwargs['error'] = 'WrongIdError'
+ context['error'] = 'WrongIdError'
+ return context
+
+ # add context params from monthly hosting bill
+ context['period_start'] = obj.get_period_start()
+ context['period_end'] = obj.get_period_end()
+ context['paid_at'] = obj.paid_at
+ context['total_in_chf'] = obj.total_in_chf()
+ context['invoice_number'] = obj.invoice_number
+ context['discount_on_stripe'] = obj.discount_in_chf()
+ return context
+ else:
+ raise Http404
+
+ @method_decorator(decorators)
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ context = self.get_context_data(object=self.get_object())
+ return self.render_to_response(context)
+
+
class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView):
login_url = reverse_lazy('hosting:login')
success_url = reverse_lazy('hosting:orders')
diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py
index a3224a0e..4b4a157e 100644
--- a/utils/stripe_utils.py
+++ b/utils/stripe_utils.py
@@ -122,6 +122,49 @@ class StripeUtils(object):
}
return card_details
+ @handleStripeError
+ def get_all_invoices(self, customer_id, created_gt):
+ return_list = []
+ has_more_invoices = True
+ starting_after = False
+ while has_more_invoices:
+ if starting_after:
+ invoices = stripe.Invoice.list(
+ limit=10, customer=customer_id, created={'gt': created_gt},
+ starting_after=starting_after
+ )
+ else:
+ invoices = stripe.Invoice.list(
+ limit=10, customer=customer_id, created={'gt': created_gt}
+ )
+ has_more_invoices = invoices.has_more
+ for invoice in invoices.data:
+ invoice_details = {
+ 'created': invoice.created,
+ 'receipt_number': invoice.receipt_number,
+ 'invoice_number': invoice.number,
+ 'paid_at': invoice.status_transitions.paid_at if invoice.paid else 0,
+ 'period_start': invoice.period_start,
+ 'period_end': invoice.period_end,
+ 'billing_reason': invoice.billing_reason,
+ 'discount': invoice.discount.coupon.amount_off if invoice.discount else 0,
+ 'total': invoice.total,
+ # to see how many line items we have in this invoice and
+ # then later check if we have more than 1
+ 'lines_data_count': len(invoice.lines.data) if invoice.lines.data is not None else 0,
+ 'invoice_id': invoice.id,
+ 'lines_meta_data_csv': ','.join(
+ [line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' for line in invoice.lines.data]
+ ),
+ 'subscription_ids_csv': ','.join(
+ [line.id if line.type == 'subscription' else '' for line in invoice.lines.data]
+ ),
+ 'line_items': invoice.lines.data
+ }
+ starting_after = invoice.id
+ return_list.append(invoice_details)
+ return return_list
+
@handleStripeError
def get_cards_details_from_token(self, token):
stripe_token = stripe.Token.retrieve(token)