Merge branch 'master' into feature/6561/invoices-webhook

This commit is contained in:
PCoder 2019-11-28 19:52:39 +05:30
commit 6873d901c9
25 changed files with 892 additions and 182 deletions

View file

@ -9,7 +9,6 @@ from django.contrib.auth import authenticate
from django.utils.translation import ugettext_lazy as _
from membership.models import CustomUser
from utils.hosting_utils import get_all_public_keys
from .models import UserHostingKey, GenericProduct
logger = logging.getLogger(__name__)
@ -110,9 +109,14 @@ class ProductPaymentForm(GenericPaymentForm):
)
)
if self.product.product_is_subscription:
payment_type = "month"
if self.product.product_subscription_interval == "month":
payment_type = _('Monthly subscription')
elif self.product.product_subscription_interval == "year":
payment_type = _('Yearly subscription')
self.fields['amount'].label = "{amt} ({payment_type})".format(
amt=_('Amount in CHF'),
payment_type=_('Monthly subscription')
payment_type=payment_type
)
else:
self.fields['amount'].label = "{amt} ({payment_type})".format(
@ -193,15 +197,6 @@ class UserHostingKeyForm(forms.ModelForm):
KEY_ERROR_MESSAGE = _("Please input a proper SSH key")
openssh_pubkey_str = self.data.get('public_key').strip()
if openssh_pubkey_str in get_all_public_keys(self.request.user):
key_name = UserHostingKey.objects.filter(
user_id=self.request.user.id,
public_key=openssh_pubkey_str).first().name
KEY_EXISTS_MESSAGE = _(
"This key exists already with the name \"%(name)s\"") % {
'name': key_name}
raise forms.ValidationError(KEY_EXISTS_MESSAGE)
with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:
tmp_public_key_file.write(openssh_pubkey_str.encode('utf-8'))
tmp_public_key_file.flush()

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-07-09 15:21+0000\n"
"POT-Creation-Date: 2019-11-15 16:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -28,28 +28,31 @@ msgid "User does not exist"
msgstr "Der Benutzer existiert nicht"
msgid "Choose a product"
msgstr ""
msgstr "Wähle ein Produkt"
msgid "Amount in CHF"
msgstr "Betrag"
msgid "Recurring monthly"
msgstr ""
msgstr "monatlich wiederkehrend"
msgid "Amount field does not match"
msgstr ""
msgstr "Betragsfeld stimmt nicht überein"
msgid "Recurring field does not match"
msgstr ""
msgstr "Betragsfeld stimmt nicht überein"
msgid "Product name"
msgstr "Produkt"
msgid "Monthly subscription"
msgstr ""
msgstr "Monatliches Abonnement"
msgid "Yearly subscription"
msgstr "Jährliches Abonnement"
msgid "One time payment"
msgstr ""
msgstr "Einmalzahlung"
msgid "Confirm Password"
msgstr "Passwort Bestätigung"
@ -72,12 +75,8 @@ msgstr "Key-Name"
msgid "Please input a proper SSH key"
msgstr "Bitte verwende einen gültigen SSH-Key"
#, python-format
msgid "This key exists already with the name \"%(name)s\""
msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"
msgid "Comma not accepted in the name of the key"
msgstr ""
msgstr "Komma im Namen des Keys wird nicht akzeptiert"
msgid "All Rights Reserved"
msgstr "Alle Rechte vorbehalten"
@ -242,6 +241,10 @@ msgstr ""
msgid "You can view your VM detail by clicking the button below."
msgstr "Um die Rechnung zu sehen, klicke auf den Button unten."
msgid "You can log in to your VM by the username <strong>puffy</strong>."
msgstr ""
"Du kannst Dich auf Deiner VM mit dem user <strong>puffy</strong> einloggen."
msgid "View Detail"
msgstr "Details anzeigen"
@ -255,6 +258,9 @@ msgstr "Deine Bestellung von %(vm_name)s wurde entgegengenommen."
msgid "You can view your VM detail by following the link below."
msgstr "Um die Rechnung zu sehen, klicke auf den Link unten."
msgid "You can log in to your VM by the username puffy."
msgstr "Du kannst Dich auf Deiner VM mit dem user puffy einloggen."
msgid "Password Reset"
msgstr "Passwort zurücksetzen"
@ -398,13 +404,19 @@ msgid "Amount"
msgstr "Betrag"
msgid "Description"
msgstr ""
msgstr "Beschreibung"
msgid "Recurring"
msgstr ""
msgstr "wiederkehrend"
msgid "of"
msgstr "von"
msgid "each year"
msgstr "jedes Jahr"
msgid "of every month"
msgstr ""
msgstr "jeden Monat"
msgid "BACK TO LIST"
msgstr "ZURÜCK ZUR LISTE"
@ -416,16 +428,13 @@ msgid "VM ID"
msgstr ""
msgid "IP Address"
msgstr ""
msgstr "IP-Adresse"
msgid "See Invoice"
msgstr "Siehe Rechnung"
msgid "Page"
msgstr ""
msgid "of"
msgstr ""
msgstr "Seite"
msgid "Log in"
msgstr "Anmelden"
@ -487,7 +496,7 @@ msgid "Hold tight, we are processing your request"
msgstr "Bitte warten - wir bearbeiten Deine Anfrage gerade"
msgid "OK"
msgstr ""
msgstr "Ok"
msgid "Close"
msgstr "Schliessen"
@ -843,7 +852,7 @@ msgstr "Ungültige Speicher-Grösse"
#, python-brace-format
msgid "Incorrect pricing name. Please contact support{support_email}"
msgstr ""
msgstr "Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
msgid ""
"We could not find the requested VM. Please "
@ -862,7 +871,7 @@ msgstr "Fehler beenden VM"
msgid ""
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
"further information."
msgstr ""
msgstr "VM beendet wegen Zeitüberschreitung. Bitte kontaktiere support@datacenterlight.ch für weitere Informationen."
#, python-format
msgid "Virtual Machine %(vm_name)s Cancelled"
@ -873,6 +882,10 @@ msgstr ""
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
"noch einmal."
#, python-format
#~ msgid "This key exists already with the name \"%(name)s\""
#~ msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"
#~ msgid "Add your public SSH key"
#~ msgstr "Füge deinen öffentlichen SSH-Key hinzu"

View file

@ -1,3 +1,4 @@
import datetime
import logging
from django.core.management.base import BaseCommand
@ -19,6 +20,10 @@ class Command(BaseCommand):
def handle(self, *args, **options):
try:
for email in options['customer_email']:
self.stdout.write(
self.style.SUCCESS(
"---------------------------------------------")
)
stripe_utils = StripeUtils()
user = CustomUser.objects.get(email=email)
if hasattr(user, 'stripecustomer'):
@ -39,7 +44,9 @@ class Command(BaseCommand):
)
if all_invoices_response['error'] is not None:
self.stdout.write(self.style.ERROR(all_invoices_response['error']))
exit(1)
user.import_stripe_bill_remark += "{}: {},".format(datetime.datetime.now(), all_invoices_response['error'])
user.save()
continue
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)))
num_invoice_created = 0
@ -50,12 +57,25 @@ class Command(BaseCommand):
logger.debug("Invoice %s exists already. Not importing." % invoice['invoice_id'])
except MonthlyHostingBill.DoesNotExist as dne:
logger.debug("Invoice id %s does not exist" % invoice['invoice_id'])
num_invoice_created += 1 if MonthlyHostingBill.create(invoice) is not None else logger.error("Did not import invoice for %s" % str(invoice))
if MonthlyHostingBill.create(invoice) is not None:
num_invoice_created += 1
else:
user.import_stripe_bill_remark += "{}: Import failed - {},".format(
datetime.datetime.now(),
invoice['invoice_id'])
user.save()
logger.error("Did not import invoice for %s"
"" % str(invoice))
self.stdout.write(
self.style.SUCCESS("Number of invoices imported = %s" % num_invoice_created)
)
else:
self.stdout.write(self.style.SUCCESS(
'Customer email %s does not have a stripe customer.' % email))
user.import_stripe_bill_remark += "{}: No stripecustomer,".format(
datetime.datetime.now()
)
user.save()
except Exception as e:
print(" *** Error occurred. Details {}".format(str(e)))

