diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 324de04d..a770018f 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -648,6 +648,7 @@ CELERY_RESULT_SERIALIZER = 'json' CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO') +ADMIN_EMAIL = env('ADMIN_EMAIL') DCL_ERROR_EMAILS_TO_LIST = [] if DCL_ERROR_EMAILS_TO is not None: diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py new file mode 100644 index 00000000..09cb3295 --- /dev/null +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -0,0 +1,52 @@ +import logging + +from django.core.management.base import BaseCommand + +from hosting.models import MonthlyHostingBill +from membership.models import CustomUser +from utils.stripe_utils import StripeUtils + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = '''Fetches invoices from Stripe and creates bills for a given + customer in the MonthlyHostingBill model''' + + def add_arguments(self, parser): + parser.add_argument('customer_email', nargs='+', type=str) + + def handle(self, *args, **options): + try: + for email in options['customer_email']: + stripe_utils = StripeUtils() + user = CustomUser.objects.get(email=email) + if hasattr(user, 'stripecustomer'): + self.stdout.write(self.style.SUCCESS( + 'Found %s. Fetching bills for him.' % email)) + mhb = MonthlyHostingBill.objects.filter( + customer=user.stripecustomer).last() + created_gt = {} + if mhb is not None: + # fetch only invoices which is created after + # mhb.created, because we already have invoices till + # this date + created_gt = int(mhb.created.timestamp()) + + all_invoices_response = stripe_utils.get_all_invoices( + user.stripecustomer.stripe_id, + created_gt=created_gt + ) + if all_invoices_response['error'] is not None: + self.stdout.write(self.style.ERROR(all_invoices_response['error'])) + exit(1) + all_invoices = all_invoices_response['response_object'] + self.stdout.write(self.style.SUCCESS("Obtained {} invoices".format(len(all_invoices) if all_invoices is not None else 0))) + for invoice in all_invoices: + invoice['customer'] = user.stripecustomer + MonthlyHostingBill.create(invoice) + else: + self.stdout.write(self.style.SUCCESS( + 'Customer email %s does not have a stripe customer.' % email)) + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/hosting/migrations/0050_monthlyhostingbill.py b/hosting/migrations/0050_monthlyhostingbill.py new file mode 100644 index 00000000..34d29e68 --- /dev/null +++ b/hosting/migrations/0050_monthlyhostingbill.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2019-04-03 03:47 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import utils.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0007_auto_20180213_0128'), + ('hosting', '0049_auto_20181005_0736'), + ] + + operations = [ + migrations.CreateModel( + name='MonthlyHostingBill', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(help_text='When the invoice was created')), + ('receipt_number', models.CharField(help_text='The receipt number that is generated on Stripe', max_length=100)), + ('invoice_number', models.CharField(help_text='The invoice number that is generated on Stripe', max_length=100)), + ('paid_at', models.DateTimeField(help_text='Date on which the bill was paid')), + ('period_start', models.DateTimeField()), + ('period_end', models.DateTimeField()), + ('billing_reason', models.CharField(max_length=25)), + ('discount', models.PositiveIntegerField()), + ('total', models.IntegerField()), + ('lines_data_count', models.IntegerField()), + ('invoice_id', models.CharField(max_length=100, unique=True)), + ('lines_meta_data_csv', models.TextField()), + ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.StripeCustomer')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosting.HostingOrder')), + ], + options={ + 'permissions': (('view_monthlyhostingbill', 'View Monthly Hosting'),), + }, + bases=(utils.mixins.AssignPermissionsMixin, models.Model), + ), + ] diff --git a/hosting/migrations/0051_auto_20190403_0703.py b/hosting/migrations/0051_auto_20190403_0703.py new file mode 100644 index 00000000..d1f87e53 --- /dev/null +++ b/hosting/migrations/0051_auto_20190403_0703.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2019-04-03 07:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0050_monthlyhostingbill'), + ] + + operations = [ + migrations.AddField( + model_name='monthlyhostingbill', + name='subscription_ids_csv', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='monthlyhostingbill', + name='lines_meta_data_csv', + field=models.TextField(default=''), + ), + ] diff --git a/hosting/migrations/0052_hostingbilllineitem.py b/hosting/migrations/0052_hostingbilllineitem.py new file mode 100644 index 00000000..6f1a3db4 --- /dev/null +++ b/hosting/migrations/0052_hostingbilllineitem.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2019-04-13 11:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import utils.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('hosting', '0051_auto_20190403_0703'), + ] + + operations = [ + migrations.CreateModel( + name='HostingBillLineItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.PositiveSmallIntegerField()), + ('description', models.CharField(max_length=255)), + ('discountable', models.BooleanField()), + ('metadata', models.CharField(max_length=128)), + ('period_start', models.DateTimeField()), + ('period_end', models.DateTimeField()), + ('proration', models.BooleanField()), + ('quantity', models.PositiveIntegerField()), + ('unit_amount', models.PositiveIntegerField()), + ('monthly_hosting_bill', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hosting.MonthlyHostingBill')), + ], + options={ + 'permissions': (('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'),), + }, + bases=(utils.mixins.AssignPermissionsMixin, models.Model), + ), + ] diff --git a/hosting/models.py b/hosting/models.py index 707b072d..550cf27f 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,8 +1,11 @@ +import json import logging import os +import pytz from Crypto.PublicKey import RSA from dateutil.relativedelta import relativedelta +from datetime import datetime from django.db import models from django.utils import timezone from django.utils.functional import cached_property @@ -232,6 +235,207 @@ class HostingBill(AssignPermissionsMixin, models.Model): return instance +class MonthlyHostingBill(AssignPermissionsMixin, models.Model): + """ + Corresponds to Invoice object of Stripe + """ + customer = models.ForeignKey(StripeCustomer) + order = models.ForeignKey(HostingOrder) + created = models.DateTimeField(help_text="When the invoice was created") + receipt_number = models.CharField( + help_text="The receipt number that is generated on Stripe", + max_length=100 + ) + invoice_number = models.CharField( + help_text="The invoice number that is generated on Stripe", + max_length=100 + ) + paid_at = models.DateTimeField(help_text="Date on which the bill was paid") + period_start = models.DateTimeField() + period_end = models.DateTimeField() + billing_reason = models.CharField(max_length=25) + discount = models.PositiveIntegerField() + total = models.IntegerField() + lines_data_count = models.IntegerField() + invoice_id = models.CharField(unique=True, max_length=100) + lines_meta_data_csv = models.TextField(default="") + subscription_ids_csv = models.TextField(default="") + + permissions = ('view_monthlyhostingbill',) + + class Meta: + permissions = ( + ('view_monthlyhostingbill', 'View Monthly Hosting'), + ) + + @classmethod + def create(cls, args): + # Try to infer the HostingOrder from subscription id or VM_ID + if len(args['subscription_ids_csv']) > 0: + sub_ids = [sub_id.strip() for sub_id in args['subscription_ids_csv'].split(",")] + if len(sub_ids) == 1: + args['order'] = HostingOrder.objects.get( + subscription_id=sub_ids[0] + ) + else: + logger.debug( + "More than one subscriptions" + "for MonthlyHostingBill {}".format(args['invoice_id']) + ) + logger.debug("SUB_IDS=".format(','.join(sub_ids))) + logger.debug("Not importing invoices") + return + elif len(args['lines_meta_data_csv']) > 0: + vm_ids = [vm_id.strip() for vm_id in args['lines_meta_data_csv'].split(",")] + if len(vm_ids) == 1: + args['order'] = HostingOrder.objects.get(vm_id=vm_ids[0]) + else: + logger.debug( + "More than one VM_ID" + "for MonthlyHostingBill {}".format(args['invoice_id']) + ) + logger.debug("VM_IDS=".format(','.join(vm_ids))) + logger.debug("Not importing invoices") + return + else: + logger.debug("Neither subscription id nor vm_id available") + logger.debug("Can't import invoice") + return + + instance = cls.objects.create( + created=datetime.utcfromtimestamp( + args['created']).replace(tzinfo=pytz.utc), + receipt_number=( + args['receipt_number'] + if args['receipt_number'] is not None else '' + ), + invoice_number=( + args['invoice_number'] + if args['invoice_number'] is not None else '' + ), + paid_at=datetime.utcfromtimestamp( + args['paid_at']).replace(tzinfo=pytz.utc), + period_start=datetime.utcfromtimestamp( + args['period_start']).replace(tzinfo=pytz.utc), + period_end=datetime.utcfromtimestamp( + args['period_end']).replace(tzinfo=pytz.utc), + billing_reason=args['billing_reason'], + discount=args['discount'], + total=args['total'], + lines_data_count=args['lines_data_count'], + invoice_id=args['invoice_id'], + lines_meta_data_csv=args['lines_meta_data_csv'], + customer=args['customer'], + order=args['order'], + subscription_ids_csv=args['subscription_ids_csv'], + ) + + if 'line_items' in args: + line_items = args['line_items'] + for item in line_items: + line_item_instance = HostingBillLineItem.objects.create( + monthly_hosting_bill=instance, + amount=item.amount, + # description seems to be set to null in the Stripe + # response for an invoice + description="" if item.description is None else item.description, + discountable=item.discountable, + metadata=json.dumps(item.metadata), + period_start=datetime.utcfromtimestamp(item.period.start).replace(tzinfo=pytz.utc), period_end=datetime.utcfromtimestamp(item.period.end).replace(tzinfo=pytz.utc), + proration=item.proration, + quantity=item.quantity, + # Strange that line item does not have unit_amount but api + # states that it is present + # https://stripe.com/docs/api/invoiceitems/object#invoiceitem_object-unit_amount + # So, for the time being I set the unit_amount to 0 if not + # found in the line item + unit_amount=item.unit_amount if hasattr(item, "unit_amount") else 0 + ) + line_item_instance.assign_permissions(instance.customer.user) + instance.assign_permissions(instance.customer.user) + return instance + + def total_in_chf(self): + """ + Returns amount in chf. The total amount in this model is in cents. + Hence we multiply it by 0.01 to obtain the result + + :return: + """ + return self.total * 0.01 + + def discount_in_chf(self): + """ + Returns discount in chf. + + :return: + """ + return self.discount * 0.01 + + def get_vm_id(self): + """ + Returns the VM_ID metadata if set in this MHB else returns None + :return: + """ + return_value = None + if len(self.lines_meta_data_csv) > 0: + vm_ids = [vm_id.strip() for vm_id in + self.lines_meta_data_csv.split(",")] + if len(vm_ids) == 1: + return vm_ids[0] + else: + logger.debug( + "More than one VM_ID" + "for MonthlyHostingBill {}".format(self.invoice_id) + ) + logger.debug("VM_IDS=".format(','.join(vm_ids))) + return return_value + + def get_period_start(self): + """ + Return the period start of the invoice for the line items + :return: + """ + items = HostingBillLineItem.objects.filter(monthly_hosting_bill=self) + if len(items) > 0: + return items[0].period_start + else: + return self.period_start + + def get_period_end(self): + """ + Return the period end of the invoice for the line items + :return: + """ + items = HostingBillLineItem.objects.filter(monthly_hosting_bill=self) + if len(items) > 0: + return items[0].period_end + else: + return self.period_end + + +class HostingBillLineItem(AssignPermissionsMixin, models.Model): + """ + Corresponds to InvoiceItem object of Stripe + """ + monthly_hosting_bill = models.ForeignKey(MonthlyHostingBill) + amount = models.PositiveSmallIntegerField() + description = models.CharField(max_length=255) + discountable = models.BooleanField() + metadata = models.CharField(max_length=128) + period_start = models.DateTimeField() + period_end = models.DateTimeField() + proration = models.BooleanField() + quantity = models.PositiveIntegerField() + unit_amount = models.PositiveIntegerField() + permissions = ('view_hostingbilllineitem',) + + class Meta: + permissions = ( + ('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'), + ) + + class VMDetail(models.Model): user = models.ForeignKey(CustomUser) vm_id = models.IntegerField(default=0) diff --git a/hosting/templates/hosting/dashboard.html b/hosting/templates/hosting/dashboard.html index d12f75ee..35ee9b6e 100644 --- a/hosting/templates/hosting/dashboard.html +++ b/hosting/templates/hosting/dashboard.html @@ -26,7 +26,7 @@ - +

{% trans "My Bills" %}

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 %} +
+ +
+ {% trans "BACK TO LIST" %} +
+ + +{%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 %} +
+
+ + + + + + + + + + + + + {% for invoice in invoices %} + + + + {% with line_items|get_value_from_dict:invoice.invoice_number as line_items_to_show %} + + {% endwith %} + + + + {% endfor %} + +
{% trans "VM ID" %}{% trans "IP Address" %}{% trans "Period" %}{% trans "Amount" %}
{{ invoice.order.vm_id }}{{ ips|get_value_from_dict:invoice.invoice_number|join:"
" }}
{{ line_items_to_show.0.period_start | date:'Y-m-d' }} — {{ line_items_to_show.0.period_end | date:'Y-m-d' }}{{ invoice.total_in_chf|floatformat:2|intcomma }} + {% trans 'See Invoice' %} +
+ + {% 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)