From 4310504e75d7c0753556c9d933dccef3ff33204e Mon Sep 17 00:00:00 2001 From: Scott Baker Date: Fri, 6 Dec 2013 23:23:36 -0800 Subject: [PATCH] check in billing system models, admin, and sample data generator --- planetstack/core/admin.py | 69 +++++++++ planetstack/core/models/__init__.py | 1 + planetstack/core/models/billing.py | 76 ++++++++++ planetstack/tests/generate_billing_sample.py | 146 +++++++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 planetstack/core/models/billing.py create mode 100644 planetstack/tests/generate_billing_sample.py diff --git a/planetstack/core/admin.py b/planetstack/core/admin.py index 713dfdd..8cd54b2 100644 --- a/planetstack/core/admin.py +++ b/planetstack/core/admin.py @@ -902,6 +902,72 @@ def cache_credentials(sender, user, request, **kwds): request.session['auth'] = auth user_logged_in.connect(cache_credentials) +class InvoiceInline(admin.TabularInline): + model = Invoice + extra = 1 + verbose_name_plural = "Invoices" + verbose_name = "Invoice" + exclude = ['enacted'] + fields = ["date", "amount"] + readonly_fields = ["date", "amount"] + suit_classes = 'suit-tab suit-tab-accountinvoice' + +class InvoiceChargeInline(admin.TabularInline): + model = Charge + extra = 1 + verbose_name_plural = "Charges" + verbose_name = "Charge" + exclude = ['enacted'] + readonly_fields = ["date", "kind", "state", "object", "coreHours", "amount", "slice"] + +class InvoiceAdmin(admin.ModelAdmin): + list_display = ("date", "account") + + inlines = [InvoiceChargeInline] + + fields = ["date", "account", "amount"] + readonly_fields = ["date", "account", "amount"] + +class PendingChargeInline(admin.TabularInline): + model = Charge + extra = 1 + verbose_name_plural = "Charges" + verbose_name = "Charge" + exclude = ['enacted', "invoice"] + readonly_fields = ["date", "kind", "state", "object", "coreHours", "amount", "slice"] + suit_classes = 'suit-tab suit-tab-accountpendingcharges' + + def queryset(self, request): + qs = super(PendingChargeInline, self).queryset(request) + qs = qs.filter(state="pending") + return qs + +class PaymentInline(admin.TabularInline): + model=Payment + extra = 1 + verbose_name_plural = "Payments" + verbose_name = "Payment" + exclude = ['enacted'] + readonly_fields = ["date", "amount"] + suit_classes = 'suit-tab suit-tab-accountpayments' + +class AccountAdmin(admin.ModelAdmin): + list_display = ("site", "balance_due") + + inlines = [InvoiceInline, PaymentInline, PendingChargeInline] + + fieldsets = [ + (None, {'fields': ['site', 'balance_due', 'total_invoices', 'total_payments']})] # ,'classes':['suit-tab suit-tab-general']}),] + + readonly_fields = ['site', 'balance_due', 'total_invoices', 'total_payments'] + + suit_form_tabs =( + ('general','Account Details'), + ('accountinvoice', 'Invoices'), + ('accountpayments', 'Payments'), + ('accountpendingcharges','Pending Charges'), + ) + # Now register the new UserAdmin... admin.site.register(User, UserAdmin) @@ -919,6 +985,9 @@ admin.site.unregister(Evolution) # only the top-levels should be displayed showAll = True +admin.site.register(Account, AccountAdmin) +#admin.site.register(Invoice, InvoiceAdmin) + admin.site.register(Deployment, DeploymentAdmin) admin.site.register(Site, SiteAdmin) admin.site.register(Slice, SliceAdmin) diff --git a/planetstack/core/models/__init__.py b/planetstack/core/models/__init__.py index ac12370..b453a14 100644 --- a/planetstack/core/models/__init__.py +++ b/planetstack/core/models/__init__.py @@ -28,3 +28,4 @@ from .sliver import Sliver from .reservation import ReservedResource from .reservation import Reservation from .network import Network, NetworkParameterType, NetworkParameter, NetworkSliver, NetworkTemplate, Router, NetworkSlice +from .billing import Account, Invoice, Charge, UsableObject, Payment diff --git a/planetstack/core/models/billing.py b/planetstack/core/models/billing.py new file mode 100644 index 0000000..6d1c331 --- /dev/null +++ b/planetstack/core/models/billing.py @@ -0,0 +1,76 @@ +import datetime +import os +import socket +from django.db import models +from core.models import PlCoreBase, Site, Slice, Sliver, Deployment +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.db.models import Sum + +class Account(PlCoreBase): + site = models.ForeignKey(Site, related_name="accounts", help_text="Site for this account") + + @property + def total_invoices(self): + # Since the amount of an invoice is the sum of it's charges, we can + # compute the sum of the invoices by summing all charges where + # charge.invoice != Null. + x=self.charges.filter(invoice__isnull=False).aggregate(Sum('amount'))["amount__sum"] + if (x==None): + return 0.0 + return x + + @property + def total_payments(self): + x=self.payments.all().aggregate(Sum('amount'))["amount__sum"] + if (x==None): + return 0.0 + return x + + @property + def balance_due(self): + return self.total_invoices - self.total_payments + + def __unicode__(self): return u'%s' % (self.site.name) + +class Invoice(PlCoreBase): + date = models.DateTimeField() + account = models.ForeignKey(Account, related_name="invoices") + + @property + def amount(self): + return str(self.charges.all().aggregate(Sum('amount'))["amount__sum"]) + + def __unicode__(self): return u'%s-%s' % (self.account.site.name, str(self.date)) + +class UsableObject(PlCoreBase): + name = models.CharField(max_length=1024) + + def __unicode__(self): return u'%s' % (self.name) + +class Payment(PlCoreBase): + account = models.ForeignKey(Account, related_name="payments") + amount = models.FloatField(default=0.0) + date = models.DateTimeField(default=datetime.datetime.now) + + def __unicode__(self): return u'%s-%0.2f-%s' % (self.account.site.name, self.amount, str(self.date)) + +class Charge(PlCoreBase): + KIND_CHOICES = (('besteffort', 'besteffort'), ('reservation', 'reservation'), ('monthlyfee', 'monthlyfee')) + STATE_CHOICES = (('pending', 'pending'), ('invoiced', 'invoiced')) + + account = models.ForeignKey(Account, related_name="charges") + slice = models.ForeignKey(Slice, related_name="charges", null=True, blank=True) + kind = models.CharField(max_length=30, choices=KIND_CHOICES, default="besteffort") + state = models.CharField(max_length=30, choices=STATE_CHOICES, default="pending") + date = models.DateTimeField() + object = models.ForeignKey(UsableObject) + amount = models.FloatField(default=0.0) + coreHours = models.FloatField(default=0.0) + invoice = models.ForeignKey(Invoice, blank=True, null=True, related_name="charges") + + def __unicode__(self): return u'%s-%0.2f-%s' % (self.account.site.name, self.amount, str(self.date)) + + + + diff --git a/planetstack/tests/generate_billing_sample.py b/planetstack/tests/generate_billing_sample.py new file mode 100644 index 0000000..4fc1374 --- /dev/null +++ b/planetstack/tests/generate_billing_sample.py @@ -0,0 +1,146 @@ +""" + Basic Sliver Test + + 1) Create a slice1 + 2) Create sliver1 on slice1 +""" + +import datetime +import os +import operator +import pytz +import json +import random +import sys +import time + +MINUTE_SECONDS = 60 +HOUR_SECONDS = MINUTE_SECONDS * 60 +DAY_SECONDS = HOUR_SECONDS * 24 +MONTH_SECONDS = DAY_SECONDS * 30 + + +sys.path.append("/opt/planetstack") +#sys.path.append("/home/smbaker/projects/vicci/plstackapi/planetstack") + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "planetstack.settings") +#from openstack.manager import OpenStackManager +from core.models import Slice, Sliver, ServiceClass, Reservation, Tag, Network, User, Node, Image, Deployment, Site, NetworkTemplate, NetworkSlice +from core.models import Invoice, Charge, Account, UsableObject, Payment + +def delete_all(model): + for item in model.objects.all(): + item.delete() + +def get_usable_object(name): + objs = UsableObject.objects.filter(name=name) + if objs: + return objs[0] + obj = UsableObject(name=name) + obj.save() + return obj + +def generate_invoice(account, batch): + invoice = Invoice(date=batch[-1].date, account=account) + invoice.save() + for charge in batch: + charge.invoice = invoice + charge.state = "invoiced" + charge.save() + +def generate_invoices(account): + invoices = sorted(Invoice.objects.filter(account=account), key=operator.attrgetter('date')) + charges = sorted(Charge.objects.filter(account=account, state="pending"), key=operator.attrgetter('date')) + + if invoices: + latest_invoice_date = invoices[-1].date() + else: + latest_invoice_date = None + + batch = [] + last_week = 0 + for charge in charges: + # check to see if we crossed a week boundary. If we did, then generate + # an invoice for the last week's batch of charges + week = charge.date.isocalendar()[1] + if (week != last_week) and (batch): + generate_invoice(account, batch) + batch = [] + last_week = week + batch.append(charge) + + # we might still have last week's data batched up, and no data for this week + # if so, invoice the batch + this_week = datetime.datetime.now().isocalendar()[1] + if (this_week != last_week) and (batch): + generate_invoice(account, batch) + +def generate_payments(account): + invoices = Invoice.objects.filter(account=account) + for invoice in invoices: + # let's be optomistic and assume everyone pays exactly two weeks after + # receiving an invoice + payment_time = int(invoice.date.strftime("%s")) + 14 * DAY_SECONDS + if payment_time < time.time(): + payment_time = datetime.datetime.utcfromtimestamp(payment_time).replace(tzinfo=pytz.utc) + payment = Payment(account=account, amount=invoice.amount, date=payment_time) + payment.save() + +delete_all(Invoice) +delete_all(Charge) +delete_all(Payment) +delete_all(Account) +delete_all(UsableObject) + +for site in Site.objects.all(): + # only create accounts for sites where some slices exist + if len(site.slices.all()) > 0: + account = Account(site=site) + account.save() + +for slice in Slice.objects.all(): + site = slice.site + account = site.accounts.all()[0] + serviceClass =slice.serviceClass + + if not (slice.name in ["DnsRedir", "DnsDemux", "HyperCache"]): + continue + + now = int(time.time())/HOUR_SECONDS*HOUR_SECONDS + + charge_kind=None + for resource in slice.serviceClass.resources.all(): + if resource.name == "cpu.cores": + charge_kind = "reservation" + cost = resource.cost + elif (resource.name == "cycles") or (resource.name == "Cycles"): + charge_kind = "besteffort" + cost = resource.cost + + if not charge_kind: + print "failed to find resource for", slice.serviceClass + continue + + for sliver in slice.slivers.all(): + hostname = sliver.node.name + for i in range(now-MONTH_SECONDS, now, HOUR_SECONDS): + if charge_kind == "besteffort": + core_hours = random.randint(1,60)/100.0 + else: + core_hours = 1 + + amount = core_hours * cost + + object = get_usable_object(hostname) + + date = datetime.datetime.utcfromtimestamp(i).replace(tzinfo=pytz.utc) + + charge = Charge(account=account, slice=slice, kind=charge_kind, state="pending", date=date, object=object, coreHours=core_hours, amount=amount) + charge.save() + +for account in Account.objects.all(): + generate_invoices(account) + generate_payments(account) + + + -- 2.47.0