Merge branch 'feature/monthly-invoices' into 'master'

Feature/monthly invoices

See merge request ungleich-public/dynamicweb!689
This commit is contained in:
pcoder116 2019-04-13 21:18:36 +02:00
commit c9fb034ebd
12 changed files with 883 additions and 3 deletions

View file

@ -648,6 +648,7 @@ CELERY_RESULT_SERIALIZER = 'json'
CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5) CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5)
DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO') DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO')
ADMIN_EMAIL = env('ADMIN_EMAIL')
DCL_ERROR_EMAILS_TO_LIST = [] DCL_ERROR_EMAILS_TO_LIST = []
if DCL_ERROR_EMAILS_TO is not None: if DCL_ERROR_EMAILS_TO is not None:

View file

@ -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)))

View file

@ -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),
),
]

View file

@ -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=''),
),
]

View file

@ -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),
),
]

View file

@ -1,8 +1,11 @@
import json
import logging import logging
import os import os
import pytz
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import datetime
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -232,6 +235,207 @@ class HostingBill(AssignPermissionsMixin, models.Model):
return instance 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): class VMDetail(models.Model):
user = models.ForeignKey(CustomUser) user = models.ForeignKey(CustomUser)
vm_id = models.IntegerField(default=0) vm_id = models.IntegerField(default=0)

View file

@ -26,7 +26,7 @@
<img class="svg-img" src="{% static 'hosting/img/key.svg' %}"> <img class="svg-img" src="{% static 'hosting/img/key.svg' %}">
</div> </div>
</a> </a>
<a href="{% url 'hosting:orders' %}" class="hosting-dashboard-item"> <a href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' %}{% endif %}" class="hosting-dashboard-item">
<h2>{% trans "My Bills" %}</h2> <h2>{% trans "My Bills" %}</h2>
<div class="hosting-dashboard-image"> <div class="hosting-dashboard-image">
<img class="svg-img" src="{% static 'hosting/img/billing.svg' %}"> <img class="svg-img" src="{% static 'hosting/img/billing.svg' %}">

View file