View file

@ -0,0 +1,44 @@
from django.core.management.base import BaseCommand
import csv
from hosting.models import VATRates
class Command(BaseCommand):
help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv'''
def add_arguments(self, parser):
parser.add_argument('csv_file', nargs='+', type=str)
def handle(self, *args, **options):
try:
for c_file in options['csv_file']:
print("c_file = %s" % c_file)
with open(c_file, mode='r') as csv_file:
csv_reader = csv.DictReader(csv_file)
line_count = 0
for row in csv_reader:
if line_count == 0:
line_count += 1
obj, created = VATRates.objects.get_or_create(
start_date=row["start_date"],
stop_date=row["stop_date"] if row["stop_date"] is not "" else None,
territory_codes=row["territory_codes"],
currency_code=row["currency_code"],
rate=row["rate"],
rate_type=row["rate_type"],
description=row["description"]
)
if created:
self.stdout.write(self.style.SUCCESS(
'%s. %s - %s - %s - %s' % (
line_count,
obj.start_date,
obj.stop_date,
obj.territory_codes,
obj.rate
)
))
line_count+=1
except Exception as e:
print(" *** Error occurred. Details {}".format(str(e)))

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-10-26 04:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0055_auto_20190701_1614'),
]
operations = [
migrations.AlterField(
model_name='hostingbilllineitem',
name='amount',
field=models.IntegerField(),
),
]

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-11-15 05:16
from __future__ import unicode_literals
from django.db import migrations, models
import utils.mixins
class Migration(migrations.Migration):
dependencies = [
('hosting', '0056_auto_20191026_0454'),
]
operations = [
migrations.CreateModel(
name='VATRates',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_date', models.DateField(blank=True, null=True)),
('stop_date', models.DateField(blank=True, null=True)),
('territory_codes', models.TextField(blank=True, default='')),
('currency_code', models.CharField(max_length=10)),
('rate', models.FloatField()),
('rate_type', models.TextField(blank=True, default='')),
('description', models.TextField(blank=True, default='')),
],
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2019-11-15 14:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0057_vatrates'),
]
operations = [
migrations.AddField(
model_name='genericproduct',
name='product_subscription_interval',
field=models.CharField(default='month', help_text='Choose between `year` and `month`', max_length=10),
),
]

View file

@ -78,13 +78,17 @@ class GenericProduct(AssignPermissionsMixin, models.Model):
product_price = models.DecimalField(max_digits=6, decimal_places=2)
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
product_is_subscription = models.BooleanField(default=True)
product_subscription_interval = models.CharField(
max_length=10, default="month",
help_text="Choose between `year` and `month`")
def __str__(self):
return self.product_name
def get_actual_price(self):
def get_actual_price(self, vat_rate=None):
VAT = vat_rate if vat_rate is not None else self.product_vat
return round(
self.product_price + (self.product_price * self.product_vat), 2
float(self.product_price) + float(self.product_price) * float(VAT), 2
)
@ -322,7 +326,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
logger.debug("Neither subscription id nor vm_id available")
logger.debug("Can't import invoice")
return None
if args['order'] is None:
logger.error(
"Order is None for {}".format(args['invoice_id']))
return None
instance = cls.objects.create(
created=datetime.utcfromtimestamp(
args['created']).replace(tzinfo=pytz.utc),
@ -340,7 +347,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
args['period_start']).replace(tzinfo=pytz.utc),
period_end=datetime.utcfromtimestamp(
args['period_end']).replace(tzinfo=pytz.utc),
billing_reason=args['billing_reason'],
billing_reason=(
args['billing_reason']
if args['billing_reason'] is not None else ''
),
discount=args['discount'],
total=args['total'],
lines_data_count=args['lines_data_count'],
@ -469,7 +479,7 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model):
on_delete=models.CASCADE)
stripe_plan = models.ForeignKey(StripePlan, null=True,
on_delete=models.CASCADE)
amount = models.PositiveSmallIntegerField()
amount = models.IntegerField()
description = models.CharField(max_length=255)
discountable = models.BooleanField()
metadata = models.CharField(max_length=128)
@ -722,6 +732,16 @@ class UserCardDetail(AssignPermissionsMixin, models.Model):
return None
class VATRates(AssignPermissionsMixin, models.Model):
start_date = models.DateField(blank=True, null=True)
stop_date = models.DateField(blank=True, null=True)
territory_codes = models.TextField(blank=True, default='')
currency_code = models.CharField(max_length=10)
rate = models.FloatField()
rate_type = models.TextField(blank=True, default='')
description = models.TextField(blank=True, default='')
class FailedInvoice(AssignPermissionsMixin, models.Model):
permissions = ('view_failedinvoice',)
stripe_customer = models.ForeignKey(StripeCustomer)

View file

@ -33,6 +33,11 @@
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}You can view your VM detail by clicking the button below.{% endblocktrans %}
</p>
{% if 'OpenBSD' in vm_name %}
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
{% blocktrans %}You can log in to your VM by the username <strong>puffy</strong>.{% endblocktrans %}
</p>
{% endif %}
</td>
</tr>
<tr>

View file

@ -5,6 +5,9 @@
{% blocktrans %}You have ordered a new virtual machine!{% endblocktrans %}
{% blocktrans %}Your order of {{vm_name}} has been charged.{% endblocktrans %}
{% blocktrans %}You can view your VM detail by following the link below.{% endblocktrans %}
{% if 'OpenBSD' in vm_name %}
{% blocktrans %}You can log in to your VM by the username puffy.{% endblocktrans %}
{% endif %}
{{ base_url }}{{ order_url }}

View file

@ -57,7 +57,7 @@
<label for="configuration">Configuration: </label>
<select class="form-control" name="vm_template_id" id="{{vm.hosting_company}}-configuration" data-vm-type="{{vm.hosting_company}}">
{% for template in templates %}
<option value="{{template.id}}">{{ template.name }}</option>
<option value="{{template.id}}">{{ template.name|cut:'public-' }}</option>
{% endfor %}
</select>
</li>

View file

@ -93,10 +93,10 @@
<table>
<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>
{% for line_item in line_items %}
<tr class="border_bottom"><td>{% if line_item.description|length > 0 %}{{line_item.description}}{% elif line_item.stripe_plan.stripe_plan_name|length > 0 %}{{line_item.stripe_plan.stripe_plan_name}}{% else %}{{line_item.get_item_detail_str|safe}}{% endif %}</td><td>{{ line_item.period_start | date:'Y-m-d' }} &mdash; {{ 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>
<tr class="border_bottom"><td>{% if line_item.description|length > 0 %}{{line_item.description}}{% elif line_item.stripe_plan.stripe_plan_name|length > 0 %}{{line_item.stripe_plan.stripe_plan_name}}{% else %}{{line_item.get_item_detail_str|safe}}{% endif %}</td><td>{{ line_item.period_start | date:'Y-m-d' }} &mdash; {{ 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|floatformat:2}}</td></tr>
{% endfor %}
<tr class="grand-total-padding"><td colspan="4">Grand Total</td><td align="right">{{total_in_chf}}</td></tr>
<tr class="grand-total-padding"><td colspan="4">Grand Total</td><td align="right">{{total_in_chf|floatformat:2}}</td></tr>
</table>
{% else %}
<p>
@ -195,8 +195,13 @@
{% if invoice.order.subscription_id %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}}
{% if invoice.order.generic_product.product_subscription_interval == 'year' %}
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}} {% trans "of" %} {{invoice.order.created_at|date:'b'|title}}
{% trans "each year" %}</strong>
{% else %}
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}}
{% trans "of every month" %}</strong>
{% endif %}
</p>
{% endif %}
</div>

View file

@ -186,7 +186,13 @@
{% if order.subscription_id %}
<p>
<span>{% trans "Recurring" %}: </span>
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong>
{% if order.generic_product.product_subscription_interval == 'year' %}
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of" %} {{order.created_at|date:'b'|title}}
{% trans "each year" %}</strong>
{% else %}
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}}
{% trans "of every month" %}</strong>
{% endif %}
</p>
{% endif %}
</div>

View file

@ -1282,6 +1282,10 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
context['vm']['total_price'] = (
price + vat - discount['amount']
)
except TypeError:
logger.error("Type error. Probably we "
"came from a generic product. "
"Invoice ID %s" % obj.invoice_id)
except WrongIdError:
logger.error("WrongIdError while accessing "
"invoice {}".format(obj.invoice_id))
@ -1587,6 +1591,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
# Cancel Stripe subscription
stripe_utils = StripeUtils()
hosting_order = None
try:
hosting_order = HostingOrder.objects.get(
vm_id=vm.id
@ -1648,9 +1653,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
else:
sleep(2)
if not response['status']:
response['text'] = _("VM terminate action timed out. Please "
"contact support@datacenterlight.ch for "
"further information.")
response['text'] = str(_("VM terminate action timed out. "
"Please contact "
"support@datacenterlight.ch for "
"further information."))
context = {
'vm_name': vm_name,
'base_url': "{0}://{1}".format(
@ -1671,6 +1677,11 @@ class VirtualMachineView(LoginRequiredMixin, View):
email = BaseEmail(**email_data)
email.send()
admin_email_body.update(response)
admin_email_body["customer_email"] = owner.email
admin_email_body["VM_ID"] = vm.id
admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if
hosting_order is not None
else "unknown")
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
vm.id,
owner.email
@ -1760,7 +1771,8 @@ class CheckUserVM(APIView):
response = check_otp(user, realm, token)
if response != 200:
return Response('Invalid token', 403)
manager = OpenNebulaManager()
manager = OpenNebulaManager(settings.OPENNEBULA_USERNAME,
settings.OPENNEBULA_PASSWORD)
# not the best way to lookup vms by ip
# TODO: make this optimal
vms = manager.get_vms()