From 0e84081880404955b5c2746e731f9ee026ba78e9 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 2 Apr 2019 09:18:15 +0200 Subject: [PATCH 01/75] Add monthlyhostingbill model + code --- hosting/models.py | 22 ++++++++++++++++++++++ utils/stripe_utils.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/hosting/models.py b/hosting/models.py index 707b072d..4e4e2a59 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -232,6 +232,28 @@ class HostingBill(AssignPermissionsMixin, models.Model): return instance +class MonthlyHostingBill(AssignPermissionsMixin, models.Model): + customer = models.ForeignKey(StripeCustomer) + order = models.ForeignKey(HostingOrder) + receipt_number = models.CharField( + help_text="The receipt number that is generated on Stripe" + ) + invoice_number = models.CharField( + help_text="The invoice number that is generated on Stripe" + ) + billing_period = models.CharField( + help_text="The billing period for which the bill is valid" + ) + date_paid = models.DateField(help_text="Date on which the bill was paid") + + permissions = ('view_monthlyhostingbill',) + + class Meta: + permissions = ( + ('view_monthlyhostingbill', 'View Monthly Hosting'), + ) + + class VMDetail(models.Model): user = models.ForeignKey(CustomUser) vm_id = models.IntegerField(default=0) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index a3224a0e..d412cbd0 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -122,6 +122,26 @@ class StripeUtils(object): } return card_details + @handleStripeError + def get_all_invoices(self, customer_id): + invoices = stripe.Invoice.list(limit=100, customer=customer_id) + return_list = [] + for invoice in invoices: + invoice_details = { + 'created': invoice.created, + 'receipt_number': invoice.receipt_number, + 'invoice_number': invoice.number, + 'date_paid': invoice.date_paid, + 'period_start': invoice.period_start, + 'period_end': invoice.period_end, + 'paid': invoice.paid, + 'billing_reason': invoice.billing_reason, + 'discount': invoice.discount.coupon.amount_off if invoice.discount else 0, + 'total': invoice.total + } + return_list.append(invoice_details) + return return_list + @handleStripeError def get_cards_details_from_token(self, token): stripe_token = stripe.Token.retrieve(token) From 8dc00c9dd9040d1921bee56c3a67fd908def5ee3 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 2 Apr 2019 09:18:46 +0200 Subject: [PATCH 02/75] Add management command --- .../management/commands/fetch_stripe_bills.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 hosting/management/commands/fetch_stripe_bills.py diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py new file mode 100644 index 00000000..3a06a1d9 --- /dev/null +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -0,0 +1,28 @@ +from django.core.management.base import BaseCommand + +from hosting.models import UserCardDetail +from membership.models import CustomUser +from utils.stripe_utils import StripeUtils + + +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)) + stripe_utils.get_all_invoices( + user.stripecustomer.stripe_id + ) + 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))) From 6d42f88be1c2d3955812ac50a3ada803afcabe35 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:12:48 +0200 Subject: [PATCH 03/75] Complete implementation of fetch_stripe_bills --- .../management/commands/fetch_stripe_bills.py | 35 +++++++++++-- hosting/models.py | 38 +++++++++++--- utils/stripe_utils.py | 51 +++++++++++++------ 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 3a06a1d9..6285ec0a 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -1,9 +1,13 @@ +import logging + from django.core.management.base import BaseCommand -from hosting.models import UserCardDetail +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 @@ -18,11 +22,32 @@ class Command(BaseCommand): 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)) - stripe_utils.get_all_invoices( - user.stripecustomer.stripe_id + self.stdout.write(self.style.SUCCESS( + 'Found %s. Fetching bills for him.' % email)) + mhb = MonthlyHostingBill.objects.last( + customer=user.stripecustomer ) + 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 = {'gt': mhb.created} + + all_invoices_response = stripe_utils.get_all_invoices( + user.stripecustomer.stripe_id, + created=created_gt + ) + all_invoices = all_invoices_response['response_object'] + logger.debug( + "Obtained {} invoices".format(len(all_invoices)) + ) + for invoice in all_invoices: + MonthlyHostingBill.create( + invoice, stripe_customer=user.stripecustomer + ) else: - self.stdout.write(self.style.SUCCESS('Customer email %s does not have a stripe customer.' % email)) + 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/models.py b/hosting/models.py index 4e4e2a59..856e83ea 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -235,16 +235,24 @@ class HostingBill(AssignPermissionsMixin, models.Model): class MonthlyHostingBill(AssignPermissionsMixin, models.Model): 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" + 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" + help_text="The invoice number that is generated on Stripe", + max_length=100 ) - billing_period = models.CharField( - help_text="The billing period for which the bill is valid" - ) - date_paid = models.DateField(help_text="Date on which the bill was paid") + 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() permissions = ('view_monthlyhostingbill',) @@ -253,6 +261,24 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): ('view_monthlyhostingbill', 'View Monthly Hosting'), ) + @classmethod + def create(cls, stripe_customer, **args): + instance = cls.objects.create(args) + instance.customer = stripe_customer + if len(instance.lines_meta_data_csv) > 0: + vm_ids = [vm_id.strip() for vm_id in instance.lines_meta_data_csv.split(",")] + if len(vm_ids) == 1: + instance.order = HostingOrder.objects.get(vm_id=vm_ids[0]) + else: + logger.debug( + "More than one VM_ID" + "for MonthlyHostingBill {}".format(instance.invoice_id) + ) + logger.debug("VM_IDS=".format(','.join(vm_ids))) + instance.assign_permissions(stripe_customer.user) + instance.save() + return instance + class VMDetail(models.Model): user = models.ForeignKey(CustomUser) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index d412cbd0..63df0133 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -123,23 +123,42 @@ class StripeUtils(object): return card_details @handleStripeError - def get_all_invoices(self, customer_id): - invoices = stripe.Invoice.list(limit=100, customer=customer_id) + def get_all_invoices(self, customer_id, created): return_list = [] - for invoice in invoices: - invoice_details = { - 'created': invoice.created, - 'receipt_number': invoice.receipt_number, - 'invoice_number': invoice.number, - 'date_paid': invoice.date_paid, - 'period_start': invoice.period_start, - 'period_end': invoice.period_end, - 'paid': invoice.paid, - 'billing_reason': invoice.billing_reason, - 'discount': invoice.discount.coupon.amount_off if invoice.discount else 0, - 'total': invoice.total - } - return_list.append(invoice_details) + has_more_invoices = True + starting_after = False + while has_more_invoices: + if starting_after: + invoices = stripe.Invoice.list( + limit=10, customer=customer_id, created=created + ) + else: + invoices = stripe.Invoice.list( + limit=10, customer=customer_id, created=created, + starting_after=starting_after + ) + 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), + 'invoice_id': invoice.id, + 'lines_meta_data_csv': ','.join( + [line.metadata.VM_ID for line in invoice.lines.data] + ) + } + starting_after = invoice.id + return_list.append(invoice_details) return return_list @handleStripeError From 0bc8c350312216baf3a23ea3d2d1a366eefe6ae8 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:13:12 +0200 Subject: [PATCH 04/75] Add migration --- hosting/migrations/0050_monthlyhostingbill.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 hosting/migrations/0050_monthlyhostingbill.py 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), + ), + ] From b1566c4c61605ff5f9122af6a00d10ebfb2b2dfd Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:22:49 +0200 Subject: [PATCH 05/75] Get the last monthly hosting bill --- hosting/management/commands/fetch_stripe_bills.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 6285ec0a..fb4718c7 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -24,9 +24,8 @@ class Command(BaseCommand): if hasattr(user, 'stripecustomer'): self.stdout.write(self.style.SUCCESS( 'Found %s. Fetching bills for him.' % email)) - mhb = MonthlyHostingBill.objects.last( - customer=user.stripecustomer - ) + mhb = MonthlyHostingBill.objects.filter( + customer=user.stripecustomer).last() created_gt = {} if mhb is not None: # fetch only invoices which is created after From 6f1449836a654cad5eefe23720d07764017de077 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:27:54 +0200 Subject: [PATCH 06/75] Fix a bug: use starting_after if its defined --- utils/stripe_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 63df0133..ee6e1b18 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -130,12 +130,12 @@ class StripeUtils(object): while has_more_invoices: if starting_after: invoices = stripe.Invoice.list( - limit=10, customer=customer_id, created=created + limit=10, customer=customer_id, created=created, + starting_after=starting_after ) else: invoices = stripe.Invoice.list( - limit=10, customer=customer_id, created=created, - starting_after=starting_after + limit=10, customer=customer_id, created=created ) has_more_invoices = invoices.has_more for invoice in invoices.data: From b7dd4acb0798d655a9becc8c5e09d0a37071f4f4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:33:28 +0200 Subject: [PATCH 07/75] Correct the way of getting VM_ID meta data --- utils/stripe_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index ee6e1b18..53346626 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -154,7 +154,7 @@ class StripeUtils(object): 'lines_data_count': len(invoice.lines.data), 'invoice_id': invoice.id, 'lines_meta_data_csv': ','.join( - [line.metadata.VM_ID for line in invoice.lines.data] + [line.metadata.VM_ID if line.metadata.VM_ID is not None else '' for line in invoice.lines.data] ) } starting_after = invoice.id From 3eaa53ca78bb7e52027853ce53409d46493fd471 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:36:28 +0200 Subject: [PATCH 08/75] Use stdout instead of logger --- hosting/management/commands/fetch_stripe_bills.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index fb4718c7..89fffb27 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -38,9 +38,7 @@ class Command(BaseCommand): created=created_gt ) all_invoices = all_invoices_response['response_object'] - logger.debug( - "Obtained {} invoices".format(len(all_invoices)) - ) + self.stdout.write(self.style.SUCCESS("Obtained {} invoices".format(len(all_invoices)))) for invoice in all_invoices: MonthlyHostingBill.create( invoice, stripe_customer=user.stripecustomer From 033db01810643f9c9129b57103269ee7622b70cf Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:51:19 +0200 Subject: [PATCH 09/75] Correct error in getting lines data count --- utils/stripe_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 53346626..834b2201 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -151,7 +151,7 @@ class StripeUtils(object): '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), + '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 line.metadata.VM_ID is not None else '' for line in invoice.lines.data] From c85a4f379652c8fb19031164b234c84f442db885 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 06:59:05 +0200 Subject: [PATCH 10/75] Catch error from stripe call --- hosting/management/commands/fetch_stripe_bills.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 89fffb27..7219341b 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -37,8 +37,11 @@ class Command(BaseCommand): user.stripecustomer.stripe_id, created=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)))) + 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: MonthlyHostingBill.create( invoice, stripe_customer=user.stripecustomer From dbf3b92c063b5b965e50874522cbd8927e32cf58 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 07:08:39 +0200 Subject: [PATCH 11/75] Add logging and verbosity --- .../management/commands/fetch_stripe_bills.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 7219341b..cbb09a2e 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -1,4 +1,5 @@ import logging +import sys from django.core.management.base import BaseCommand @@ -13,10 +14,32 @@ class Command(BaseCommand): help = '''Fetches invoices from Stripe and creates bills for a given customer in the MonthlyHostingBill model''' + def set_logger(self, verbosity): + """ + Set logger level based on verbosity option + """ + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter('%(asctime)s|%(levelname)s|%(module)s| %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + if verbosity == 0: + self.logger.setLevel(logging.WARN) + elif verbosity == 1: # default + self.logger.setLevel(logging.INFO) + elif verbosity > 1: + self.logger.setLevel(logging.DEBUG) + + # verbosity 3: also enable all logging statements that reach the root + # logger + if verbosity > 2: + logging.getLogger().setLevel(logging.DEBUG) + def add_arguments(self, parser): parser.add_argument('customer_email', nargs='+', type=str) def handle(self, *args, **options): + self.set_logger(options.get('verbosity')) try: for email in options['customer_email']: stripe_utils = StripeUtils() From 66ffbf38aa729ba753878bcccb6b799fb4f58582 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 07:32:18 +0200 Subject: [PATCH 12/75] Handle if VM_ID metadata is not set --- utils/stripe_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 834b2201..7211465a 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -154,7 +154,7 @@ class StripeUtils(object): '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 line.metadata.VM_ID is not None else '' for line in invoice.lines.data] + [line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' for line in invoice.lines.data] ) } starting_after = invoice.id From 444f79eab7f03724be7ec5bfef165229895475e0 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 07:35:43 +0200 Subject: [PATCH 13/75] Remove unwanted logger code --- .../management/commands/fetch_stripe_bills.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index cbb09a2e..7219341b 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -1,5 +1,4 @@ import logging -import sys from django.core.management.base import BaseCommand @@ -14,32 +13,10 @@ class Command(BaseCommand): help = '''Fetches invoices from Stripe and creates bills for a given customer in the MonthlyHostingBill model''' - def set_logger(self, verbosity): - """ - Set logger level based on verbosity option - """ - handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter('%(asctime)s|%(levelname)s|%(module)s| %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) - - if verbosity == 0: - self.logger.setLevel(logging.WARN) - elif verbosity == 1: # default - self.logger.setLevel(logging.INFO) - elif verbosity > 1: - self.logger.setLevel(logging.DEBUG) - - # verbosity 3: also enable all logging statements that reach the root - # logger - if verbosity > 2: - logging.getLogger().setLevel(logging.DEBUG) - def add_arguments(self, parser): parser.add_argument('customer_email', nargs='+', type=str) def handle(self, *args, **options): - self.set_logger(options.get('verbosity')) try: for email in options['customer_email']: stripe_utils = StripeUtils() From 12b8a778623de9121a2246ac0506264070aabe33 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 09:03:58 +0200 Subject: [PATCH 14/75] Fix issues and also include subscription_id --- .../management/commands/fetch_stripe_bills.py | 5 +- hosting/migrations/0051_auto_20190403_0703.py | 25 +++++++ hosting/models.py | 65 ++++++++++++++++--- utils/stripe_utils.py | 5 +- 4 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 hosting/migrations/0051_auto_20190403_0703.py diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 7219341b..8f35aa8c 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -43,9 +43,8 @@ class Command(BaseCommand): 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: - MonthlyHostingBill.create( - invoice, stripe_customer=user.stripecustomer - ) + invoice['customer'] = user.stripecustomer + MonthlyHostingBill.create(invoice) else: self.stdout.write(self.style.SUCCESS( 'Customer email %s does not have a stripe customer.' % email)) 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/models.py b/hosting/models.py index 856e83ea..c976c336 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -1,8 +1,10 @@ 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 @@ -252,7 +254,8 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): total = models.IntegerField() lines_data_count = models.IntegerField() invoice_id = models.CharField(unique=True, max_length=100) - lines_meta_data_csv = models.TextField() + lines_meta_data_csv = models.TextField(default="") + subscription_ids_csv = models.TextField(default="") permissions = ('view_monthlyhostingbill',) @@ -262,21 +265,63 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): ) @classmethod - def create(cls, stripe_customer, **args): - instance = cls.objects.create(args) - instance.customer = stripe_customer - if len(instance.lines_meta_data_csv) > 0: - vm_ids = [vm_id.strip() for vm_id in instance.lines_meta_data_csv.split(",")] + 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: - instance.order = HostingOrder.objects.get(vm_id=vm_ids[0]) + args['order'] = HostingOrder.objects.get(vm_id=vm_ids[0]) else: logger.debug( "More than one VM_ID" - "for MonthlyHostingBill {}".format(instance.invoice_id) + "for MonthlyHostingBill {}".format(args['invoice_id']) ) logger.debug("VM_IDS=".format(','.join(vm_ids))) - instance.assign_permissions(stripe_customer.user) - instance.save() + 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 '' + ), + 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'], + stripe_customer=args['customer'], + subscription_ids_csv=args['subscription_ids_csv'], + ) + + instance.assign_permissions(instance.stripe_customer.user) return instance diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index 7211465a..ec430485 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -155,7 +155,10 @@ class StripeUtils(object): '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.subscription if hasattr(line, 'subscription') else '' for line in invoice.lines.data] + ), } starting_after = invoice.id return_list.append(invoice_details) From 8e1e3e41576baa4c33bb7a23e733999632bba68c Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 09:12:14 +0200 Subject: [PATCH 15/75] Correct variable names --- hosting/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hosting/models.py b/hosting/models.py index c976c336..9b81addd 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -317,7 +317,8 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): lines_data_count=args['lines_data_count'], invoice_id=args['invoice_id'], lines_meta_data_csv=args['lines_meta_data_csv'], - stripe_customer=args['customer'], + customer=args['customer'], + order=args['order'], subscription_ids_csv=args['subscription_ids_csv'], ) From 2c3146111f5113116bf4dfa52772cbf26924b448 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 09:20:38 +0200 Subject: [PATCH 16/75] Fix getting subscription id --- utils/stripe_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/stripe_utils.py b/utils/stripe_utils.py index ec430485..b43470fa 100644 --- a/utils/stripe_utils.py +++ b/utils/stripe_utils.py @@ -157,7 +157,7 @@ class StripeUtils(object): [line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' for line in invoice.lines.data] ), 'subscription_ids_csv': ','.join( - [line.subscription if hasattr(line, 'subscription') else '' for line in invoice.lines.data] + [line.id if line.type == 'subscription' else '' for line in invoice.lines.data] ), } starting_after = invoice.id From a690ef421f8f55320c7c05121f715e1b67102347 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 09:24:25 +0200 Subject: [PATCH 17/75] Fix variable name --- hosting/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/models.py b/hosting/models.py index 9b81addd..fb2a805b 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -322,7 +322,7 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): subscription_ids_csv=args['subscription_ids_csv'], ) - instance.assign_permissions(instance.stripe_customer.user) + instance.assign_permissions(instance.customer.user) return instance From cc6afa8d2abd1d7a540fd9f471b09228cf4ceaaa Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 19:22:27 +0200 Subject: [PATCH 18/75] Fix datetime issue: pass unix timestamp instead of datetime --- hosting/management/commands/fetch_stripe_bills.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index 8f35aa8c..e6dd9536 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -31,7 +31,7 @@ class Command(BaseCommand): # fetch only invoices which is created after # mhb.created, because we already have invoices till # this date - created_gt = {'gt': mhb.created} + created_gt = {'gt': mhb.created.timestamp()} all_invoices_response = stripe_utils.get_all_invoices( user.stripecustomer.stripe_id, From 5c31417a371c0376bc747208481da4e6ee9b69a0 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 19:34:57 +0200 Subject: [PATCH 19/75] Convert timestamp to int --- hosting/management/commands/fetch_stripe_bills.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index e6dd9536..2a37ed61 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -31,7 +31,7 @@ class Command(BaseCommand): # fetch only invoices which is created after # mhb.created, because we already have invoices till # this date - created_gt = {'gt': mhb.created.timestamp()} + created_gt = {'gt': int(mhb.created.timestamp())} all_invoices_response = stripe_utils.get_all_invoices( user.stripecustomer.stripe_id, From 147fd0fe5edbb4566fcb043ef55decbd2792a44e Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 20:29:06 +0200 Subject: [PATCH 20/75] Add invoices.html --- hosting/templates/hosting/invoices.html | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 hosting/templates/hosting/invoices.html diff --git a/hosting/templates/hosting/invoices.html b/hosting/templates/hosting/invoices.html new file mode 100644 index 00000000..96d9e9e3 --- /dev/null +++ b/hosting/templates/hosting/invoices.html @@ -0,0 +1,57 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3 humanize i18n %} + +{% block content %} +
+
+

