356 lines
11 KiB
Python
356 lines
11 KiB
Python
import logging
|
|
import math
|
|
import re
|
|
|
|
from django.conf import settings
|
|
from django.core.mail import send_mail
|
|
from django.db import models, transaction
|
|
from django.utils import timezone
|
|
from django.utils.html import strip_tags
|
|
from djmoney.models.fields import MoneyField
|
|
from djmoney.money import Money
|
|
|
|
from core.component import labels
|
|
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
|
|
LABEL_VOLUMES
|
|
from core.utils.date_utils import DateTimeLocalField, current_localtime
|
|
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
|
|
|
|
LOG = logging.getLogger("yuyu")
|
|
|
|
|
|
#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)
|
|
|
|
|
|
#endregion
|
|
|
|
#region Pricing
|
|
class FlavorPrice(BaseModel, TimestampMixin, PriceMixin):
|
|
flavor_id = models.CharField(max_length=256, unique=True, blank=False)
|
|
|
|
|
|
class FloatingIpsPrice(BaseModel, TimestampMixin, PriceMixin):
|
|
# No need for any additional field
|
|
pass
|
|
|
|
|
|
class VolumePrice(BaseModel, TimestampMixin, PriceMixin):
|
|
volume_type_id = models.CharField(max_length=256, unique=True, blank=False)
|
|
|
|
|
|
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
|
|
|
|
|
|
#endregion
|
|
|
|
#region Invoicing
|
|
class BillingProject(BaseModel, TimestampMixin):
|
|
tenant_id = models.CharField(max_length=256)
|
|
email_notification = models.CharField(max_length=512, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.tenant_id
|
|
|
|
|
|
class Invoice(BaseModel, TimestampMixin):
|
|
class InvoiceState(models.IntegerChoices):
|
|
IN_PROGRESS = 1
|
|
UNPAID = 2
|
|
FINISHED = 100
|
|
|
|
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
|
|
start_date = DateTimeLocalField()
|
|
end_date = DateTimeLocalField(default=None, blank=True, null=True)
|
|
finish_date = DateTimeLocalField(default=None, blank=True, null=True)
|
|
state = models.IntegerField(choices=InvoiceState.choices)
|
|
tax = MoneyField(max_digits=256, default=None, blank=True, null=True)
|
|
total = MoneyField(max_digits=256, default=None, 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?
|
|
# 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 price
|
|
|
|
@property
|
|
def total_resource(self):
|
|
total = 0
|
|
for component_relation_label in labels.INVOICE_COMPONENT_LABELS:
|
|
total += getattr(self, component_relation_label).count()
|
|
|
|
return total
|
|
|
|
@property
|
|
def state_str(self):
|
|
if self.state == Invoice.InvoiceState.IN_PROGRESS:
|
|
return 'In Progress'
|
|
|
|
if self.state == Invoice.InvoiceState.UNPAID:
|
|
return 'Unpaid'
|
|
|
|
if self.state == Invoice.InvoiceState.FINISHED:
|
|
return 'Finished'
|
|
|
|
def close(self, date, tax_percentage):
|
|
self.state = Invoice.InvoiceState.UNPAID
|
|
self.end_date = date
|
|
self.tax = tax_percentage * self.subtotal / 100
|
|
self.total = self.tax + self.subtotal
|
|
self.save()
|
|
|
|
def finish(self):
|
|
self.state = Invoice.InvoiceState.FINISHED
|
|
self.finish_date = current_localtime()
|
|
self.save()
|
|
|
|
def rollback_to_unpaid(self):
|
|
self.state = Invoice.InvoiceState.UNPAID
|
|
self.finish_date = None
|
|
self.save()
|
|
|
|
# Price for individual key
|
|
@property
|
|
def instance_price(self):
|
|
relation_all_row = getattr(self, LABEL_INSTANCES).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
@property
|
|
def volume_price(self):
|
|
relation_all_row = getattr(self, LABEL_VOLUMES).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
@property
|
|
def fip_price(self):
|
|
relation_all_row = getattr(self, LABEL_FLOATING_IPS).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
@property
|
|
def router_price(self):
|
|
relation_all_row = getattr(self, LABEL_ROUTERS).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
@property
|
|
def snapshot_price(self):
|
|
relation_all_row = getattr(self, LABEL_SNAPSHOTS).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
@property
|
|
def images_price(self):
|
|
relation_all_row = getattr(self, LABEL_IMAGES).all()
|
|
return sum(map(lambda x: x.price_charged, relation_all_row))
|
|
|
|
|
|
#endregion
|
|
|
|
#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)
|
|
|
|
# Price Dependency
|
|
flavor_id = models.CharField(max_length=256)
|
|
|
|
# Informative
|
|
name = models.CharField(max_length=256)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
# Price Dependency
|
|
volume_type_id = models.CharField(max_length=256)
|
|
space_allocation_gb = models.FloatField()
|
|
|
|
# Informative
|
|
volume_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 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)
|
|
|
|
|
|
#endregion
|
|
|
|
class Notification(BaseModel, TimestampMixin):
|
|
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE, blank=True, null=True)
|
|
title = models.CharField(max_length=256)
|
|
short_description = models.CharField(max_length=524)
|
|
content = models.TextField()
|
|
sent_status = models.BooleanField()
|
|
is_read = models.BooleanField()
|
|
|
|
def recipient(self):
|
|
if self.project and self.project.email_notification:
|
|
return self.project.email_notification
|
|
return 'Admin'
|
|
|
|
def send(self):
|
|
from core.utils.dynamic_setting import get_dynamic_setting, EMAIL_ADMIN
|
|
try:
|
|
def textify(html):
|
|
# Remove html tags and continuous whitespaces
|
|
text_only = re.sub('[ \t]+', ' ', strip_tags(html))
|
|
# Strip single spaces in the beginning of each line
|
|
return text_only.replace('\n ', '\n').strip()
|
|
|
|
recipient = get_dynamic_setting(EMAIL_ADMIN).split(",")
|
|
if self.project and self.project.email_notification:
|
|
recipient += self.project.email_notification.split(",")
|
|
|
|
send_mail(
|
|
subject=self.title,
|
|
message=textify(self.content),
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
recipient_list=recipient,
|
|
html_message=self.content,
|
|
)
|
|
self.sent_status = True
|
|
self.save()
|
|
except Exception as e:
|
|
LOG.exception('Error sending notification')
|
|
self.sent_status = False
|
|
self.save()
|
|
|
|
|
|
#region balance
|
|
class Balance(BaseModel, TimestampMixin):
|
|
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
|
|
amount = MoneyField(max_digits=256, default=0)
|
|
|
|
@classmethod
|
|
def get_balance_for_project(cls, project):
|
|
balance, created = Balance.objects.get_or_create(project=project, defaults={
|
|
"project": project
|
|
})
|
|
|
|
return balance
|
|
|
|
@transaction.atomic
|
|
def top_up(self, amount, description):
|
|
balance_transaction = BalanceTransaction(balance=self, action=BalanceTransaction.ActionType.TOP_UP,
|
|
amount=amount, description=description)
|
|
balance_transaction.save()
|
|
|
|
self.amount += amount
|
|
self.save()
|
|
|
|
return balance_transaction
|
|
|
|
@transaction.atomic
|
|
def top_down(self, amount, description):
|
|
balance_transaction = BalanceTransaction(balance=self, action=BalanceTransaction.ActionType.TOP_DOWN,
|
|
amount=amount, description=description)
|
|
balance_transaction.save()
|
|
|
|
self.amount -= amount
|
|
self.save()
|
|
|
|
return balance_transaction
|
|
|
|
def top_down_if_amount_is_good(self, amount, description) -> bool:
|
|
if self.amount >= amount:
|
|
self.top_down(amount, description)
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class BalanceTransaction(BaseModel, TimestampMixin):
|
|
class ActionType(models.TextChoices):
|
|
TOP_UP = "top_up"
|
|
TOP_DOWN = "top_down"
|
|
|
|
balance = models.ForeignKey('Balance', on_delete=models.CASCADE, blank=True, null=True)
|
|
amount = MoneyField(max_digits=256)
|
|
action = models.CharField(choices=ActionType.choices, max_length=256)
|
|
description = models.CharField(max_length=256)
|
|
|
|
#endregion
|