invoicing
This commit is contained in:
parent
a6c0f1d67a
commit
34db325ffe
9 changed files with 270 additions and 25 deletions
|
@ -1,3 +1,4 @@
|
|||
from djmoney.contrib.django_rest_framework import MoneyField
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, Invoice, InvoiceInstance, InvoiceVolume, \
|
||||
|
@ -23,18 +24,30 @@ class VolumePriceSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class InvoiceInstanceSerializer(serializers.ModelSerializer):
|
||||
adjusted_end_date = serializers.DateTimeField()
|
||||
price_charged = MoneyField(max_digits=10, decimal_places=0)
|
||||
price_charged_currency = serializers.CharField(source="price_charged.currency")
|
||||
|
||||
class Meta:
|
||||
model = InvoiceInstance
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class InvoiceVolumeSerializer(serializers.ModelSerializer):
|
||||
adjusted_end_date = serializers.DateTimeField()
|
||||
price_charged = MoneyField(max_digits=10, decimal_places=0)
|
||||
price_charged_currency = serializers.CharField(source="price_charged.currency")
|
||||
|
||||
class Meta:
|
||||
model = InvoiceVolume
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class InvoiceFloatingIpSerializer(serializers.ModelSerializer):
|
||||
adjusted_end_date = serializers.DateTimeField()
|
||||
price_charged = MoneyField(max_digits=10, decimal_places=0)
|
||||
price_charged_currency = serializers.CharField(source="price_charged.currency")
|
||||
|
||||
class Meta:
|
||||
model = InvoiceFloatingIp
|
||||
fields = '__all__'
|
||||
|
@ -44,6 +57,8 @@ class InvoiceSerializer(serializers.ModelSerializer):
|
|||
instances = InvoiceInstanceSerializer(many=True)
|
||||
floating_ips = InvoiceFloatingIpSerializer(many=True)
|
||||
volumes = InvoiceVolumeSerializer(many=True)
|
||||
subtotal = MoneyField(max_digits=10, decimal_places=0)
|
||||
subtotal_currency = serializers.CharField(source="subtotal.currency")
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
|
@ -53,5 +68,5 @@ class InvoiceSerializer(serializers.ModelSerializer):
|
|||
class SimpleInvoiceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = '__all__'
|
||||
fields = ['id', 'start_date', 'end_date', 'state']
|
||||
|
||||
|
|
63
api/views.py
63
api/views.py
|
@ -1,10 +1,13 @@
|
|||
from django.utils import timezone
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
import dateutil.parser
|
||||
|
||||
from api.serializers import FlavorPriceSerializer, FloatingIpsPriceSerializer, VolumePriceSerializer, InvoiceSerializer, \
|
||||
SimpleInvoiceSerializer
|
||||
from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice
|
||||
from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice, BillingProject, InvoiceInstance, \
|
||||
InvoiceFloatingIp
|
||||
|
||||
|
||||
class FlavorPriceViewSet(viewsets.ModelViewSet):
|
||||
|
@ -27,15 +30,65 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
|||
|
||||
def get_queryset(self):
|
||||
tenant_id = self.request.query_params.get('tenant_id', None)
|
||||
return Invoice.objects.filter(project__tenant_id=tenant_id)
|
||||
return Invoice.objects.filter(project__tenant_id=tenant_id).order_by('-start_date')
|
||||
|
||||
@action(detail=False)
|
||||
def simple_lists(self, request):
|
||||
def simple_list(self, request):
|
||||
serializer = SimpleInvoiceSerializer(self.get_queryset(), many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['POST'])
|
||||
def init_invoice(self, request):
|
||||
# TODO: Init invoice
|
||||
serializer = InvoiceSerializer()
|
||||
project, created = BillingProject.objects.get_or_create(tenant_id=request.data['tenant_id'])
|
||||
|
||||
date_today = timezone.now()
|
||||
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
new_invoice = Invoice.objects.create(
|
||||
project=project,
|
||||
start_date=month_first_day,
|
||||
state=Invoice.InvoiceState.IN_PROGRESS
|
||||
)
|
||||
new_invoice.save()
|
||||
|
||||
# Create Instance
|
||||
for instance in request.data['instances']:
|
||||
# Get Price
|
||||
flavor_price = FlavorPrice.objects.filter(flavor_id=instance['flavor_id']).first()
|
||||
|
||||
# Create new invoice instance
|
||||
start_date = dateutil.parser.isoparse(instance['start_date'])
|
||||
if start_date < month_first_day:
|
||||
start_date = month_first_day
|
||||
InvoiceInstance.objects.create(
|
||||
invoice=new_invoice,
|
||||
instance_id=instance['instance_id'],
|
||||
name=instance['name'],
|
||||
flavor_id=instance['flavor_id'],
|
||||
current_state=instance['current_state'],
|
||||
start_date=start_date,
|
||||
daily_price=flavor_price.daily_price,
|
||||
monthly_price=flavor_price.monthly_price,
|
||||
)
|
||||
|
||||
for fip in request.data['floating_ips']:
|
||||
# Get Price
|
||||
fip_price = FloatingIpsPrice.objects.first()
|
||||
|
||||
# Create new invoice floating ip
|
||||
start_date = dateutil.parser.isoparse(fip['start_date'])
|
||||
if start_date < month_first_day:
|
||||
start_date = month_first_day
|
||||
InvoiceFloatingIp.objects.create(
|
||||
invoice=new_invoice,
|
||||
fip_id=fip['fip_id'],
|
||||
ip=fip['ip'],
|
||||
current_state=fip['current_state'],
|
||||
start_date=start_date,
|
||||
daily_price=fip_price.daily_price,
|
||||
monthly_price=fip_price.monthly_price,
|
||||
)
|
||||
|
||||
serializer = InvoiceSerializer(new_invoice)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
|
23
core/migrations/0008_auto_20210901_0636.py
Normal file
23
core/migrations/0008_auto_20210901_0636.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-01 06:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_auto_20210831_0403'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoicefloatingip',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
23
core/migrations/0009_auto_20210901_0644.py
Normal file
23
core/migrations/0009_auto_20210901_0644.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-01 06:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_auto_20210901_0636'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoicefloatingip',
|
||||
name='end_date',
|
||||
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='end_date',
|
||||
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
|
@ -1,9 +1,10 @@
|
|||
from calendar import monthrange
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from djmoney.models.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
class FlavorPrice(models.Model):
|
||||
|
@ -44,11 +45,14 @@ class Invoice(models.Model):
|
|||
@property
|
||||
def subtotal(self):
|
||||
# Need to optimize? currently price is calculated on the fly, maybe need to save to db to make performance faster?
|
||||
# Instance Price
|
||||
instance_price = sum(map(lambda x: x.price_charged, self.instances.all()))
|
||||
fip_price = sum(map(lambda x: x.price_charged, self.floating_ips.all()))
|
||||
volume_price = sum(map(lambda x: x.price_charged, self.volumes.all()))
|
||||
price = instance_price + fip_price + volume_price
|
||||
if price == 0:
|
||||
return Money(amount=price, currency=settings.DEFAULT_CURRENCY)
|
||||
|
||||
# TODO: Add other price
|
||||
return instance_price
|
||||
return instance_price + fip_price + volume_price
|
||||
|
||||
|
||||
class InvoiceInstance(models.Model):
|
||||
|
@ -65,16 +69,20 @@ class InvoiceInstance(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def price_charged(self):
|
||||
# TODO: Fix price calculation
|
||||
# Currently only calculate daily price and it can return zero if end date not yet 1 day
|
||||
|
||||
def adjusted_end_date(self):
|
||||
current_date = timezone.now()
|
||||
if self.end_date:
|
||||
end_date = self.end_date
|
||||
else:
|
||||
end_date = current_date
|
||||
|
||||
return end_date
|
||||
|
||||
@property
|
||||
def price_charged(self):
|
||||
# TODO: Fix price calculation
|
||||
# Currently only calculate daily price and it can return zero if end date not yet 1 day
|
||||
end_date = self.adjusted_end_date
|
||||
# TODO: For Testing, please delete
|
||||
end_date += timedelta(days=1)
|
||||
|
||||
|
@ -88,12 +96,33 @@ class InvoiceFloatingIp(models.Model):
|
|||
ip = models.CharField(max_length=256)
|
||||
current_state = models.CharField(max_length=256)
|
||||
start_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField(default=None, blank=True, null=True)
|
||||
daily_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
created_at = models.DateTimeField(auto_created=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def adjusted_end_date(self):
|
||||
current_date = timezone.now()
|
||||
if self.end_date:
|
||||
end_date = self.end_date
|
||||
else:
|
||||
end_date = current_date
|
||||
|
||||
return end_date
|
||||
|
||||
@property
|
||||
def price_charged(self):
|
||||
# TODO: Fix price calculation
|
||||
# Currently only calculate daily price and it can return zero if end date not yet 1 day
|
||||
end_date = self.adjusted_end_date
|
||||
# TODO: For Testing, please delete
|
||||
end_date += timedelta(days=1)
|
||||
|
||||
days = (end_date - self.start_date).days
|
||||
return self.daily_price * days
|
||||
|
||||
|
||||
class InvoiceVolume(models.Model):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='volumes')
|
||||
|
@ -102,8 +131,29 @@ class InvoiceVolume(models.Model):
|
|||
space_allocation_gb = models.IntegerField()
|
||||
current_state = models.CharField(max_length=256)
|
||||
start_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField(default=None, blank=True, null=True)
|
||||
daily_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
created_at = models.DateTimeField(auto_created=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def adjusted_end_date(self):
|
||||
current_date = timezone.now()
|
||||
if self.end_date:
|
||||
end_date = self.end_date
|
||||
else:
|
||||
end_date = current_date
|
||||
|
||||
return end_date
|
||||
|
||||
@property
|
||||
def price_charged(self):
|
||||
# TODO: Fix price calculation
|
||||
# Currently only calculate daily price and it can return zero if end date not yet 1 day
|
||||
end_date = self.adjusted_end_date
|
||||
# TODO: For Testing, please delete
|
||||
end_date += timedelta(days=1)
|
||||
|
||||
days = (end_date - self.start_date).days
|
||||
return self.daily_price * self.space_allocation_gb * days
|
||||
|
|
|
@ -3,17 +3,26 @@ import logging
|
|||
from oslo_messaging import NotificationResult
|
||||
|
||||
from core.notification_handler.compute_handler import ComputeHandler
|
||||
from core.notification_handler.network_handler import NetworkHandler
|
||||
from core.notification_handler.volume_handler import VolumeHandler
|
||||
|
||||
LOG = logging.getLogger("rintik_notification")
|
||||
|
||||
|
||||
class NotifyEndpoint(object):
|
||||
handlers = [
|
||||
ComputeHandler(),
|
||||
NetworkHandler(),
|
||||
VolumeHandler()
|
||||
]
|
||||
|
||||
def info(self, ctxt, publisher_id, event_type, payload, metadata):
|
||||
# LOG.info("=== Event Received ===")
|
||||
# LOG.info("Event Type: " + str(event_type))
|
||||
# LOG.info("Payload: " + str(payload))
|
||||
LOG.info("=== Event Received ===")
|
||||
LOG.info("Event Type: " + str(event_type))
|
||||
LOG.info("Payload: " + str(payload))
|
||||
|
||||
ComputeHandler().handle(event_type, payload)
|
||||
# TODO: Error Handling
|
||||
for handler in self.handlers:
|
||||
handler.handle(event_type, payload)
|
||||
|
||||
return NotificationResult.HANDLED
|
||||
return NotificationResult.HANDLED
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceInstance, FlavorPrice
|
||||
|
@ -5,7 +6,7 @@ from core.models import Invoice, InvoiceInstance, FlavorPrice
|
|||
|
||||
class ComputeHandler:
|
||||
def get_tenant_progress_invoice(self, tenant_id):
|
||||
return Invoice.objects.filter(project_tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
||||
return Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
||||
|
||||
def handle(self, event_type, payload):
|
||||
if event_type == 'compute.instance.update':
|
||||
|
@ -25,6 +26,7 @@ class ComputeHandler:
|
|||
|
||||
# TODO: Handle flavor change
|
||||
|
||||
@transaction.atomic
|
||||
def handle_active_state(self, invoice, payload):
|
||||
display_name = payload['display_name']
|
||||
instance_id = payload['instance_id']
|
||||
|
@ -53,6 +55,7 @@ class ComputeHandler:
|
|||
monthly_price=flavor_price.monthly_price,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle_delete_state(self, invoice, payload):
|
||||
instance_id = payload['instance_id']
|
||||
flavor_id = payload['instance_flavor_id']
|
||||
|
|
|
@ -1,2 +1,68 @@
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceFloatingIp, FloatingIpsPrice
|
||||
|
||||
|
||||
class NetworkHandler:
|
||||
pass
|
||||
def get_tenant_progress_invoice(self, tenant_id):
|
||||
return Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
||||
|
||||
def handle(self, event_type, payload):
|
||||
if event_type == 'floatingip.create.end':
|
||||
tenant_id = payload['floatingip']['tenant_id']
|
||||
|
||||
# Get instance invoice
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
if not invoice:
|
||||
return
|
||||
|
||||
self.handle_create(invoice, payload)
|
||||
|
||||
if event_type == 'floatingip.delete.end':
|
||||
tenant_id = payload['floatingip']['tenant_id']
|
||||
|
||||
# Get instance invoice
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
if not invoice:
|
||||
return
|
||||
|
||||
self.handle_delete(invoice, payload)
|
||||
|
||||
@transaction.atomic
|
||||
def handle_create(self, invoice: Invoice, payload):
|
||||
fip_id = payload['floatingip']['id']
|
||||
ip = payload['floatingip']['floating_ip_address']
|
||||
is_exists = InvoiceFloatingIp.objects.filter(
|
||||
invoice=invoice,
|
||||
fip_id=fip_id
|
||||
).exists()
|
||||
|
||||
if not is_exists:
|
||||
# Get Price
|
||||
fip_price = FloatingIpsPrice.objects.first()
|
||||
|
||||
# Create new invoice floating ip
|
||||
InvoiceFloatingIp.objects.create(
|
||||
invoice=invoice,
|
||||
fip_id=fip_id,
|
||||
ip=ip,
|
||||
current_state='allocated',
|
||||
start_date=timezone.now(),
|
||||
daily_price=fip_price.daily_price,
|
||||
monthly_price=fip_price.monthly_price,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle_delete(self, invoice: Invoice, payload):
|
||||
fip_id = payload['floatingip']['id']
|
||||
|
||||
invoice_ip = FloatingIpsPrice.objects.filter(
|
||||
invoice=invoice,
|
||||
fip_id=fip_id,
|
||||
).first()
|
||||
|
||||
if invoice_ip:
|
||||
invoice_ip.end_date = timezone.now()
|
||||
invoice_ip.state = 'released'
|
||||
invoice_ip.save()
|
||||
|
|
3
core/notification_handler/volume_handler.py
Normal file
3
core/notification_handler/volume_handler.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
class VolumeHandler:
|
||||
def handle(self, event_type, payload):
|
||||
pass
|
Loading…
Add table
Reference in a new issue