{% trans "My Bills" %}

+ {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} +
+
+ + + + + + + + + + + + {% for order in orders %} + + + + + + + {% endfor %} + +
{% trans "Order Nr." %}{% trans "Date" %}{% trans "Amount" %}
{{ order.id }}{{ order.created_at | date:'Y-m-d h:i a' }}{{ order.price|floatformat:2|intcomma }} + {% trans 'See Invoice' %} +
+ + {% if is_paginated %} + + {% endif %} +
+{% endblock %} From de3734bf2000ba0c2db170342cb055f0a960b48b Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 20:29:33 +0200 Subject: [PATCH 21/75] Add total_in_chf utility method --- hosting/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hosting/models.py b/hosting/models.py index fb2a805b..b735bb8f 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -325,6 +325,15 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): 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 + class VMDetail(models.Model): user = models.ForeignKey(CustomUser) From 71832f8afc277cb1ebf1ea4d710d353d3d6651f3 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 20:31:24 +0200 Subject: [PATCH 22/75] invoices.html: Replace all order instances by invoice --- hosting/templates/hosting/invoices.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hosting/templates/hosting/invoices.html b/hosting/templates/hosting/invoices.html index 96d9e9e3..2fa2e3f4 100644 --- a/hosting/templates/hosting/invoices.html +++ b/hosting/templates/hosting/invoices.html @@ -18,20 +18,20 @@ - + - {% for order in orders %} + {% for invoice in invoices %} - - - + + + {% endfor %} From dbe3b2558cd132270fabd46ff89aef7d5f32f487 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 20:31:54 +0200 Subject: [PATCH 23/75] Create an InvoiceListView --- hosting/views.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index 32de4e54..043bad99 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 ) logger = logging.getLogger(__name__) @@ -1146,6 +1146,22 @@ class OrdersHostingListView(LoginRequiredMixin, ListView): return super(OrdersHostingListView, self).get(request, *args, **kwargs) +class InvoiceListView(OrdersHostingListView): + template_name = "hosting/invoices.html" + context_object_name = "invoices" + model = MonthlyHostingBill + ordering = '-created' + + def get_queryset(self): + user = self.request.user + self.queryset = MonthlyHostingBill.objects.filter(customer__user=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 OrdersHostingDeleteView(LoginRequiredMixin, DeleteView): login_url = reverse_lazy('hosting:login') success_url = reverse_lazy('hosting:orders') From def5a3a0115c70bbbdca5daef51a4fbaffe8462b Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 20:34:04 +0200 Subject: [PATCH 24/75] Add invoice urls --- hosting/urls.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hosting/urls.py b/hosting/urls.py index 32ef8400..3a0dd72f 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 ) @@ -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'invoices/(?P\d+)/?$', OrdersHostingDetailView.as_view(), + name='invoices'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'), url(r'bills/(?P\d+)/?$', HostingBillDetailView.as_view(), name='bills'), From e843a6f85753292e803dc6d373e7ca36490094e5 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 21:16:19 +0200 Subject: [PATCH 25/75] Make invoicelistview not inherit OrderHostingListView --- hosting/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index 043bad99..47456574 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1146,8 +1146,9 @@ class OrdersHostingListView(LoginRequiredMixin, ListView): return super(OrdersHostingListView, self).get(request, *args, **kwargs) -class InvoiceListView(OrdersHostingListView): +class InvoiceListView(LoginRequiredMixin, ListView): template_name = "hosting/invoices.html" + login_url = reverse_lazy('hosting:login') context_object_name = "invoices" model = MonthlyHostingBill ordering = '-created' From 247bbe622fb47352e457faa5f6a370199fb941b4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 21:29:49 +0200 Subject: [PATCH 26/75] Add missing invoice_number argument to MHB create --- hosting/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hosting/models.py b/hosting/models.py index b735bb8f..5b48abbf 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -305,6 +305,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): 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( From ba9e5548811bb1e4160ac777b294b5268c9e9fd4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 21:52:07 +0200 Subject: [PATCH 27/75] Implement get_object for invoice detail + url fix --- hosting/urls.py | 4 ++-- hosting/views.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/hosting/urls.py b/hosting/urls.py index 3a0dd72f..3f5a6f50 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -9,7 +9,7 @@ from .views import ( HostingPricingView, CreateVirtualMachinesView, HostingBillListView, HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView, - InvoiceListView + InvoiceListView, InvoiceDetailView ) @@ -28,7 +28,7 @@ urlpatterns = [ name='order-confirmation'), url(r'orders/(?P\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'), - url(r'invoices/(?P\d+)/?$', OrdersHostingDetailView.as_view(), + url(r'invoices/(?P[-\w]+)/?$', InvoiceDetailView.as_view(), name='invoices'), url(r'bills/?$', HostingBillListView.as_view(), name='bills'), url(r'bills/(?P\d+)/?$', HostingBillDetailView.as_view(), diff --git a/hosting/views.py b/hosting/views.py index 47456574..17f63039 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1163,6 +1163,35 @@ class InvoiceListView(LoginRequiredMixin, ListView): 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 HostingOrder.DoesNotExist: + logger.debug("MHB not found for id {invoice_id}".format( + invoice_id=invoice_id + )) + invoice_obj = None + return invoice_obj + + class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView): login_url = reverse_lazy('hosting:login') success_url = reverse_lazy('hosting:orders') From 94586c854a47f7569f3ef35371e50fec9d589414 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:48:23 +0200 Subject: [PATCH 28/75] Add invoice detail --- hosting/templates/hosting/invoice_detail.html | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 hosting/templates/hosting/invoice_detail.html diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html new file mode 100644 index 00000000..ff6ec31d --- /dev/null +++ b/hosting/templates/hosting/invoice_detail.html @@ -0,0 +1,235 @@ +{% 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 %} + {{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}}
+ {{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 %} From d37a2de6eb5dec8c935b7b4692d87875cf60775a Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:48:56 +0200 Subject: [PATCH 29/75] Add utility functions --- hosting/models.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/hosting/models.py b/hosting/models.py index 5b48abbf..d58e2fce 100644 --- a/hosting/models.py +++ b/hosting/models.py @@ -338,6 +338,33 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model): """ 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 + class VMDetail(models.Model): user = models.ForeignKey(CustomUser) From ba6fa531db76bb7a8683f45cf9b76fd973f4a393 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:49:25 +0200 Subject: [PATCH 30/75] Correct the name of the layout --- hosting/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index 17f63039..5a8f45c7 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1164,7 +1164,7 @@ class InvoiceListView(LoginRequiredMixin, ListView): class InvoiceDetailView(LoginRequiredMixin, DetailView): - template_name = "hosting/invoice-detail.html" + template_name = "hosting/invoice_detail.html" context_object_name = "invoice" login_url = reverse_lazy('hosting:login') permission_required = ['view_monthlyhostingbill'] From 7de2129a0053fd799e568a7508b791a2345d2ee4 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:49:45 +0200 Subject: [PATCH 31/75] Implement get invoice --- hosting/views.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/hosting/views.py b/hosting/views.py index 5a8f45c7..49c78b7e 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1191,6 +1191,82 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): 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.vm_pricing.name + if obj.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.vm_pricing.name + if obj.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.period_start + context['period_end'] = obj.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): + context = self.get_context_data() + return self.render_to_response(context) + class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView): login_url = reverse_lazy('hosting:login') From d07f3d7eba1d54c552bfaea0d56d94b2220bec62 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:59:01 +0200 Subject: [PATCH 32/75] Add missing object param --- hosting/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index 49c78b7e..39e2a7a9 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1264,7 +1264,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): @method_decorator(decorators) def get(self, request, *args, **kwargs): - context = self.get_context_data() + context = self.get_context_data(object=self.get_object()) return self.render_to_response(context) From 76e3d951354f4d2278f322792be90d221566e1f3 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 22:59:39 +0200 Subject: [PATCH 33/75] Use invoice_number of invoice pk --- hosting/templates/hosting/invoices.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/templates/hosting/invoices.html b/hosting/templates/hosting/invoices.html index 2fa2e3f4..6a0aeb41 100644 --- a/hosting/templates/hosting/invoices.html +++ b/hosting/templates/hosting/invoices.html @@ -31,7 +31,7 @@
{% endfor %} From 3ed5823c93651e25bf1e0749d5b0045b2b0459fe Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:04:35 +0200 Subject: [PATCH 34/75] Add missing self.object initializer --- hosting/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hosting/views.py b/hosting/views.py index 39e2a7a9..3a8997a9 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1264,6 +1264,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): @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) From ef09ae4dab1b355630af25edd4aeefa5a0f6062a Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:07:37 +0200 Subject: [PATCH 35/75] Obtaing pricing from order --- hosting/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index 3a8997a9..12b0027f 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1208,8 +1208,8 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): cpu=context['vm']['cores'], ssd_size=context['vm']['disk_size'], memory=context['vm']['memory'], - pricing_name=(obj.vm_pricing.name - if obj.vm_pricing else 'default') + pricing_name=(obj.order.vm_pricing.name + if obj.order.vm_pricing else 'default') ) context['vm']['vat'] = vat context['vm']['price'] = price @@ -1229,8 +1229,8 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): cpu=context['vm']['cores'], ssd_size=context['vm']['disk_size'], memory=context['vm']['memory'], - pricing_name=(obj.vm_pricing.name - if obj.vm_pricing else 'default') + pricing_name=(obj.order.vm_pricing.name + if obj.order.vm_pricing else 'default') ) context['vm']['vat'] = vat context['vm']['price'] = price From ddd3cebc39f49aca62973921be9f7d716001332b Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:09:57 +0200 Subject: [PATCH 36/75] Fix blocktrans reformatted mistakenly --- hosting/templates/hosting/invoice_detail.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index ff6ec31d..8a094519 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -14,9 +14,8 @@ {% if not error %}

- {% - blocktrans with page_header_text=page_header_text|default:"Invoice" - %}{{page_header_text}}{% endblocktrans %} + + {% blocktrans with page_header_text=page_header_text|default:"Invoice" %}{{page_header_text}}{% endblocktrans %}

{% if invoice %}
From 47422a99afa1b6cccd0533915d2fefe565c1bbf7 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:11:59 +0200 Subject: [PATCH 37/75] Fix more autoformatting related errors --- hosting/templates/hosting/invoice_detail.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index 8a094519..22d1d87a 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -207,7 +207,6 @@ CHE-156.970.649 MWST
- {% endif %}
@@ -216,10 +215,7 @@
{%endblock%} From d00e84a4b69508904225ce69934a671c5cf05db5 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:24:56 +0200 Subject: [PATCH 38/75] Fix bug related to proper alignment --- hosting/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index 12b0027f..ce73ad3c 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1251,14 +1251,14 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): context['error'] = 'WrongIdError' return context - # add context params from monthly hosting bill - context['period_start'] = obj.period_start - context['period_end'] = obj.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 + # add context params from monthly hosting bill + context['period_start'] = obj.period_start + context['period_end'] = obj.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 From 903fee4db198be5a4e35ba5c068a9ea97cf558f2 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:31:52 +0200 Subject: [PATCH 39/75] Fix more autoformatting issues --- hosting/templates/hosting/invoice_detail.html | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index 22d1d87a..9c300b3f 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -137,8 +137,7 @@ CHF

- {% trans "VAT" %} ({{ - vm.vat_percent|floatformat:2|intcomma }}%) + {% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) {{vm.vat|floatformat:2|intcomma}} CHF @@ -147,10 +146,8 @@ {% if vm.discount.amount > 0 %}

{%trans "Discount" as discount_name %} - {{ vm.discount.name|default:discount_name - }} - - {{ vm.discount.amount - }} CHF + {{ vm.discount.name|default:discount_name }} + - {{ vm.discount.amount }} CHF

{% endif %} @@ -162,9 +159,7 @@

{% trans "Total" %} - {% if vm.total_price - %}{{vm.total_price|floatformat:2|intcomma}}{% else - %}{{vm.price|floatformat:2|intcomma}}{% endif %} + {% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF

From f1a7958f03e3c61eef6e4b3dd4028066c273c353 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:34:26 +0200 Subject: [PATCH 40/75] Use correct class --- hosting/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index ce73ad3c..34e720f6 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1184,7 +1184,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): else: logger.error("User does not have permission to access") invoice_obj = None - except HostingOrder.DoesNotExist: + except MonthlyHostingBill.DoesNotExist: logger.debug("MHB not found for id {invoice_id}".format( invoice_id=invoice_id )) From baf62f1924c4079bb01bbb8fb0d629223fdd4661 Mon Sep 17 00:00:00 2001 From: PCoder Date: Wed, 3 Apr 2019 23:54:52 +0200 Subject: [PATCH 41/75] Simplify showing total price --- hosting/templates/hosting/invoice_detail.html | 6 ++---- hosting/views.py | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index 9c300b3f..80226123 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -139,8 +139,7 @@

{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) - {{vm.vat|floatformat:2|intcomma}} - CHF + {{vm.vat|floatformat:2|intcomma}} CHF

{% endif %} {% if vm.discount.amount > 0 %} @@ -159,8 +158,7 @@

{% trans "Total" %} - {% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} - CHF + {{total_in_chf}} CHF

diff --git a/hosting/views.py b/hosting/views.py index 34e720f6..af01ae86 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1173,7 +1173,9 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): def get_object(self, queryset=None): invoice_id = self.kwargs.get('invoice_id') try: - invoice_obj = MonthlyHostingBill.objects.get(invoice_number=invoice_id) + invoice_obj = MonthlyHostingBill.objects.get( + invoice_number=invoice_id + ) logger.debug("Found MHB for id {invoice_id}".format( invoice_id=invoice_id )) @@ -1184,7 +1186,7 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): else: logger.error("User does not have permission to access") invoice_obj = None - except MonthlyHostingBill.DoesNotExist: + except MonthlyHostingBill.DoesNotExist as dne: logger.debug("MHB not found for id {invoice_id}".format( invoice_id=invoice_id )) From 13f84a8580cda21cf55675fb1cf1e6d3a41ee248 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 4 Apr 2019 00:05:20 +0200 Subject: [PATCH 42/75] Add missing endif --- hosting/templates/hosting/invoice_detail.html | 1 + 1 file changed, 1 insertion(+) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index 80226123..0a2473e3 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -200,6 +200,7 @@ CHE-156.970.649 MWST
+ {% endif %}
From ef1bdee9a7da7e341f6e44d7046f7b544cc0f5cd Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 4 Apr 2019 00:05:45 +0200 Subject: [PATCH 43/75] Remove more autoformatting --- hosting/templates/hosting/invoice_detail.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hosting/templates/hosting/invoice_detail.html b/hosting/templates/hosting/invoice_detail.html index 0a2473e3..675962fa 100644 --- a/hosting/templates/hosting/invoice_detail.html +++ b/hosting/templates/hosting/invoice_detail.html @@ -204,8 +204,7 @@
{% trans "Order Nr." %}{% trans "Invoice Nr." %} {% trans "Date" %} {% trans "Amount" %}
{{ order.id }}{{ order.created_at | date:'Y-m-d h:i a' }}{{ order.price|floatformat:2|intcomma }}{{ invoice.invoice_number }}{{ invoice.paid_at | date:'Y-m-d h:i a' }}{{ invoice.total_in_chf|floatformat:2|intcomma }} - {% trans 'See Invoice' %} + {% trans 'See Invoice' %}
{{ invoice.paid_at | date:'Y-m-d h:i a' }} {{ invoice.total_in_chf|floatformat:2|intcomma }} - {% trans 'See Invoice' %} + {% trans 'See Invoice' %}