check in billing system models, admin, and sample data generator
authorScott Baker <smbaker@gmail.com>
Sat, 7 Dec 2013 07:23:36 +0000 (23:23 -0800)
committerScott Baker <smbaker@gmail.com>
Sat, 7 Dec 2013 07:23:36 +0000 (23:23 -0800)
planetstack/core/admin.py
planetstack/core/models/__init__.py
planetstack/core/models/billing.py [new file with mode: 0644]
planetstack/tests/generate_billing_sample.py [new file with mode: 0644]

index 713dfdd..8cd54b2 100644 (file)
@@ -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)
index ac12370..b453a14 100644 (file)
@@ -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 (file)
index 0000000..6d1c331
--- /dev/null
@@ -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 (file)
index 0000000..4fc1374
--- /dev/null
@@ -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)
+
+
+