yuyu/core/models.py

357 lines
11 KiB
Python
Raw Permalink Normal View History

2022-09-20 12:45:09 +07:00
import logging
2021-10-29 13:00:33 +07:00
import math
2022-11-21 22:06:57 +07:00
import re
2021-08-31 11:11:51 +07:00
2021-09-01 19:29:24 +07:00
from django.conf import settings
from django.core.mail import send_mail
2023-02-10 07:05:06 +00:00
from django.db import models, transaction
2022-02-14 16:11:54 +07:00
from django.utils import timezone
from django.utils.html import strip_tags
2021-08-31 11:11:51 +07:00
from djmoney.models.fields import MoneyField
2021-09-01 19:29:24 +07:00
from djmoney.money import Money
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
from core.component import labels
from core.component.labels import LABEL_INSTANCES, LABEL_IMAGES, LABEL_SNAPSHOTS, LABEL_ROUTERS, LABEL_FLOATING_IPS, \
LABEL_VOLUMES
2021-10-29 13:00:33 +07:00
from core.utils.model_utils import BaseModel, TimestampMixin, PriceMixin, InvoiceComponentMixin
2021-08-31 11:11:51 +07:00
2022-09-20 12:45:09 +07:00
LOG = logging.getLogger("yuyu")
2021-10-29 13:00:33 +07:00
2023-09-26 22:17:13 +07:00
#region Dynamic Setting
2021-10-29 13:00:33 +07:00
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)
2023-09-26 22:17:13 +07:00
#endregion
2021-10-29 13:00:33 +07:00
2023-09-26 22:17:13 +07:00
#region Pricing
2021-10-29 13:00:33 +07:00
class FlavorPrice(BaseModel, TimestampMixin, PriceMixin):
2022-06-17 15:09:27 +07:00
flavor_id = models.CharField(max_length=256, unique=True, blank=False)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
class FloatingIpsPrice(BaseModel, TimestampMixin, PriceMixin):
# No need for any additional field
pass
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
class VolumePrice(BaseModel, TimestampMixin, PriceMixin):
2022-06-17 15:09:27 +07:00
volume_type_id = models.CharField(max_length=256, unique=True, blank=False)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
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
2023-09-26 22:17:13 +07:00
#endregion
2021-10-29 13:00:33 +07:00
2023-09-26 22:17:13 +07:00
#region Invoicing
2021-10-29 13:00:33 +07:00
class BillingProject(BaseModel, TimestampMixin):
2021-08-31 11:11:51 +07:00
tenant_id = models.CharField(max_length=256)
2022-11-21 22:06:57 +07:00
email_notification = models.CharField(max_length=512, blank=True, null=True)
2021-08-31 11:11:51 +07:00
def __str__(self):
return self.tenant_id
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
class Invoice(BaseModel, TimestampMixin):
2021-08-31 11:11:51 +07:00
class InvoiceState(models.IntegerChoices):
IN_PROGRESS = 1
2022-02-14 16:11:54 +07:00
UNPAID = 2
2021-08-31 11:11:51 +07:00
FINISHED = 100
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
start_date = models.DateTimeField()
end_date = models.DateTimeField(default=None, blank=True, null=True)
2022-02-14 16:11:54 +07:00
finish_date = models.DateTimeField(default=None, blank=True, null=True)
2021-08-31 11:11:51 +07:00
state = models.IntegerField(choices=InvoiceState.choices)
2023-09-26 22:17:13 +07:00
tax = MoneyField(max_digits=256, default=None, blank=True, null=True)
total = MoneyField(max_digits=256, default=None, blank=True, null=True)
2021-08-31 11:11:51 +07:00
@property
def subtotal(self):
2021-10-29 13:00:33 +07:00
# 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))
2021-09-01 19:29:24 +07:00
if price == 0:
return Money(amount=price, currency=settings.DEFAULT_CURRENCY)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
return price
2021-08-31 11:11:51 +07:00
2022-02-03 22:37:02 +07:00
@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'
2021-10-29 13:00:33 +07:00
def close(self, date, tax_percentage):
2022-02-14 16:11:54 +07:00
self.state = Invoice.InvoiceState.UNPAID
2021-10-29 13:00:33 +07:00
self.end_date = date
self.tax = tax_percentage * self.subtotal / 100
2021-10-29 13:00:33 +07:00
self.total = self.tax + self.subtotal
2023-02-10 07:05:06 +00:00
# TODO: Deduct balance
2021-10-29 13:00:33 +07:00
self.save()
2021-08-31 11:11:51 +07:00
2022-02-14 16:11:54 +07:00
def finish(self):
self.state = Invoice.InvoiceState.FINISHED
self.finish_date = timezone.now()
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))
2021-08-31 11:11:51 +07:00
2023-09-26 22:17:13 +07:00
#endregion
2021-08-31 11:11:51 +07:00
2023-09-26 22:17:13 +07:00
#region Invoice Component
2021-10-29 13:00:33 +07:00
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)
2021-09-01 19:29:24 +07:00
2021-10-29 13:00:33 +07:00
# Price Dependency
flavor_id = models.CharField(max_length=256)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
# Informative
name = models.CharField(max_length=256)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
class InvoiceFloatingIp(BaseModel, InvoiceComponentMixin):
invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE, related_name=labels.LABEL_FLOATING_IPS)
# Key
2021-08-31 11:11:51 +07:00
fip_id = models.CharField(max_length=266)
2021-10-29 13:00:33 +07:00
# Informative
2021-08-31 11:11:51 +07:00
ip = models.CharField(max_length=256)
2021-09-01 19:29:24 +07:00
2021-10-29 13:00:33 +07:00
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)
2021-09-01 19:29:24 +07:00
@property
def price_charged(self):
2021-10-29 13:00:33 +07:00
price_without_allocation = super().price_charged
return price_without_allocation * math.ceil(self.space_allocation_gb)
2021-09-01 19:29:24 +07:00
2021-10-29 13:00:33 +07:00
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)
2021-08-31 11:11:51 +07:00
2021-10-29 13:00:33 +07:00
# Informative
name = models.CharField(max_length=256)
2021-09-01 19:29:24 +07:00
2021-10-29 13:00:33 +07:00
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)
2021-09-01 19:29:24 +07:00
@property
def price_charged(self):
2021-10-29 13:00:33 +07:00
price_without_allocation = super().price_charged
return price_without_allocation * math.ceil(self.space_allocation_gb)
2022-02-14 16:11:54 +07:00
2021-10-29 13:00:33 +07:00
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)
2021-09-01 19:29:24 +07:00
2021-10-29 13:00:33 +07:00
# 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)
2023-09-26 22:17:13 +07:00
#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()
2022-07-20 16:17:26 +07:00
def recipient(self):
2022-11-21 22:06:57 +07:00
if self.project and self.project.email_notification:
2022-07-20 16:17:26 +07:00
return self.project.email_notification
return 'Admin'
def send(self):
from core.utils.dynamic_setting import get_dynamic_setting, EMAIL_ADMIN
2022-09-20 12:45:09 +07:00
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()
2022-11-21 22:06:57 +07:00
recipient = get_dynamic_setting(EMAIL_ADMIN).split(",")
if self.project and self.project.email_notification:
recipient += self.project.email_notification.split(",")
2022-09-20 12:45:09 +07:00
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()
2023-02-10 07:05:06 +00:00
2023-09-26 22:17:13 +07:00
#region balance
2023-02-10 07:05:06 +00:00
class Balance(BaseModel, TimestampMixin):
project = models.ForeignKey('BillingProject', on_delete=models.CASCADE)
2023-09-26 22:17:13 +07:00
amount = MoneyField(max_digits=256, default=0)
2023-02-10 07:05:06 +00:00
@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)
2023-09-26 22:17:13 +07:00
amount = MoneyField(max_digits=256)
2023-02-10 07:05:06 +00:00
action = models.CharField(choices=ActionType.choices, max_length=256)
description = models.CharField(max_length=256)
2023-09-26 22:17:13 +07:00
#endregion