Merge branch 'mega_refactor' into 'master'
Refactoring and Updating Component See merge request dev/rintik!1
This commit is contained in:
commit
b7088f9844
51 changed files with 1332 additions and 992 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -1,65 +1,42 @@
|
|||
from djmoney.contrib.django_rest_framework import MoneyField
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, Invoice, InvoiceInstance, InvoiceVolume, \
|
||||
InvoiceFloatingIp
|
||||
from core.models import Invoice
|
||||
from core.component import component
|
||||
|
||||
|
||||
class FlavorPriceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FlavorPrice
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class FloatingIpsPriceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FloatingIpsPrice
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class VolumePriceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = VolumePrice
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class InvoiceInstanceSerializer(serializers.ModelSerializer):
|
||||
class InvoiceComponentSerializer(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__'
|
||||
|
||||
def generate_invoice_component_serializer(model):
|
||||
"""
|
||||
Generate Invoice Component Serializer for particular model
|
||||
:param model: The invoice component model
|
||||
:return: serializer for particular model
|
||||
"""
|
||||
name = type(model).__name__
|
||||
meta_params = {
|
||||
"model": model,
|
||||
"fields": "__all__"
|
||||
}
|
||||
meta_class = type("Meta", (object,), meta_params)
|
||||
serializer_class = type(f"{name}Serializer", (InvoiceComponentSerializer,), {"Meta": meta_class})
|
||||
|
||||
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__'
|
||||
return serializer_class
|
||||
|
||||
|
||||
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")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field, model in component.INVOICE_COMPONENT_MODEL.items():
|
||||
self.fields[field] = generate_invoice_component_serializer(model)(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = '__all__'
|
||||
|
@ -69,4 +46,3 @@ class SimpleInvoiceSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['id', 'start_date', 'end_date', 'state']
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@ from django.urls import path, include
|
|||
from rest_framework import routers
|
||||
|
||||
from api import views
|
||||
from core.component import component
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'flavor_price', views.FlavorPriceViewSet)
|
||||
router.register(r'floating_ips_price', views.FloatingIpsPriceViewSet)
|
||||
router.register(r'volume_price', views.VolumePriceViewSet)
|
||||
for name, model in component.PRICE_MODEL.items():
|
||||
router.register(f"price/{name}", views.get_generic_model_view_set(model))
|
||||
|
||||
router.register(r'settings', views.DynamicSettingViewSet, basename='settings')
|
||||
router.register(r'invoice', views.InvoiceViewSet, basename='invoice')
|
||||
|
||||
urlpatterns = [
|
||||
|
|
161
api/views.py
161
api/views.py
|
@ -1,29 +1,55 @@
|
|||
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
|
||||
import pytz
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework import viewsets, serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.serializers import FlavorPriceSerializer, FloatingIpsPriceSerializer, VolumePriceSerializer, InvoiceSerializer, \
|
||||
SimpleInvoiceSerializer
|
||||
from core.models import FlavorPrice, FloatingIpsPrice, VolumePrice, Invoice, BillingProject, InvoiceInstance, \
|
||||
InvoiceFloatingIp, InvoiceVolume
|
||||
from api.serializers import InvoiceSerializer, SimpleInvoiceSerializer
|
||||
from core.models import Invoice, BillingProject
|
||||
from core.component import component
|
||||
from core.utils.dynamic_setting import get_dynamic_settings, get_dynamic_setting, set_dynamic_setting, BILLING_ENABLED
|
||||
|
||||
|
||||
class FlavorPriceViewSet(viewsets.ModelViewSet):
|
||||
queryset = FlavorPrice.objects
|
||||
serializer_class = FlavorPriceSerializer
|
||||
def get_generic_model_view_set(model):
|
||||
name = type(model).__name__
|
||||
meta_params = {
|
||||
"model": model,
|
||||
"fields": "__all__"
|
||||
}
|
||||
meta_class = type("Meta", (object,), meta_params)
|
||||
serializer_class = type(f"{name}Serializer", (serializers.ModelSerializer,), {"Meta": meta_class})
|
||||
|
||||
view_set_params = {
|
||||
"model": model,
|
||||
"queryset": model.objects,
|
||||
"serializer_class": serializer_class
|
||||
}
|
||||
|
||||
return type(f"{name}ViewSet", (viewsets.ModelViewSet,), view_set_params)
|
||||
|
||||
|
||||
class FloatingIpsPriceViewSet(viewsets.ModelViewSet):
|
||||
queryset = FloatingIpsPrice.objects
|
||||
serializer_class = FloatingIpsPriceSerializer
|
||||
class DynamicSettingViewSet(viewsets.ViewSet):
|
||||
def list(self, request):
|
||||
return Response(get_dynamic_settings())
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
return Response({
|
||||
pk: get_dynamic_setting(pk)
|
||||
})
|
||||
|
||||
class VolumePriceViewSet(viewsets.ModelViewSet):
|
||||
queryset = VolumePrice.objects
|
||||
serializer_class = VolumePriceSerializer
|
||||
def update(self, request, pk=None):
|
||||
set_dynamic_setting(pk, request.data['value'])
|
||||
return Response({
|
||||
pk: get_dynamic_setting(pk)
|
||||
})
|
||||
|
||||
def partial_update(self, request, pk=None):
|
||||
set_dynamic_setting(pk, request.data['value'])
|
||||
return Response({
|
||||
pk: get_dynamic_setting(pk)
|
||||
})
|
||||
|
||||
|
||||
class InvoiceViewSet(viewsets.ModelViewSet):
|
||||
|
@ -46,79 +72,48 @@ class InvoiceViewSet(viewsets.ModelViewSet):
|
|||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['POST'])
|
||||
def init_invoice(self, request):
|
||||
project, created = BillingProject.objects.get_or_create(tenant_id=request.data['tenant_id'])
|
||||
def enable_billing(self, request):
|
||||
# TODO: Handle unknown price
|
||||
self.handle_init_billing(request.data)
|
||||
|
||||
return Response({
|
||||
"status": "success"
|
||||
})
|
||||
|
||||
@transaction.atomic
|
||||
def handle_init_billing(self, data):
|
||||
set_dynamic_setting(BILLING_ENABLED, True)
|
||||
|
||||
projects = {}
|
||||
invoices = {}
|
||||
|
||||
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()
|
||||
for name, handler in component.INVOICE_HANDLER.items():
|
||||
payloads = data[name]
|
||||
|
||||
# Create Instance
|
||||
for instance in request.data['instances']:
|
||||
# Get Price
|
||||
flavor_price = FlavorPrice.objects.filter(flavor_id=instance['flavor_id']).first()
|
||||
for payload in payloads:
|
||||
|
||||
# Create new invoice instance
|
||||
start_date = self.parse_time(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,
|
||||
)
|
||||
if payload['tenant_id'] not in projects:
|
||||
project, created = BillingProject.objects.get_or_create(tenant_id=payload['tenant_id'])
|
||||
projects[payload['tenant_id']] = project
|
||||
|
||||
for fip in request.data['floating_ips']:
|
||||
# Get Price
|
||||
fip_price = FloatingIpsPrice.objects.first()
|
||||
if payload['tenant_id'] not in invoices:
|
||||
invoice = Invoice.objects.create(
|
||||
project=projects[payload['tenant_id']],
|
||||
start_date=month_first_day,
|
||||
state=Invoice.InvoiceState.IN_PROGRESS
|
||||
)
|
||||
invoices[payload['tenant_id']] = invoice
|
||||
|
||||
# Create new invoice floating ip
|
||||
start_date = self.parse_time(fip['start_date'])
|
||||
if start_date < month_first_day:
|
||||
start_date = month_first_day
|
||||
start_date = self.parse_time(payload['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,
|
||||
)
|
||||
payload['start_date'] = start_date
|
||||
payload['invoice'] = invoices[payload['tenant_id']]
|
||||
|
||||
for volume in request.data['volumes']:
|
||||
# Get Price
|
||||
volume_price = VolumePrice.objects.filter(volume_type_id=volume['volume_type_id']).first()
|
||||
|
||||
# Create new invoice floating ip
|
||||
start_date = self.parse_time(volume['start_date'])
|
||||
if start_date < month_first_day:
|
||||
start_date = month_first_day
|
||||
|
||||
InvoiceVolume.objects.create(
|
||||
invoice=new_invoice,
|
||||
volume_id=volume['volume_id'],
|
||||
volume_name=volume['volume_name'],
|
||||
volume_type_id=volume['volume_type_id'],
|
||||
space_allocation_gb=volume['space_allocation_gb'],
|
||||
current_state=volume['current_state'],
|
||||
start_date=start_date,
|
||||
daily_price=volume_price.daily_price,
|
||||
monthly_price=volume_price.monthly_price,
|
||||
)
|
||||
|
||||
serializer = InvoiceSerializer(new_invoice)
|
||||
|
||||
return Response(serializer.data)
|
||||
# create not accepting tenant_id, delete it
|
||||
del payload['tenant_id']
|
||||
handler.create(payload)
|
|
@ -1,22 +1,27 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from core.models import FlavorPrice, VolumePrice, FloatingIpsPrice, BillingProject, Invoice, InvoiceVolume, \
|
||||
InvoiceFloatingIp, InvoiceInstance
|
||||
InvoiceFloatingIp, InvoiceInstance, DynamicSetting
|
||||
|
||||
|
||||
@admin.register(DynamicSetting)
|
||||
class FlavorPriceAdmin(admin.ModelAdmin):
|
||||
list_display = ('key', 'value', 'type')
|
||||
|
||||
|
||||
@admin.register(FlavorPrice)
|
||||
class FlavorPriceAdmin(admin.ModelAdmin):
|
||||
list_display = ('flavor_id', 'daily_price', 'monthly_price')
|
||||
list_display = ('flavor_id', 'hourly_price', 'monthly_price')
|
||||
|
||||
|
||||
@admin.register(FloatingIpsPrice)
|
||||
class FloatingIpsPriceAdmin(admin.ModelAdmin):
|
||||
list_display = ('daily_price', 'monthly_price')
|
||||
list_display = ('hourly_price', 'monthly_price')
|
||||
|
||||
|
||||
@admin.register(VolumePrice)
|
||||
class VolumePriceAdmin(admin.ModelAdmin):
|
||||
list_display = ('volume_type_id', 'daily_price', 'monthly_price')
|
||||
list_display = ('volume_type_id', 'hourly_price', 'monthly_price')
|
||||
|
||||
|
||||
@admin.register(BillingProject)
|
||||
|
@ -26,7 +31,7 @@ class BillingProjectAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__',)
|
||||
list_display = ('__str__', 'project',)
|
||||
|
||||
|
||||
@admin.register(InvoiceInstance)
|
||||
|
|
123
core/component/base/event_handler.py
Normal file
123
core/component/base/event_handler.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import abc
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, BillingProject
|
||||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
|
||||
|
||||
class EventHandler(metaclass=abc.ABCMeta):
|
||||
def __init__(self, invoice_handler):
|
||||
self.invoice_handler: InvoiceHandler = invoice_handler
|
||||
|
||||
def get_tenant_progress_invoice(self, tenant_id):
|
||||
"""
|
||||
Get in progress invoice for specific tenant id.
|
||||
Will create new instance if active invoice not found.
|
||||
And will create new billing project if tenant id not found.
|
||||
:param tenant_id: Tenant id to get the invoice from.
|
||||
:return:
|
||||
"""
|
||||
invoice = Invoice.objects.filter(project__tenant_id=tenant_id, state=Invoice.InvoiceState.IN_PROGRESS).first()
|
||||
if not invoice:
|
||||
project = BillingProject.objects.get_or_create(tenant_id=tenant_id)
|
||||
date_today = timezone.now()
|
||||
month_first_day = date_today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
invoice = Invoice.objects.create(
|
||||
project=project,
|
||||
start_date=month_first_day,
|
||||
state=Invoice.InvoiceState.IN_PROGRESS
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
@abc.abstractmethod
|
||||
def handle(self, event_type, raw_payload):
|
||||
"""
|
||||
Handle event from the message queue
|
||||
:param event_type: The event type
|
||||
:param raw_payload: Payload inside the message
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
"""
|
||||
Clean raw payload into payload that can be accepted by invoice handler
|
||||
:param event_type: Current event type
|
||||
:param raw_payload: Raw payload from messaging queue
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@transaction.atomic
|
||||
def handle_create(self, invoice: Invoice, event_type, raw_payload):
|
||||
"""
|
||||
Create new invoice component that will be saved to current invoice.
|
||||
|
||||
You need to call this method manually from handle() if you want to use it.
|
||||
|
||||
:param invoice: The invoice that will be saved into
|
||||
:param event_type: Current event type
|
||||
:param raw_payload: Raw payload from messaging queue.
|
||||
:return:
|
||||
"""
|
||||
payload = self.clean_payload(event_type, raw_payload)
|
||||
instance = self.invoice_handler.get_active_instance(invoice, payload)
|
||||
if not instance:
|
||||
payload['invoice'] = invoice
|
||||
payload['start_date'] = timezone.now()
|
||||
|
||||
self.invoice_handler.create(payload)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@transaction.atomic
|
||||
def handle_delete(self, invoice: Invoice, event_type, raw_payload):
|
||||
"""
|
||||
Close invoice component when delete event occurred.
|
||||
|
||||
You need to call this method manually from handle() if you want to use it.
|
||||
|
||||
:param invoice: The invoice that will be saved into
|
||||
:param event_type: Current event type
|
||||
:param raw_payload: Raw payload from messaging queue.
|
||||
:return:
|
||||
"""
|
||||
payload = self.clean_payload(event_type, raw_payload)
|
||||
instance = self.invoice_handler.get_active_instance(invoice, payload)
|
||||
if instance:
|
||||
self.invoice_handler.update_and_close(instance, payload)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@transaction.atomic
|
||||
def handle_update(self, invoice: Invoice, event_type, raw_payload):
|
||||
"""
|
||||
Update invoice component when update event occurred.
|
||||
|
||||
You need to call this method manually from handle() if you want to use it.
|
||||
|
||||
:param invoice: The invoice that will be saved into
|
||||
:param event_type: Current event type
|
||||
:param raw_payload: Raw payload from messaging queue.
|
||||
:return:
|
||||
"""
|
||||
payload = self.clean_payload(event_type, raw_payload)
|
||||
instance = self.invoice_handler.get_active_instance(invoice, payload)
|
||||
|
||||
if instance:
|
||||
if self.invoice_handler.is_price_dependency_changed(instance, payload):
|
||||
self.invoice_handler.roll(instance, close_date=timezone.now(), update_payload=payload)
|
||||
return True
|
||||
|
||||
if self.invoice_handler.is_informative_changed(instance, payload):
|
||||
self.invoice_handler.update(instance, update_payload=payload)
|
||||
return True
|
||||
|
||||
return False
|
138
core/component/base/invoice_handler.py
Normal file
138
core/component/base/invoice_handler.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
import abc
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import InvoiceComponentMixin, PriceMixin
|
||||
|
||||
|
||||
class InvoiceHandler(metaclass=abc.ABCMeta):
|
||||
INVOICE_CLASS = None
|
||||
KEY_FIELD = None
|
||||
INFORMATIVE_FIELDS = []
|
||||
PRICE_DEPENDENCY_FIELDS = []
|
||||
|
||||
def create(self, payload):
|
||||
"""
|
||||
Create new invoice component
|
||||
:param payload: the data that will be created
|
||||
:return:
|
||||
"""
|
||||
price = self.get_price(payload)
|
||||
payload['hourly_price'] = price.hourly_price
|
||||
payload['monthly_price'] = price.monthly_price
|
||||
|
||||
self.INVOICE_CLASS.objects.create(**payload)
|
||||
|
||||
def roll(self, instance: InvoiceComponentMixin, close_date, update_payload=None):
|
||||
"""
|
||||
Roll current instance.
|
||||
Close current component instance and clone it
|
||||
:param instance: The instance that want to be rolled
|
||||
:param close_date: The close date of current instance
|
||||
:param update_payload: New data to update the next component instance
|
||||
:return:
|
||||
"""
|
||||
if update_payload is None:
|
||||
update_payload = {}
|
||||
|
||||
if not instance.is_closed():
|
||||
instance.close(close_date)
|
||||
|
||||
# Set primary ke to None, this will make save() to create a new row
|
||||
instance.pk = None
|
||||
|
||||
instance.start_date = instance.end_date
|
||||
instance.end_date = None
|
||||
|
||||
instance.created_at = None
|
||||
instance.updated_at = None
|
||||
|
||||
# Update the new instance without saving
|
||||
instance = self.update(instance, update_payload, save=False)
|
||||
|
||||
# Update the price
|
||||
price = self.get_price(self.get_price_dependency_from_instance(instance))
|
||||
instance.hourly_price = price.hourly_price
|
||||
instance.monthly_price = price.monthly_price
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
def update(self, instance, update_payload, save=True):
|
||||
"""
|
||||
Update instance
|
||||
:param instance: instance that will be updated
|
||||
:param update_payload: new data
|
||||
:param save: will it be saved or not
|
||||
:return:
|
||||
"""
|
||||
for key, value in update_payload.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
if save:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
def update_and_close(self, instance, payload):
|
||||
"""
|
||||
:param instance: Instance that will be closed
|
||||
:param payload: update payload
|
||||
:return:
|
||||
"""
|
||||
self.update(instance, payload, save=False)
|
||||
instance.close(timezone.now()) # Close will also save the instance
|
||||
|
||||
def is_informative_changed(self, instance, payload):
|
||||
"""
|
||||
Check whether informative field in instance is changed compared to the payload
|
||||
:param instance: the instance that will be checked
|
||||
:param payload: payload to compare
|
||||
:return:
|
||||
"""
|
||||
for informative in self.INFORMATIVE_FIELDS:
|
||||
if getattr(instance, informative) != payload[informative]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_price_dependency_changed(self, instance, payload):
|
||||
"""
|
||||
Check whether price dependency field in instance is changed compared to the payload
|
||||
:param instance: the instance that will be checked
|
||||
:param payload: payload to compare
|
||||
:return:
|
||||
"""
|
||||
for price_dependency in self.PRICE_DEPENDENCY_FIELDS:
|
||||
if getattr(instance, price_dependency) != payload[price_dependency]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_active_instance(self, invoice, payload):
|
||||
"""
|
||||
Get currently active invoice component instance.
|
||||
Filtered by invoice and key field in payload
|
||||
:param invoice: Invoice target
|
||||
:param payload: Payload to get key field, please make sure there are value for the key field inside the payload
|
||||
:return:
|
||||
"""
|
||||
kwargs = {"invoice": invoice, "end_date": None, self.KEY_FIELD: payload[self.KEY_FIELD]}
|
||||
return self.INVOICE_CLASS.objects.filter(**kwargs).first()
|
||||
|
||||
def get_price_dependency_from_instance(self, instance):
|
||||
"""
|
||||
Get payload with price dependency field extracted from instance
|
||||
:param instance: Instance to extract the price dependency
|
||||
:return:
|
||||
"""
|
||||
return {field: getattr(instance, field) for field in self.PRICE_DEPENDENCY_FIELDS}
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
"""
|
||||
Get price based on payload
|
||||
:param payload: the payload that will contain filter to get the price
|
||||
:return:
|
||||
"""
|
||||
raise NotImplementedError()
|
65
core/component/component.py
Normal file
65
core/component/component.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from core.component.image.event_handler import ImageEventHandler
|
||||
from core.component.image.invoice_handler import ImageInvoiceHandler
|
||||
from core.component.router.event_handler import RouterEventHandler
|
||||
from core.component.router.invoice_handler import RouterInvoiceHandler
|
||||
from core.component.snapshot.event_handler import SnapshotEventHandler
|
||||
from core.component.snapshot.invoice_handler import SnapshotInvoiceHandler
|
||||
from core.models import InvoiceInstance, InvoiceVolume, InvoiceFloatingIp, FlavorPrice, FloatingIpsPrice, VolumePrice, \
|
||||
RouterPrice, SnapshotPrice, InvoiceRouter, InvoiceSnapshot, ImagePrice, InvoiceImage
|
||||
from core.component.floating_ips.event_handler import FloatingIpEventHandler
|
||||
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
|
||||
from core.component.instances.event_handler import InstanceEventHandler
|
||||
from core.component.instances.invoice_handler import InstanceInvoiceHandler
|
||||
from core.component.labels import LABEL_INSTANCES, LABEL_VOLUMES, LABEL_FLOATING_IPS, LABEL_ROUTERS, LABEL_SNAPSHOTS, \
|
||||
LABEL_IMAGES
|
||||
from core.component.volume.event_handler import VolumeEventHandler
|
||||
from core.component.volume.invoice_handler import VolumeInvoiceHandler
|
||||
|
||||
"""
|
||||
Define a model that represent price for particular component
|
||||
"""
|
||||
PRICE_MODEL = {
|
||||
"flavor": FlavorPrice,
|
||||
"floating_ip": FloatingIpsPrice,
|
||||
"volume": VolumePrice,
|
||||
"router": RouterPrice,
|
||||
"snapshot": SnapshotPrice,
|
||||
"image": ImagePrice
|
||||
}
|
||||
|
||||
"""
|
||||
Define a model that represent a component in invoice.
|
||||
The label that used for the key must be able to access through [Invoice] model
|
||||
"""
|
||||
INVOICE_COMPONENT_MODEL = {
|
||||
LABEL_INSTANCES: InvoiceInstance,
|
||||
LABEL_VOLUMES: InvoiceVolume,
|
||||
LABEL_FLOATING_IPS: InvoiceFloatingIp,
|
||||
LABEL_ROUTERS: InvoiceRouter,
|
||||
LABEL_SNAPSHOTS: InvoiceSnapshot,
|
||||
LABEL_IMAGES: InvoiceImage
|
||||
}
|
||||
|
||||
"""
|
||||
Define a class that handle the event from message queue
|
||||
"""
|
||||
EVENT_HANDLER = {
|
||||
LABEL_INSTANCES: InstanceEventHandler,
|
||||
LABEL_VOLUMES: VolumeEventHandler,
|
||||
LABEL_FLOATING_IPS: FloatingIpEventHandler,
|
||||
LABEL_ROUTERS: RouterEventHandler,
|
||||
LABEL_SNAPSHOTS: SnapshotEventHandler,
|
||||
LABEL_IMAGES: ImageEventHandler
|
||||
}
|
||||
|
||||
"""
|
||||
Define an instance that handle invoice creation or update
|
||||
"""
|
||||
INVOICE_HANDLER = {
|
||||
LABEL_INSTANCES: InstanceInvoiceHandler(),
|
||||
LABEL_VOLUMES: VolumeInvoiceHandler(),
|
||||
LABEL_FLOATING_IPS: FloatingIpInvoiceHandler(),
|
||||
LABEL_ROUTERS: RouterInvoiceHandler(),
|
||||
LABEL_SNAPSHOTS: SnapshotInvoiceHandler(),
|
||||
LABEL_IMAGES: ImageInvoiceHandler()
|
||||
}
|
23
core/component/floating_ips/event_handler.py
Normal file
23
core/component/floating_ips/event_handler.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
|
||||
|
||||
|
||||
class FloatingIpEventHandler(EventHandler):
|
||||
def handle(self, event_type, raw_payload):
|
||||
if event_type == 'floatingip.create.end':
|
||||
tenant_id = raw_payload['floatingip']['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'floatingip.delete.end':
|
||||
tenant_id = raw_payload['floatingip']['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"fip_id": raw_payload['floatingip']['id'],
|
||||
"ip": raw_payload['floatingip']['floating_ip_address'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/floating_ips/invoice_handler.py
Normal file
12
core/component/floating_ips/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.models import FloatingIpsPrice, InvoiceFloatingIp, PriceMixin
|
||||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
|
||||
|
||||
class FloatingIpInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceFloatingIp
|
||||
KEY_FIELD = "fip_id"
|
||||
PRICE_DEPENDENCY_FIELDS = []
|
||||
INFORMATIVE_FIELDS = ["ip"]
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return FloatingIpsPrice.objects.first()
|
28
core/component/image/event_handler.py
Normal file
28
core/component/image/event_handler.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
|
||||
|
||||
class ImageEventHandler(EventHandler):
|
||||
def handle(self, event_type, raw_payload):
|
||||
if event_type == 'image.activate':
|
||||
tenant_id = raw_payload['owner']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'image.delete':
|
||||
tenant_id = raw_payload['owner']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'image.update':
|
||||
tenant_id = raw_payload['owner']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_update(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"image_id": raw_payload['id'],
|
||||
"space_allocation_gb": raw_payload['size'] / 1024 / 1024 / 1024,
|
||||
"name": raw_payload['name'] or raw_payload['id'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/image/invoice_handler.py
Normal file
12
core/component/image/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
from core.models import PriceMixin, InvoiceImage, ImagePrice
|
||||
|
||||
|
||||
class ImageInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceImage
|
||||
KEY_FIELD = "image_id"
|
||||
PRICE_DEPENDENCY_FIELDS = ["space_allocation_gb"]
|
||||
INFORMATIVE_FIELDS = ["name"]
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return ImagePrice.objects.first()
|
25
core/component/instances/event_handler.py
Normal file
25
core/component/instances/event_handler.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
|
||||
|
||||
class InstanceEventHandler(EventHandler):
|
||||
def handle(self, event_type, raw_payload):
|
||||
if event_type == 'compute.instance.update':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
|
||||
is_updated = self.handle_update(invoice, event_type, raw_payload)
|
||||
|
||||
if not is_updated and raw_payload['state'] == 'active':
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if not is_updated and raw_payload['state'] == 'deleted':
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"instance_id": raw_payload['instance_id'],
|
||||
"flavor_id": raw_payload['instance_flavor_id'],
|
||||
"name": raw_payload['display_name'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/instances/invoice_handler.py
Normal file
12
core/component/instances/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.models import FlavorPrice, InvoiceInstance, PriceMixin
|
||||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
|
||||
|
||||
class InstanceInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceInstance
|
||||
KEY_FIELD = "instance_id"
|
||||
PRICE_DEPENDENCY_FIELDS = ['flavor_id']
|
||||
INFORMATIVE_FIELDS = ['name']
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return FlavorPrice.objects.filter(flavor_id=payload['flavor_id']).first()
|
9
core/component/labels.py
Normal file
9
core/component/labels.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
LABEL_INSTANCES = "instances"
|
||||
LABEL_VOLUMES = "volumes"
|
||||
LABEL_FLOATING_IPS = "floating_ips"
|
||||
LABEL_ROUTERS = "routers"
|
||||
LABEL_SNAPSHOTS = "snapshots"
|
||||
LABEL_IMAGES = "images"
|
||||
|
||||
INVOICE_COMPONENT_LABELS = [LABEL_INSTANCES, LABEL_VOLUMES, LABEL_FLOATING_IPS, LABEL_ROUTERS, LABEL_SNAPSHOTS,
|
||||
LABEL_IMAGES]
|
43
core/component/router/event_handler.py
Normal file
43
core/component/router/event_handler.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
|
||||
|
||||
|
||||
class RouterEventHandler(EventHandler):
|
||||
def is_external_gateway_set(self, raw_payload):
|
||||
return raw_payload['router']['external_gateway_info'] is not None
|
||||
|
||||
def handle(self, event_type, raw_payload):
|
||||
# Case: Creating router with external gateway
|
||||
if event_type == 'router.create.end' and self.is_external_gateway_set(raw_payload):
|
||||
tenant_id = raw_payload['router']['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'router.update.end':
|
||||
tenant_id = raw_payload['router']['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
|
||||
# Handel update for existing instance
|
||||
self.handle_update(invoice, event_type, raw_payload)
|
||||
|
||||
# Case: Existing router set gateway
|
||||
if self.is_external_gateway_set(raw_payload):
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
# Case: Existing router remove gateway
|
||||
if not self.is_external_gateway_set(raw_payload):
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
# Case: Delete router
|
||||
if event_type == 'router.delete.end':
|
||||
tenant_id = raw_payload['router']['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"router_id": raw_payload['router']['id'],
|
||||
"name": raw_payload['router']['name'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/router/invoice_handler.py
Normal file
12
core/component/router/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
from core.models import PriceMixin, InvoiceRouter, RouterPrice
|
||||
|
||||
|
||||
class RouterInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceRouter
|
||||
KEY_FIELD = "router_id"
|
||||
PRICE_DEPENDENCY_FIELDS = []
|
||||
INFORMATIVE_FIELDS = ["name"]
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return RouterPrice.objects.first()
|
29
core/component/snapshot/event_handler.py
Normal file
29
core/component/snapshot/event_handler.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
from core.component.floating_ips.invoice_handler import FloatingIpInvoiceHandler
|
||||
|
||||
|
||||
class SnapshotEventHandler(EventHandler):
|
||||
def handle(self, event_type, raw_payload):
|
||||
if event_type == 'snapshot.create.end':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'snapshot.delete.end':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'snapshot.update.end':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_update(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"snapshot_id": raw_payload['snapshot_id'],
|
||||
"space_allocation_gb": raw_payload['volume_size'],
|
||||
"name": raw_payload['display_name'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/snapshot/invoice_handler.py
Normal file
12
core/component/snapshot/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
from core.models import PriceMixin, InvoiceSnapshot, SnapshotPrice
|
||||
|
||||
|
||||
class SnapshotInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceSnapshot
|
||||
KEY_FIELD = "snapshot_id"
|
||||
PRICE_DEPENDENCY_FIELDS = ["space_allocation_gb"]
|
||||
INFORMATIVE_FIELDS = ["name"]
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return SnapshotPrice.objects.first()
|
29
core/component/volume/event_handler.py
Normal file
29
core/component/volume/event_handler.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from core.component.base.event_handler import EventHandler
|
||||
|
||||
|
||||
class VolumeEventHandler(EventHandler):
|
||||
def handle(self, event_type, raw_payload):
|
||||
if event_type == 'volume.create.end':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_create(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type == 'volume.delete.end':
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_delete(invoice, event_type, raw_payload)
|
||||
|
||||
if event_type in ['volume.resize.end', 'volume.update.end', 'volume.retype']:
|
||||
tenant_id = raw_payload['tenant_id']
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
self.handle_update(invoice, event_type, raw_payload)
|
||||
|
||||
def clean_payload(self, event_type, raw_payload):
|
||||
payload = {
|
||||
"volume_id": raw_payload['volume_id'],
|
||||
"volume_type_id": raw_payload['volume_type'],
|
||||
"volume_name": raw_payload['display_name'],
|
||||
"space_allocation_gb": raw_payload['size'],
|
||||
}
|
||||
|
||||
return payload
|
12
core/component/volume/invoice_handler.py
Normal file
12
core/component/volume/invoice_handler.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from core.models import VolumePrice, InvoiceVolume, PriceMixin
|
||||
from core.component.base.invoice_handler import InvoiceHandler
|
||||
|
||||
|
||||
class VolumeInvoiceHandler(InvoiceHandler):
|
||||
INVOICE_CLASS = InvoiceVolume
|
||||
KEY_FIELD = "volume_id"
|
||||
PRICE_DEPENDENCY_FIELDS = ['volume_type_id', 'space_allocation_gb']
|
||||
INFORMATIVE_FIELDS = ['volume_name']
|
||||
|
||||
def get_price(self, payload) -> PriceMixin:
|
||||
return VolumePrice.objects.filter(volume_type_id=payload['volume_type_id']).first()
|
29
core/event_endpoint.py
Normal file
29
core/event_endpoint.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import logging
|
||||
|
||||
from oslo_messaging import NotificationResult
|
||||
|
||||
from core.component import component
|
||||
from core.utils.dynamic_setting import get_dynamic_settings, BILLING_ENABLED, get_dynamic_setting
|
||||
|
||||
LOG = logging.getLogger("rintik_notification")
|
||||
|
||||
|
||||
class EventEndpoint(object):
|
||||
def __init__(self):
|
||||
self.event_handler = [
|
||||
cls(component.INVOICE_HANDLER[label]) for label, cls in component.EVENT_HANDLER.items()
|
||||
]
|
||||
|
||||
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))
|
||||
|
||||
if not get_dynamic_setting(BILLING_ENABLED):
|
||||
return NotificationResult.HANDLED
|
||||
|
||||
# TODO: Error Handling
|
||||
for handler in self.event_handler:
|
||||
handler.handle(event_type, payload)
|
||||
|
||||
return NotificationResult.HANDLED
|
|
@ -10,7 +10,7 @@ from oslo_messaging import notify # noqa
|
|||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.notification_endpoint import NotifyEndpoint
|
||||
from core.event_endpoint import EventEndpoint
|
||||
|
||||
LOG = logging.getLogger("rintik_notification")
|
||||
|
||||
|
@ -42,7 +42,7 @@ class Command(BaseCommand):
|
|||
transport.cleanup()
|
||||
|
||||
def notify_server(self, transport, topics):
|
||||
endpoints = [NotifyEndpoint()]
|
||||
endpoints = [EventEndpoint()]
|
||||
targets = list(map(lambda t: messaging.Target(topic=t), topics))
|
||||
server = notify.get_notification_listener(
|
||||
transport,
|
|
@ -1,97 +0,0 @@
|
|||
import logging
|
||||
from typing import List
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceInstance, InvoiceFloatingIp, InvoiceVolume
|
||||
|
||||
LOG = logging.getLogger("rintik_new_invoice")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Rintik New Invoice'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.close_date = timezone.now()
|
||||
self.tax_pertentage = 0
|
||||
|
||||
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
||||
for active_invoice in active_invoices:
|
||||
self.close_active_invoice(active_invoice)
|
||||
|
||||
def close_active_invoice(self, active_invoice: Invoice):
|
||||
# Close Active instances
|
||||
active_instances: List[InvoiceInstance] = active_invoice.instances.filter(end_date=None).all()
|
||||
for instance in active_instances:
|
||||
instance.end_date = self.close_date
|
||||
instance.save()
|
||||
|
||||
# Close Active Floating
|
||||
active_ips: List[InvoiceFloatingIp] = active_invoice.floating_ips.filter(end_date=None).all()
|
||||
for ip in active_ips:
|
||||
ip.end_date = self.close_date
|
||||
ip.save()
|
||||
|
||||
# Close Active Volume
|
||||
active_volumes: List[InvoiceVolume] = active_invoice.volumes.filter(end_date=None).all()
|
||||
for volume in active_volumes:
|
||||
volume.end_date = self.close_date
|
||||
volume.save()
|
||||
|
||||
# Save current instance
|
||||
active_invoice.state = Invoice.InvoiceState.FINISHED
|
||||
active_invoice.end_date = self.close_date
|
||||
active_invoice.tax = self.tax_pertentage * active_invoice.subtotal
|
||||
active_invoice.total = active_invoice.tax + active_invoice.subtotal
|
||||
active_invoice.save()
|
||||
|
||||
# Creating new Invoice
|
||||
new_invoice = Invoice.objects.create(
|
||||
project=active_invoice.project,
|
||||
start_date=self.close_date,
|
||||
state=Invoice.InvoiceState.IN_PROGRESS
|
||||
)
|
||||
new_invoice.save()
|
||||
|
||||
# Cloning Active Instance to Continue in next invoice
|
||||
# Using the same price
|
||||
for instance in active_instances:
|
||||
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=instance.end_date,
|
||||
daily_price=instance.daily_price,
|
||||
monthly_price=instance.monthly_price,
|
||||
)
|
||||
|
||||
# Cloning Active Ips to Continue in next invoice
|
||||
# Using the same price
|
||||
for ip in active_ips:
|
||||
InvoiceFloatingIp.objects.create(
|
||||
invoice=new_invoice,
|
||||
fip_id=ip.fip_id,
|
||||
ip=ip.ip,
|
||||
current_state=ip.current_state,
|
||||
start_date=ip.end_date,
|
||||
daily_price=ip.daily_price,
|
||||
monthly_price=ip.monthly_price,
|
||||
)
|
||||
|
||||
# Cloning Active Volumes to Continue in next invoice
|
||||
# Using the same price
|
||||
for volume in active_volumes:
|
||||
InvoiceVolume.objects.create(
|
||||
invoice=new_invoice,
|
||||
volume_id=volume.volume_id,
|
||||
volume_name=volume.volume_name,
|
||||
volume_type_id=volume.volume_type_id,
|
||||
space_allocation_gb=volume.space_allocation_gb,
|
||||
current_state=volume.current_state,
|
||||
start_date=volume.end_date,
|
||||
daily_price=volume.daily_price,
|
||||
monthly_price=volume.monthly_price,
|
||||
)
|
55
core/management/commands/process_invoice.py
Normal file
55
core/management/commands/process_invoice.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
import logging
|
||||
from typing import Mapping, Dict, Iterable
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceComponentMixin
|
||||
from core.component import component, labels
|
||||
from core.utils.dynamic_setting import get_dynamic_setting, BILLING_ENABLED, INVOICE_TAX
|
||||
|
||||
LOG = logging.getLogger("rintik_new_invoice")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Rintik New Invoice'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not get_dynamic_setting(BILLING_ENABLED):
|
||||
return
|
||||
|
||||
self.close_date = timezone.now()
|
||||
self.tax_pertentage = get_dynamic_setting(INVOICE_TAX)
|
||||
|
||||
active_invoices = Invoice.objects.filter(state=Invoice.InvoiceState.IN_PROGRESS).all()
|
||||
for active_invoice in active_invoices:
|
||||
self.close_active_invoice(active_invoice)
|
||||
|
||||
def close_active_invoice(self, active_invoice: Invoice):
|
||||
active_components_map: Dict[str, Iterable[InvoiceComponentMixin]] = {}
|
||||
|
||||
for label in labels.INVOICE_COMPONENT_LABELS:
|
||||
active_components_map[label] = getattr(active_invoice, label).filter(end_date=None).all()
|
||||
|
||||
# Close Invoice Component
|
||||
for active_component in active_components_map[label]:
|
||||
active_component.close(self.close_date)
|
||||
|
||||
# Finish current invoice
|
||||
active_invoice.close(self.close_date, self.tax_pertentage)
|
||||
|
||||
# Creating new Invoice
|
||||
new_invoice = Invoice.objects.create(
|
||||
project=active_invoice.project,
|
||||
start_date=self.close_date,
|
||||
state=Invoice.InvoiceState.IN_PROGRESS
|
||||
)
|
||||
new_invoice.save()
|
||||
|
||||
# Cloning active component to continue in next invoice
|
||||
for label, active_components in active_components_map.items():
|
||||
handler = component.INVOICE_HANDLER[label]
|
||||
for active_component in active_components:
|
||||
handler.roll(active_component, self.close_date, update_payload={
|
||||
"invoice": new_invoice
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 07:05
|
||||
# Generated by Django 3.2.6 on 2021-10-10 13:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -17,112 +17,141 @@ class Migration(migrations.Migration):
|
|||
name='BillingProject',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('project_id', models.CharField(max_length=256)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('tenant_id', models.CharField(max_length=256)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FlavorPrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('flavor_id', models.CharField(max_length=256)),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('flavor_id', models.CharField(max_length=256)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FloatingIpsPrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invoice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_created=True)),
|
||||
('invoice_number', models.CharField(default=None, max_length=256, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField()),
|
||||
('due_date', models.DateTimeField(default=None, null=True)),
|
||||
('state', models.IntegerField(choices=[(1, 'In Progress'), (2, 'Waiting Payment'), (100, 'Finished')])),
|
||||
('subtotal', models.BigIntegerField(default=None, null=True)),
|
||||
('tax', models.BigIntegerField(default=None, null=True)),
|
||||
('total', models.BigIntegerField(default=None, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('state', models.IntegerField(choices=[(1, 'In Progress'), (100, 'Finished')])),
|
||||
('tax_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('tax', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('total_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('total', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.billingproject')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VolumePrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('volume_type_id', models.CharField(max_length=256)),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('volume_type_id', models.CharField(max_length=256)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceVolume',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_created=True)),
|
||||
('volume_id', models.CharField(max_length=266)),
|
||||
('volume_type_id', models.CharField(max_length=266)),
|
||||
('space_allocation_gb', models.IntegerField()),
|
||||
('current_state', models.CharField(max_length=256)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField()),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('volume_id', models.CharField(max_length=256)),
|
||||
('volume_type_id', models.CharField(max_length=256)),
|
||||
('space_allocation_gb', models.IntegerField()),
|
||||
('volume_name', models.CharField(max_length=256)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceInstance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_created=True)),
|
||||
('instance_id', models.CharField(max_length=266)),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('flavor_id', models.CharField(max_length=256)),
|
||||
('current_state', models.CharField(max_length=256)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField()),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('instance_id', models.CharField(max_length=266)),
|
||||
('flavor_id', models.CharField(max_length=256)),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceFloatingIp',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_created=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('fip_id', models.CharField(max_length=266)),
|
||||
('ip', models.CharField(max_length=256)),
|
||||
('current_state', models.CharField(max_length=256)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField()),
|
||||
('daily_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('daily_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=10)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.invoice')),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='floating_ips', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_created=True, blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='due_date',
|
||||
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='end_date',
|
||||
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='tax',
|
||||
field=models.BigIntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='total',
|
||||
field=models.BigIntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_created=True, blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='end_date',
|
||||
field=models.DateTimeField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, null=True),
|
||||
),
|
||||
]
|
25
core/migrations/0002_dynamicsetting.py
Normal file
25
core/migrations/0002_dynamicsetting.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.2.6 on 2021-10-13 16:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DynamicSetting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(db_index=True, max_length=256, unique=True)),
|
||||
('value', models.TextField()),
|
||||
('type', models.IntegerField(choices=[(1, 'Boolean'), (2, 'Int'), (3, 'Str'), (4, 'Json')])),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.6 on 2021-10-13 17:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_dynamicsetting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='space_allocation_gb',
|
||||
field=models.FloatField(),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 11:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_auto_20210828_1112'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='invoice_number',
|
||||
field=models.CharField(blank=True, default=None, max_length=256, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='subtotal',
|
||||
field=models.BigIntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 11:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_auto_20210828_1115'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='billingproject',
|
||||
old_name='project_id',
|
||||
new_name='tenant_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='invoice_number',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 3.2.6 on 2021-10-14 05:47
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_alter_invoicevolume_space_allocation_gb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RouterPrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SnapshotPrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceSnapshot',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('snapshot_id', models.CharField(max_length=256)),
|
||||
('space_allocation_gb', models.FloatField()),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snapshots', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceRouter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('router_id', models.CharField(max_length=256)),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='routers', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-30 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_auto_20210828_1137'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='due_date',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicefloatingip',
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='floating_ips', to='core.invoice'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='core.invoice'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='core.invoice'),
|
||||
),
|
||||
]
|
51
core/migrations/0005_imageprice_invoiceimage.py
Normal file
51
core/migrations/0005_imageprice_invoiceimage.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Generated by Django 3.2.6 on 2021-10-29 04:22
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_invoicerouter_invoicesnapshot_routerprice_snapshotprice'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImagePrice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('hourly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('hourly_price', djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10)),
|
||||
('monthly_price_currency', djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3)),
|
||||
('monthly_price', djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True)),
|
||||
('start_date', models.DateTimeField()),
|
||||
('end_date', models.DateTimeField(blank=True, default=None, null=True)),
|
||||
('image_id', models.CharField(max_length=256)),
|
||||
('space_allocation_gb', models.FloatField()),
|
||||
('name', models.CharField(max_length=256)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='core.invoice')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,103 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-31 04:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_auto_20210830_0931'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='subtotal',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='tax_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='total_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[('IDR', 'Indonesian Rupiah'), ('USD', 'US Dollar')], default='IDR', editable=False, max_length=3),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavorprice',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='flavorprice',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='floatingipsprice',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='floatingipsprice',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='state',
|
||||
field=models.IntegerField(choices=[(1, 'In Progress'), (100, 'Finished')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='tax',
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='total',
|
||||
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=0, default=None, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicefloatingip',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicefloatingip',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='volumeprice',
|
||||
name='daily_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='volumeprice',
|
||||
name='monthly_price',
|
||||
field=djmoney.models.fields.MoneyField(decimal_places=0, max_digits=10),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-31 04:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_auto_20210831_0400'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceinstance',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# 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,29 +0,0 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-06 13:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_auto_20210901_0644'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoicevolume',
|
||||
name='volume_name',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='volume_id',
|
||||
field=models.CharField(max_length=256),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicevolume',
|
||||
name='volume_type_id',
|
||||
field=models.CharField(max_length=256),
|
||||
),
|
||||
]
|
237
core/models.py
237
core/models.py
|
@ -1,34 +1,66 @@
|
|||
from datetime import timedelta
|
||||
import math
|
||||
|
||||
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
|
||||
|
||||
from core.component import labels
|
||||
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
||||
|
||||
class FlavorPrice(models.Model):
|
||||
|
||||
# region Dynamic Setting
|
||||
class DynamicSetting(BaseModel):
|
||||
class DataType(models.IntegerChoices):
|
||||
BOOLEAN = 1
|
||||
INT = 2
|
||||
STR = 3
|
||||
JSON = 4
|
||||
|
||||
key = models.CharField(max_length=256, unique=True, db_index=True)
|
||||
value = models.TextField()
|
||||
type = models.IntegerField(choices=DataType.choices)
|
||||
|
||||
|
||||
# end region
|
||||
|
||||
# region Pricing
|
||||
class FlavorPrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
flavor_id = models.CharField(max_length=256)
|
||||
daily_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
|
||||
|
||||
class FloatingIpsPrice(models.Model):
|
||||
daily_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
class FloatingIpsPrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
# No need for any additional field
|
||||
pass
|
||||
|
||||
|
||||
class VolumePrice(models.Model):
|
||||
class VolumePrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
volume_type_id = models.CharField(max_length=256)
|
||||
daily_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
|
||||
|
||||
class BillingProject(models.Model):
|
||||
class RouterPrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
# No need for any additional field
|
||||
pass
|
||||
|
||||
|
||||
class SnapshotPrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
# No need for any additional field
|
||||
pass
|
||||
|
||||
|
||||
class ImagePrice(BaseModel, TimestampMixin, PriceMixin):
|
||||
# No need for any additional field
|
||||
pass
|
||||
|
||||
|
||||
# end region
|
||||
|
||||
# region Invoicing
|
||||
class BillingProject(BaseModel, TimestampMixin):
|
||||
tenant_id = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
class Invoice(BaseModel, TimestampMixin):
|
||||
class InvoiceState(models.IntegerChoices):
|
||||
IN_PROGRESS = 1
|
||||
FINISHED = 100
|
||||
|
@ -39,122 +71,111 @@ class Invoice(models.Model):
|
|||
state = models.IntegerField(choices=InvoiceState.choices)
|
||||
tax = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
|
||||
total = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
|
||||
|
||||
@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 = 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
|
||||
# Need to optimize?
|
||||
# currently price is calculated on the fly, maybe need to save to db to make performance faster?
|
||||
# or using cache?
|
||||
|
||||
price = 0
|
||||
for component_relation_label in labels.INVOICE_COMPONENT_LABELS:
|
||||
relation_all_row = getattr(self, component_relation_label).all()
|
||||
price += sum(map(lambda x: x.price_charged, relation_all_row))
|
||||
|
||||
if price == 0:
|
||||
return Money(amount=price, currency=settings.DEFAULT_CURRENCY)
|
||||
|
||||
return instance_price + fip_price + volume_price
|
||||
return price
|
||||
|
||||
def close(self, date, tax_percentage):
|
||||
self.state = Invoice.InvoiceState.FINISHED
|
||||
self.end_date = date
|
||||
self.tax = tax_percentage * self.subtotal
|
||||
self.total = self.tax + self.subtotal
|
||||
self.save()
|
||||
|
||||
|
||||
class InvoiceInstance(models.Model):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='instances')
|
||||
# end region
|
||||
|
||||
# region Invoice Component
|
||||
class InvoiceInstance(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_INSTANCES)
|
||||
# Key
|
||||
instance_id = models.CharField(max_length=266)
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
# Price Dependency
|
||||
flavor_id = models.CharField(max_length=256)
|
||||
current_state = models.CharField(max_length=256)
|
||||
start_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_now_add=True, blank=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, blank=True, null=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):
|
||||
# 1. Belum 1 hari -> Kehitung 1 hari
|
||||
# 2. Perhitungan bulanan
|
||||
# 3. 1 Bulan 15 hari gimana?
|
||||
# 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
|
||||
end_date += timedelta(days=1)
|
||||
|
||||
days = (end_date - self.start_date).days
|
||||
return self.daily_price * days
|
||||
# Informative
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class InvoiceFloatingIp(models.Model):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name='floating_ips')
|
||||
class InvoiceFloatingIp(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_FLOATING_IPS)
|
||||
# Key
|
||||
fip_id = models.CharField(max_length=266)
|
||||
|
||||
# Informative
|
||||
ip = models.CharField(max_length=256)
|
||||
current_state = models.CharField(max_length=256)
|
||||
start_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_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
|
||||
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')
|
||||
class InvoiceVolume(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_VOLUMES)
|
||||
# Key
|
||||
volume_id = models.CharField(max_length=256)
|
||||
volume_name = models.CharField(max_length=256)
|
||||
|
||||
# Price Dependency
|
||||
volume_type_id = models.CharField(max_length=256)
|
||||
space_allocation_gb = models.IntegerField()
|
||||
current_state = models.CharField(max_length=256)
|
||||
start_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_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
space_allocation_gb = models.FloatField()
|
||||
|
||||
@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
|
||||
# Informative
|
||||
volume_name = models.CharField(max_length=256)
|
||||
|
||||
@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
|
||||
end_date += timedelta(days=1)
|
||||
price_without_allocation = super().price_charged
|
||||
return price_without_allocation * math.ceil(self.space_allocation_gb)
|
||||
|
||||
days = (end_date - self.start_date).days
|
||||
return self.daily_price * self.space_allocation_gb * days
|
||||
|
||||
class InvoiceRouter(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_ROUTERS)
|
||||
# Key
|
||||
router_id = models.CharField(max_length=256)
|
||||
|
||||
# Informative
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class InvoiceSnapshot(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_SNAPSHOTS)
|
||||
# Key
|
||||
snapshot_id = models.CharField(max_length=256)
|
||||
|
||||
# Price Dependency
|
||||
space_allocation_gb = models.FloatField()
|
||||
|
||||
# Informative
|
||||
name = models.CharField(max_length=256)
|
||||
|
||||
@property
|
||||
def price_charged(self):
|
||||
price_without_allocation = super().price_charged
|
||||
return price_without_allocation * math.ceil(self.space_allocation_gb)
|
||||
|
||||
class InvoiceImage(BaseModel, InvoiceComponentMixin):
|
||||
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_IMAGES)
|
||||
# Key
|
||||
image_id = models.CharField(max_length=256)
|
||||
|
||||
# Price Dependency
|
||||
space_allocation_gb = models.FloatField()
|
||||
|
||||
# Informative
|
||||
name = models.CharField(max_length=256)
|
||||
#
|
||||
# @property
|
||||
# def price_charged(self):
|
||||
# price_without_allocation = super().price_charged
|
||||
# return price_without_allocation * math.ceil(self.space_allocation_gb)
|
||||
# end region
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
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))
|
||||
|
||||
# TODO: Error Handling
|
||||
for handler in self.handlers:
|
||||
handler.handle(event_type, payload)
|
||||
|
||||
return NotificationResult.HANDLED
|
|
@ -1,75 +0,0 @@
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
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()
|
||||
|
||||
def handle(self, event_type, payload):
|
||||
if event_type == 'compute.instance.update':
|
||||
tenant_id = payload['tenant_id']
|
||||
|
||||
# Get instance invoice
|
||||
invoice = self.get_tenant_progress_invoice(tenant_id)
|
||||
if not invoice:
|
||||
return
|
||||
|
||||
# print('New Compute State: ' + str(payload['state']))
|
||||
if payload['state'] == 'active':
|
||||
self.handle_active_state(invoice, payload)
|
||||
|
||||
if payload['state'] == 'deleted':
|
||||
self.handle_delete_state(invoice, payload)
|
||||
|
||||
# TODO: Handle flavor change
|
||||
|
||||
@transaction.atomic
|
||||
def handle_active_state(self, invoice, payload):
|
||||
display_name = payload['display_name']
|
||||
instance_id = payload['instance_id']
|
||||
flavor_id = payload['instance_flavor_id']
|
||||
state = payload['state']
|
||||
|
||||
# TODO: More filter if update is implemented
|
||||
is_exists = InvoiceInstance.objects.filter(
|
||||
invoice=invoice,
|
||||
instance_id=instance_id,
|
||||
flavor_id=flavor_id
|
||||
).exists()
|
||||
|
||||
if not is_exists:
|
||||
# Get Price
|
||||
flavor_price = FlavorPrice.objects.filter(flavor_id=flavor_id).first()
|
||||
|
||||
# Create new invoice instance
|
||||
InvoiceInstance.objects.create(
|
||||
invoice=invoice,
|
||||
instance_id=instance_id,
|
||||
name=display_name,
|
||||
flavor_id=flavor_id,
|
||||
current_state=state,
|
||||
start_date=timezone.now(),
|
||||
daily_price=flavor_price.daily_price,
|
||||
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']
|
||||
state = payload['state']
|
||||
|
||||
# TODO: More filter if update is implemented
|
||||
invoice_instance = InvoiceInstance.objects.filter(
|
||||
invoice=invoice,
|
||||
instance_id=instance_id,
|
||||
flavor_id=flavor_id
|
||||
).first()
|
||||
|
||||
if invoice_instance:
|
||||
invoice_instance.end_date = timezone.now()
|
||||
invoice_instance.current_state = state
|
||||
invoice_instance.save()
|
|
@ -1,68 +0,0 @@
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceFloatingIp, FloatingIpsPrice
|
||||
|
||||
|
||||
class NetworkHandler:
|
||||
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 = InvoiceFloatingIp.objects.filter(
|
||||
invoice=invoice,
|
||||
fip_id=fip_id,
|
||||
).first()
|
||||
|
||||
if invoice_ip:
|
||||
invoice_ip.end_date = timezone.now()
|
||||
invoice_ip.current_state = 'released'
|
||||
invoice_ip.save()
|
|
@ -1,77 +0,0 @@
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Invoice, InvoiceVolume, VolumePrice
|
||||
|
||||
|
||||
class VolumeHandler:
|
||||
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 == 'volume.create.end':
|
||||
tenant_id = payload['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 == 'volume.delete.end':
|
||||
tenant_id = payload['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):
|
||||
volume_id = payload['volume_id']
|
||||
volume_type_id = payload['volume_type']
|
||||
name = payload['display_name']
|
||||
status = payload['status']
|
||||
size = payload['size']
|
||||
|
||||
# TODO: More filter if update is implemented
|
||||
is_exists = InvoiceVolume.objects.filter(
|
||||
invoice=invoice,
|
||||
volume_id=volume_id
|
||||
).exists()
|
||||
|
||||
if not is_exists:
|
||||
# Get Price
|
||||
volume_price = VolumePrice.objects.filter(volume_type_id=volume_type_id).first()
|
||||
|
||||
# Create new invoice floating ip
|
||||
InvoiceVolume.objects.create(
|
||||
invoice=invoice,
|
||||
volume_id=volume_id,
|
||||
volume_name=name,
|
||||
volume_type_id=volume_type_id,
|
||||
space_allocation_gb=size,
|
||||
current_state=status,
|
||||
start_date=timezone.now(),
|
||||
daily_price=volume_price.daily_price,
|
||||
monthly_price=volume_price.monthly_price,
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle_delete(self, invoice: Invoice, payload):
|
||||
volume_id = payload['volume_id']
|
||||
status = payload['status']
|
||||
|
||||
# TODO: More filter if update is implemented
|
||||
invoice_volume = InvoiceVolume.objects.filter(
|
||||
invoice=invoice,
|
||||
volume_id=volume_id
|
||||
).first()
|
||||
|
||||
if invoice_volume:
|
||||
invoice_volume.end_date = timezone.now()
|
||||
invoice_volume.current_state = status
|
||||
invoice_volume.save()
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
0
core/utils/__init__.py
Normal file
0
core/utils/__init__.py
Normal file
63
core/utils/dynamic_setting.py
Normal file
63
core/utils/dynamic_setting.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import json
|
||||
|
||||
from core.models import DynamicSetting
|
||||
|
||||
BILLING_ENABLED = "billing_enabled"
|
||||
INVOICE_TAX = "invoice_tax"
|
||||
|
||||
DEFAULTS = {
|
||||
BILLING_ENABLED: False,
|
||||
INVOICE_TAX: 10
|
||||
}
|
||||
|
||||
|
||||
def _get_casted_value(setting: DynamicSetting):
|
||||
if setting.type == DynamicSetting.DataType.JSON:
|
||||
return json.loads(setting.value)
|
||||
elif setting.type == DynamicSetting.DataType.BOOLEAN:
|
||||
return setting.value == "1"
|
||||
elif setting.type == DynamicSetting.DataType.INT:
|
||||
return int(setting.value)
|
||||
elif setting.type == DynamicSetting.DataType.STR:
|
||||
return setting.value
|
||||
else:
|
||||
raise ValueError("Type not supported")
|
||||
|
||||
|
||||
def get_dynamic_settings():
|
||||
result = DEFAULTS.copy()
|
||||
settings = DynamicSetting.objects.all()
|
||||
for setting in settings:
|
||||
result[setting.key] = _get_casted_value(setting)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_dynamic_setting(key):
|
||||
setting: DynamicSetting = DynamicSetting.objects.filter(key=key).first()
|
||||
if not setting:
|
||||
return DEFAULTS[key]
|
||||
|
||||
return setting.value
|
||||
|
||||
|
||||
def set_dynamic_setting(key, value):
|
||||
if type(value) is dict:
|
||||
inserted_value = json.dumps(value)
|
||||
data_type = DynamicSetting.DataType.JSON
|
||||
elif type(value) is bool:
|
||||
inserted_value = "1" if value else "0"
|
||||
data_type = DynamicSetting.DataType.BOOLEAN
|
||||
elif type(value) is int:
|
||||
inserted_value = str(value)
|
||||
data_type = DynamicSetting.DataType.INT
|
||||
elif type(value) is str:
|
||||
inserted_value = value
|
||||
data_type = DynamicSetting.DataType.STR
|
||||
else:
|
||||
raise ValueError("Type not supported")
|
||||
|
||||
DynamicSetting.objects.update_or_create(key=key, defaults={
|
||||
"value": inserted_value,
|
||||
"type": data_type
|
||||
})
|
88
core/utils/model_utils.py
Normal file
88
core/utils/model_utils.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import math
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from djmoney.models.fields import MoneyField
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TimestampMixin(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class PriceMixin(models.Model):
|
||||
hourly_price = MoneyField(max_digits=10, decimal_places=0)
|
||||
monthly_price = MoneyField(max_digits=10, decimal_places=0, default=None, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class InvoiceComponentMixin(TimestampMixin, PriceMixin):
|
||||
"""
|
||||
Storing start time for price calculation
|
||||
"""
|
||||
start_date = models.DateTimeField()
|
||||
|
||||
"""
|
||||
Storing end time of the component, when component still active it will be None.
|
||||
It will be set when component is closed or rolled
|
||||
"""
|
||||
end_date = models.DateTimeField(default=None, blank=True, null=True)
|
||||
|
||||
@property
|
||||
def adjusted_end_date(self):
|
||||
"""
|
||||
Get end date that will be used for calculation. Please use this for calculation and displaying usage duration
|
||||
Basically it just return current time if end_date is None
|
||||
end_date will be set when invoice is finished every end of the month or when invoice component is rolled
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Calculate the price to be charged to user
|
||||
"""
|
||||
end_date = self.adjusted_end_date
|
||||
if self.start_date.date().day == 1 and end_date.date().day == 1 \
|
||||
and self.start_date.date().month != end_date.date().month \
|
||||
and self.monthly_price:
|
||||
# Using monthly price
|
||||
return self.monthly_price
|
||||
|
||||
seconds_passes = (end_date - self.start_date).total_seconds()
|
||||
hour_passes = math.ceil(seconds_passes / 3600)
|
||||
|
||||
return self.hourly_price * hour_passes
|
||||
|
||||
def is_closed(self):
|
||||
"""
|
||||
Is component closed
|
||||
"""
|
||||
return self.end_date is not None
|
||||
|
||||
def close(self, date):
|
||||
"""
|
||||
Close component the component
|
||||
"""
|
||||
self.end_date = date
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -150,4 +150,5 @@ DEFAULT_CURRENCY = "IDR"
|
|||
|
||||
# Notification
|
||||
RINTIK_NOTIFICATION_URL = "rabbit://openstack:HcmHchx2wbxZYjvbZUFkA5FObioWSkUY74DCpgvB@172.10.10.10:5672/"
|
||||
RINTIK_NOTIFICATION_TOPICS = ["notifications", "cinder_notifications", "versioned_notifications"]
|
||||
RINTIK_NOTIFICATION_TOPICS = ["notifications", "cinder_notifications", "versioned_notifications"]
|
||||
RINTIK_INVOICE_TAX = 10
|
Loading…
Add table
Reference in a new issue