@ -0,0 +1,224 @@
{% extends "hosting/base_short.html" %}
{% load staticfiles bootstrap3 humanize i18n custom_tags %}
{% block content %}
<div id="order-detail{{invoice.invoice_number}}" class="order-detail-container">
{% if messages %}
<div class="alert alert-warning">
{% for message in messages %}
<span>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
{% if not error %}
<div class="dashboard-container-head">
<h1 class="dashboard-title-thin">
<img src="{% static 'hosting/img/billing.svg' %}" class="un-icon">
{% blocktrans with page_header_text=page_header_text|default:"Invoice" %}{{page_header_text}}{% endblocktrans %}
</h1>
{% if invoice %}
<div class="dashboard-container-options">
<button type="button" class="btn-plain btn-pdf"
data-target="#order-detail{{invoice.invoice_number}}"><img
src="{% static 'hosting/img/icon-pdf.svg' %}"
class="svg-img"></button>
<button type="button" class="btn-plain btn-print"><img
src="{% static 'hosting/img/icon-print.svg' %}"
class="svg-img"></button>
</div>
{% endif %}
</div>
<div class="order-details">
{% if invoice %}
<p>
<strong>{% trans "Invoice #" %} {{invoice.invoice_number}}</strong>
</p>
{% endif %}
<p>
<strong>{% trans "Date" %}:</strong>
<span class="locale_date">
{% if invoice %}
{{invoice.paid_at|date:'Y-m-d h:i a'}}
{% else %}
{% now "Y-m-d h:i a" %}
{% endif %}
</span>
</p>
{% if invoice and vm %}
<p>
<strong>{% trans "Status" %}: </strong>
<strong>
{% if vm.terminated_at %}
<span class="vm-color-failed">{% trans "Terminated" %}</span>
{% elif invoice.order.status == 'Approved' %}
<span class="vm-color-online">{% trans "Approved" %}</span>
{% else %}
<span class="vm-status-failed">{% trans "Declined" %}</span>
{% endif %}
</strong>
</p>
{% endif %}
<hr>
<div>
<address>
<h4>{% trans "Billed to" %}:</h4>
<p>
{% if invoice.order %}
{{invoice.customer.user.name}}<br>
{{invoice.order.billing_address.street_address}},
{{invoice.order.billing_address.postal_code}}<br>
{{invoice.order.billing_address.city}},
{{invoice.order.billing_address.country}}
{% endif %}
</p>
</address>
</div>
<hr>
<div>
<h4>{% trans "Payment method" %}:</h4>
<p>
{% if invoice.order %}
{{invoice.order.cc_brand}} {% trans "ending in" %} ****
{{invoice.order.last4}}<br>
{{invoice.customer.user.email}}
{% endif %}
</p>
</div>
<hr>
<div>
<h4>{% trans "Invoice summary" %}</h4>
{% if vm %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{% if vm.name %}
{{ vm.name }}
{% endif %}
</p>
<div class="row">
<div class="col-sm-6">
{% if period_start %}
<p>
<span>{% trans "Period" %}: </span>
<span>
<span class="locale_date"
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 %}
</p>
<p>
<span>{% trans "Memory" %}: </span>
<strong class="pull-right">{{vm.memory}} GB</strong>
</p>
<p>
<span>{% trans "Disk space" %}: </span>
<strong class="pull-right">{{vm.disk_size}} GB</strong>
</p>
</div>
<div class="col-sm-12">
<hr class="thin-hr">
</div>
{% 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 %}
<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 %}
<p>
<strong>{% trans "Product" %}:</strong>&nbsp;
{{ product_name }}
</p>
<div class="row">
<div class="col-sm-6">
<p>
<span>{% trans "Amount" %}: </span>
<strong class="pull-right">{{total_in_chf|floatformat:2|intcomma}}
CHF</strong>
</p>
{% if invoice.order.generic_payment_description %}
<p>
<span>{% trans "Description" %}: </span>
<strong class="pull-right">{{invoice.order.generic_payment_description}}</strong>
</p>
{% endif %}
{% if invoice.order.subscription_id %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}}
{% trans "of every month" %}</strong>
</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
<hr class="thin-hr">
</div>
<div class="order_detail_footer">
<strong>ungleich glarus ag</strong>&nbsp;&nbsp;Bahnhofstrasse 1, 8783
Linthal, Switzerland<br>
www.datacenterlight.ch&nbsp;&nbsp;|&nbsp;&nbsp;info@datacenterlight.ch&nbsp;&nbsp;|&nbsp;&nbsp;<small>
CHE-156.970.649 MWST
</small>
</div>
{% endif %}
</div>
<div class="text-center" style="margin-bottom: 50px;">
<a class="btn btn-vm-back" href="{% url 'hosting:invoices' %}">{% trans "BACK TO LIST" %}</a>
</div>
<script type="text/javascript">
{% trans "Some problem encountered. Please try again later." as err_msg %}
var create_vm_error_message = '{{err_msg|safe}}';
</script>
{%endblock%}
{% block js_extra %}
{% if invoice.order %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.5/jspdf.min.js"></script>
<script src="{% static 'hosting/js/html2canvas.min.js' %}"></script>
<script src="{% static 'hosting/js/html2pdf.min.js' %}"></script>
<script src="{% static 'hosting/js/order.js' %}"></script>
{% endif %}
{% endblock js_extra %}

View file

@ -0,0 +1,61 @@
{% extends "hosting/base_short.html" %}
{% load staticfiles bootstrap3 humanize i18n custom_tags %}
{% block content %}
<div class="dashboard-container">
<div class="dashboard-container-head">
<h3 class="dashboard-title-thin"><img src="{% static 'hosting/img/shopping-cart.svg' %}" class="un-icon" style="margin-top: -4px; width: 30px;"> {% trans "My Bills" %}</h3>
{% if messages %}
<div class="alert alert-warning">
{% for message in messages %}
<span>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
<div class="dashboard-subtitle"></div>
</div>
<table class="table table-switch">
<thead>
<tr>
<th>{% trans "VM ID" %}</th>
<th>{% trans "IP Address" %}</th>
<th>{% trans "Period" %}</th>
<th>{% trans "Amount" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for invoice in invoices %}
<tr>
<td class="xs-td-inline" data-header="{% trans 'VM ID' %}">{{ invoice.order.vm_id }}</td>
<td class="xs-td-inline" data-header="{% trans 'IP Address' %}">{{ ips|get_value_from_dict:invoice.invoice_number|join:"<br/>" }}</td>
{% with line_items|get_value_from_dict:invoice.invoice_number as line_items_to_show %}
<td class="xs-td-inline" data-header="{% trans 'Period' %}">{{ line_items_to_show.0.period_start | date:'Y-m-d' }} &mdash; {{ line_items_to_show.0.period_end | date:'Y-m-d' }}</td>
{% endwith %}
<td class="xs-td-inline" data-header="{% trans 'Amount' %}">{{ invoice.total_in_chf|floatformat:2|intcomma }}</td>
<td class="text-right last-td">
<a class="btn btn-order-detail" href="{% url 'hosting:invoices' invoice.invoice_number %}">{% trans 'See Invoice' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<div class="pagination">
<span class="page-links">
{% if page_obj.has_previous %}
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
{% endif %}
<span class="page-current">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
{% endif %}
</span>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -8,7 +8,8 @@ from .views import (
MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView, MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView,
HostingPricingView, CreateVirtualMachinesView, HostingBillListView, HostingPricingView, CreateVirtualMachinesView, HostingBillListView,
HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, 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'payment/?$', PaymentVMView.as_view(), name='payment'),
url(r'settings/?$', SettingsView.as_view(), name='settings'), url(r'settings/?$', SettingsView.as_view(), name='settings'),
url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'), url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
url(r'invoices/?$', InvoiceListView.as_view(), name='invoices'),
url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(), url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(),
name='order-confirmation'), name='order-confirmation'),
url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(),
name='orders'), name='orders'),
url(r'invoice/(?P<invoice_id>[-\w]+)/?$', InvoiceDetailView.as_view(),
name='invoices'),
url(r'bills/?$', HostingBillListView.as_view(), name='bills'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(), url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(),
name='bills'), name='bills'),

View file

@ -61,7 +61,7 @@ from .forms import (
from .mixins import ProcessVMSelectionMixin, HostingContextMixin from .mixins import ProcessVMSelectionMixin, HostingContextMixin
from .models import ( from .models import (
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail, HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
GenericProduct GenericProduct, MonthlyHostingBill, HostingBillLineItem
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -83,6 +83,19 @@ class DashboardView(LoginRequiredMixin, View):
@method_decorator(decorators) @method_decorator(decorators)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
context = self.get_context_data() 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) return render(request, self.template_name, context)
@ -1146,6 +1159,180 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
return super(OrdersHostingListView, self).get(request, *args, **kwargs) 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): class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView):
login_url = reverse_lazy('hosting:login') login_url = reverse_lazy('hosting:login')
success_url = reverse_lazy('hosting:orders') success_url = reverse_lazy('hosting:orders')

View file

@ -122,6 +122,49 @@ class StripeUtils(object):
} }
return card_details 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 @handleStripeError
def get_cards_details_from_token(self, token): def get_cards_details_from_token(self, token):
stripe_token = stripe.Token.retrieve(token) stripe_token = stripe.Token.retrieve(token)