2019-11-15 06:54:24 +00:00
|
|
|
import decimal
|
2019-04-13 11:42:04 +00:00
|
|
|
import json
|
2017-05-12 17:13:18 +00:00
|
|
|
import logging
|
2017-10-15 16:00:43 +00:00
|
|
|
import os
|
2019-07-01 17:41:49 +00:00
|
|
|
from datetime import datetime
|
2017-05-12 17:13:18 +00:00
|
|
|
|
2019-07-01 17:41:49 +00:00
|
|
|
import pytz
|
2017-10-15 16:00:43 +00:00
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
from dateutil.relativedelta import relativedelta
|
2015-05-27 10:21:30 +00:00
|
|
|
from django.db import models
|
2017-09-30 12:35:02 +00:00
|
|
|
from django.utils import timezone
|
2016-05-03 05:59:40 +00:00
|
|
|
from django.utils.functional import cached_property
|
2017-10-15 16:00:43 +00:00
|
|
|
|
2019-04-20 10:41:30 +00:00
|
|
|
from datacenterlight.models import VMPricing, VMTemplate, StripePlan
|
2017-05-04 04:19:32 +00:00
|
|
|
from membership.models import StripeCustomer, CustomUser
|
2017-08-20 19:11:46 +00:00
|
|
|
from utils.mixins import AssignPermissionsMixin
|
2018-09-23 10:34:13 +00:00
|
|
|
from utils.models import BillingAddress
|
2017-10-26 22:40:38 +00:00
|
|
|
from utils.stripe_utils import StripeUtils
|
2016-05-03 05:59:40 +00:00
|
|
|
|
2017-05-08 10:53:03 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2016-04-18 00:52:19 +00:00
|
|
|
|
2016-04-26 06:16:03 +00:00
|
|
|
|
2017-05-13 11:47:53 +00:00
|
|
|
class HostingPlan(models.Model):
|
|
|
|
disk_size = models.FloatField(default=0.0)
|
|
|
|
cpu_cores = models.FloatField(default=0.0)
|
|
|
|
memory = models.FloatField(default=0.0)
|
|
|
|
|
|
|
|
def serialize(self):
|
|
|
|
return {
|
|
|
|
'id': self.id,
|
2017-06-29 14:34:40 +00:00
|
|
|
'cpu': self.cpu_cores,
|
2017-05-13 11:47:53 +00:00
|
|
|
'memory': self.memory,
|
|
|
|
'disk_size': self.disk_size,
|
|
|
|
'price': self.price(),
|
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_serialized_configs(cls):
|
|
|
|
return [cfg.serialize()
|
|
|
|
for cfg in cls.objects.all()]
|
|
|
|
|
|
|
|
def price(self):
|
2017-05-14 00:17:48 +00:00
|
|
|
price = self.disk_size * 0.6
|
2017-05-13 11:47:53 +00:00
|
|
|
price += self.cpu_cores * 5
|
|
|
|
price += self.memory * 2
|
|
|
|
return price
|
|
|
|
|
2017-06-29 14:34:40 +00:00
|
|
|
|
2018-07-01 20:30:23 +00:00
|
|
|
class OrderDetail(AssignPermissionsMixin, models.Model):
|
2018-07-01 17:23:05 +00:00
|
|
|
vm_template = models.ForeignKey(
|
|
|
|
VMTemplate, blank=True, null=True, default=None,
|
|
|
|
on_delete=models.SET_NULL
|
|
|
|
)
|
2018-07-01 16:33:10 +00:00
|
|
|
cores = models.IntegerField(default=0)
|
|
|
|
memory = models.IntegerField(default=0)
|
|
|
|
hdd_size = models.IntegerField(default=0)
|
|
|
|
ssd_size = models.IntegerField(default=0)
|
|
|
|
|
|
|
|
def __str__(self):
|
2018-08-23 07:06:31 +00:00
|
|
|
return "Not available" if self.vm_template is None else (
|
|
|
|
"%s - %s, %s cores, %s GB RAM, %s GB SSD" % (
|
|
|
|
self.vm_template.name, self.vm_template.vm_type, self.cores,
|
|
|
|
self.memory, self.ssd_size
|
|
|
|
)
|
2018-07-01 16:33:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-09-25 00:20:31 +00:00
|
|
|
class GenericProduct(AssignPermissionsMixin, models.Model):
|
|
|
|
permissions = ('view_genericproduct',)
|
|
|
|
product_name = models.CharField(max_length=128, default="")
|
2018-10-02 07:27:20 +00:00
|
|
|
product_slug = models.SlugField(
|
2018-10-02 08:00:59 +00:00
|
|
|
unique=True,
|
2018-10-02 07:27:20 +00:00
|
|
|
help_text=(
|
2018-10-05 07:37:57 +00:00
|
|
|
'An mandatory unique slug for the product'
|
2018-10-02 07:27:20 +00:00
|
|
|
)
|
|
|
|
)
|
2018-10-03 07:58:40 +00:00
|
|
|
product_description = models.CharField(max_length=500, default="")
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
product_price = models.DecimalField(max_digits=6, decimal_places=2)
|
|
|
|
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
|
|
|
|
product_is_subscription = models.BooleanField(default=True)
|
2018-09-25 00:20:31 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.product_name
|
|
|
|
|
2019-11-15 06:28:15 +00:00
|
|
|
def get_actual_price(self, vat_rate=None):
|
2019-11-15 06:54:24 +00:00
|
|
|
VAT = decimal.Decimal(vat_rate) if vat_rate is not None else self.product_vat
|
2018-09-25 00:20:31 +00:00
|
|
|
return round(
|
2019-11-15 06:28:15 +00:00
|
|
|
self.product_price + (self.product_price * VAT), 2
|
2018-09-25 00:20:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-07-11 03:08:51 +00:00
|
|
|
class HostingOrder(AssignPermissionsMixin, models.Model):
|
2016-05-03 05:59:40 +00:00
|
|
|
ORDER_APPROVED_STATUS = 'Approved'
|
|
|
|
ORDER_DECLINED_STATUS = 'Declined'
|
|
|
|
|
2017-05-12 10:07:05 +00:00
|
|
|
vm_id = models.IntegerField(default=0)
|
2016-04-26 06:16:03 +00:00
|
|
|
customer = models.ForeignKey(StripeCustomer)
|
|
|
|
billing_address = models.ForeignKey(BillingAddress)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
approved = models.BooleanField(default=False)
|
2016-05-03 05:59:40 +00:00
|
|
|
last4 = models.CharField(max_length=4)
|
2018-08-21 12:42:01 +00:00
|
|
|
cc_brand = models.CharField(max_length=128)
|
2016-04-26 06:16:03 +00:00
|
|
|
stripe_charge_id = models.CharField(max_length=100, null=True)
|
2017-05-12 17:13:18 +00:00
|
|
|
price = models.FloatField()
|
2017-08-17 16:16:36 +00:00
|
|
|
subscription_id = models.CharField(max_length=100, null=True)
|
2018-04-16 01:23:09 +00:00
|
|
|
vm_pricing = models.ForeignKey(VMPricing)
|
2018-07-01 20:30:23 +00:00
|
|
|
order_detail = models.ForeignKey(
|
|
|
|
OrderDetail, null=True, blank=True, default=None,
|
2018-07-01 17:23:05 +00:00
|
|
|
on_delete=models.SET_NULL
|
|
|
|
)
|
2018-09-25 00:20:31 +00:00
|
|
|
generic_product = models.ForeignKey(
|
|
|
|
GenericProduct, null=True, blank=True, default=None,
|
|
|
|
on_delete=models.SET_NULL
|
|
|
|
)
|
|
|
|
generic_payment_description = models.CharField(
|
|
|
|
max_length=500, null=True
|
2018-09-23 10:34:13 +00:00
|
|
|
)
|
2016-07-11 03:08:51 +00:00
|
|
|
permissions = ('view_hostingorder',)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('view_hostingorder', 'View Hosting Order'),
|
|
|
|
)
|
|
|
|
|
2016-05-14 06:42:42 +00:00
|
|
|
def __str__(self):
|
2018-09-23 10:34:13 +00:00
|
|
|
hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - "
|
|
|
|
"Specs: {} - Price: {}").format(
|
2018-08-23 07:18:55 +00:00
|
|
|
self.id, self.vm_id, self.customer.user.email, self.created_at,
|
|
|
|
self.order_detail, self.price
|
2018-08-23 07:07:07 +00:00
|
|
|
)
|
2018-09-25 00:20:31 +00:00
|
|
|
if self.generic_product_id is not None:
|
2018-09-23 10:34:13 +00:00
|
|
|
hosting_order_str += " - Generic Payment"
|
2018-09-23 11:34:26 +00:00
|
|
|
if self.stripe_charge_id is not None:
|
|
|
|
hosting_order_str += " - One time charge"
|
|
|
|
else:
|
|
|
|
hosting_order_str += " - Recurring"
|
2018-09-23 10:34:13 +00:00
|
|
|
return hosting_order_str
|
2016-05-14 06:42:42 +00:00
|
|
|
|
2016-05-03 05:59:40 +00:00
|
|
|
@cached_property
|
|
|
|
def status(self):
|
|
|
|
return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS
|
|
|
|
|
2016-04-26 06:16:03 +00:00
|
|
|
@classmethod
|
2018-04-21 16:57:43 +00:00
|
|
|
def create(cls, price=None, vm_id=0, customer=None,
|
2018-04-16 01:36:56 +00:00
|
|
|
billing_address=None, vm_pricing=None):
|
2017-05-12 17:13:18 +00:00
|
|
|
instance = cls.objects.create(
|
|
|
|
price=price,
|
|
|
|
vm_id=vm_id,
|
|
|
|
customer=customer,
|
2018-04-16 01:36:56 +00:00
|
|
|
billing_address=billing_address,
|
|
|
|
vm_pricing=vm_pricing
|
2017-05-12 17:13:18 +00:00
|
|
|
)
|
2016-07-11 03:08:51 +00:00
|
|
|
instance.assign_permissions(customer.user)
|
2016-04-26 06:16:03 +00:00
|
|
|
return instance
|
|
|
|
|
|
|
|
def set_approved(self):
|
|
|
|
self.approved = True
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
def set_stripe_charge(self, stripe_charge):
|
|
|
|
self.stripe_charge_id = stripe_charge.id
|
2016-05-03 05:59:40 +00:00
|
|
|
self.last4 = stripe_charge.source.last4
|
|
|
|
self.cc_brand = stripe_charge.source.brand
|
2016-04-26 06:16:03 +00:00
|
|
|
self.save()
|
|
|
|
|
2017-09-29 06:42:17 +00:00
|
|
|
def set_subscription_id(self, subscription_id, cc_details):
|
2017-08-17 16:16:36 +00:00
|
|
|
"""
|
2017-08-24 06:29:38 +00:00
|
|
|
When creating a Stripe subscription, we have subscription id.
|
|
|
|
We store this in the subscription_id field.
|
2017-09-29 06:42:17 +00:00
|
|
|
This method sets the subscription id
|
|
|
|
and the last4 and credit card brands used for this order.
|
2017-08-17 16:16:36 +00:00
|
|
|
|
2017-09-29 06:42:17 +00:00
|
|
|
:param subscription_id: Stripe's subscription id
|
2017-08-24 06:29:38 +00:00
|
|
|
:param cc_details: A dict containing card details
|
|
|
|
{last4, brand}
|
2017-08-17 16:16:36 +00:00
|
|
|
:return:
|
|
|
|
"""
|
2017-09-29 06:42:17 +00:00
|
|
|
self.subscription_id = subscription_id
|
2017-08-24 07:21:55 +00:00
|
|
|
self.last4 = cc_details.get('last4')
|
|
|
|
self.cc_brand = cc_details.get('brand')
|
2017-08-17 16:16:36 +00:00
|
|
|
self.save()
|
|
|
|
|
2017-05-11 05:11:33 +00:00
|
|
|
def get_cc_data(self):
|
|
|
|
return {
|
|
|
|
'last4': self.last4,
|
|
|
|
'cc_brand': self.cc_brand,
|
|
|
|
} if self.last4 and self.cc_brand else None
|
|
|
|
|
2016-04-23 07:22:44 +00:00
|
|
|
|
2017-05-04 04:19:32 +00:00
|
|
|
class UserHostingKey(models.Model):
|
2019-07-01 17:41:49 +00:00
|
|
|
user = models.ForeignKey(CustomUser, blank=True, null=True)
|
2017-05-04 04:19:32 +00:00
|
|
|
public_key = models.TextField()
|
2017-08-17 16:16:36 +00:00
|
|
|
private_key = models.FileField(upload_to='private_keys', blank=True)
|
2017-05-04 04:19:32 +00:00
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
name = models.CharField(max_length=100)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def generate_RSA(bits=2048):
|
|
|
|
'''
|
|
|
|
Generate an RSA keypair with an exponent of 65537 in PEM format
|
|
|
|
param: bits The key length in bits
|
|
|
|
Return private key and public key
|
|
|
|
'''
|
|
|
|
new_key = RSA.generate(2048, os.urandom)
|
|
|
|
public_key = new_key.publickey().exportKey("OpenSSH")
|
|
|
|
private_key = new_key.exportKey("PEM")
|
|
|
|
return private_key, public_key
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def generate_keys(cls):
|
|
|
|
private_key, public_key = cls.generate_RSA()
|
|
|
|
# self.public_key = public_key
|
|
|
|
# self.save(update_fields=['public_key'])
|
|
|
|
return private_key, public_key
|
|
|
|
|
2019-04-28 21:13:54 +00:00
|
|
|
def delete(self,*args,**kwargs):
|
2019-05-06 06:48:26 +00:00
|
|
|
if bool(self.private_key) and os.path.isfile(self.private_key.path):
|
|
|
|
logger.debug("Removing private key {}".format(self.private_key.path))
|
2019-04-28 21:13:54 +00:00
|
|
|
os.remove(self.private_key.path)
|
2019-05-06 06:48:26 +00:00
|
|
|
else:
|
|
|
|
logger.debug("No private_key to remove")
|
2019-04-28 21:13:54 +00:00
|
|
|
|
|
|
|
super(UserHostingKey, self).delete(*args,**kwargs)
|
|
|
|
|
2017-06-29 14:34:40 +00:00
|
|
|
|
2017-05-05 12:59:11 +00:00
|
|
|
class HostingBill(AssignPermissionsMixin, models.Model):
|
|
|
|
customer = models.ForeignKey(StripeCustomer)
|
|
|
|
billing_address = models.ForeignKey(BillingAddress)
|
2017-05-06 12:44:08 +00:00
|
|
|
total_price = models.FloatField(default=0.0)
|
2017-05-05 12:59:11 +00:00
|
|
|
|
|
|
|
permissions = ('view_hostingbill',)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('view_hostingbill', 'View Hosting Bill'),
|
|
|
|
)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return "%s" % (self.customer.user.email)
|
|
|
|
|
2017-05-09 02:41:45 +00:00
|
|
|
@classmethod
|
|
|
|
def create(cls, customer=None, billing_address=None):
|
2017-08-24 06:29:38 +00:00
|
|
|
instance = cls.objects.create(customer=customer,
|
|
|
|
billing_address=billing_address)
|
2017-05-09 02:41:45 +00:00
|
|
|
return instance
|
2017-09-24 18:30:45 +00:00
|
|
|
|
|
|
|
|
2019-04-02 07:18:15 +00:00
|
|
|
class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
|
2019-04-13 11:42:04 +00:00
|
|
|
"""
|
|
|
|
Corresponds to Invoice object of Stripe
|
|
|
|
"""
|
2019-04-02 07:18:15 +00:00
|
|
|
customer = models.ForeignKey(StripeCustomer)
|
|
|
|
order = models.ForeignKey(HostingOrder)
|
2019-04-03 04:12:48 +00:00
|
|
|
created = models.DateTimeField(help_text="When the invoice was created")
|
2019-04-02 07:18:15 +00:00
|
|
|
receipt_number = models.CharField(
|
2019-04-03 04:12:48 +00:00
|
|
|
help_text="The receipt number that is generated on Stripe",
|
|
|
|
max_length=100
|
2019-04-02 07:18:15 +00:00
|
|
|
)
|
|
|
|
invoice_number = models.CharField(
|
2019-04-03 04:12:48 +00:00
|
|
|
help_text="The invoice number that is generated on Stripe",
|
|
|
|
max_length=100
|
2019-04-02 07:18:15 +00:00
|
|
|
)
|
2019-04-03 04:12:48 +00:00
|
|
|
paid_at = models.DateTimeField(help_text="Date on which the bill was paid")
|
|
|
|
period_start = models.DateTimeField()
|
|
|
|
period_end = models.DateTimeField()
|
|
|
|
billing_reason = models.CharField(max_length=25)
|
|
|
|
discount = models.PositiveIntegerField()
|
|
|
|
total = models.IntegerField()
|
|
|
|
lines_data_count = models.IntegerField()
|
|
|
|
invoice_id = models.CharField(unique=True, max_length=100)
|
2019-04-03 07:03:58 +00:00
|
|
|
lines_meta_data_csv = models.TextField(default="")
|
|
|
|
subscription_ids_csv = models.TextField(default="")
|
2019-04-02 07:18:15 +00:00
|
|
|
|
|
|
|
permissions = ('view_monthlyhostingbill',)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('view_monthlyhostingbill', 'View Monthly Hosting'),
|
|
|
|
)
|
|
|
|
|
2019-04-03 04:12:48 +00:00
|
|
|
@classmethod
|
2019-04-03 07:03:58 +00:00
|
|
|
def create(cls, args):
|
|
|
|
# Try to infer the HostingOrder from subscription id or VM_ID
|
|
|
|
if len(args['subscription_ids_csv']) > 0:
|
|
|
|
sub_ids = [sub_id.strip() for sub_id in args['subscription_ids_csv'].split(",")]
|
2019-04-20 05:22:49 +00:00
|
|
|
set_sub_ids = set(sub_ids)
|
|
|
|
if len(set_sub_ids) == 1:
|
|
|
|
# the multiple line items belong to the same subscription
|
|
|
|
sub_id = set_sub_ids.pop()
|
|
|
|
try:
|
|
|
|
args['order'] = HostingOrder.objects.get(
|
|
|
|
subscription_id=sub_id
|
|
|
|
)
|
|
|
|
except HostingOrder.DoesNotExist as dne:
|
|
|
|
logger.error("Hosting order for {} doesn't exist".format(
|
|
|
|
sub_id
|
|
|
|
))
|
|
|
|
args['order'] = None
|
2019-04-03 07:03:58 +00:00
|
|
|
else:
|
|
|
|
logger.debug(
|
|
|
|
"More than one subscriptions"
|
|
|
|
"for MonthlyHostingBill {}".format(args['invoice_id'])
|
|
|
|
)
|
2019-04-18 06:13:56 +00:00
|
|
|
logger.debug("SUB_IDS={}".format(','.join(sub_ids)))
|
2019-04-03 07:03:58 +00:00
|
|
|
logger.debug("Not importing invoices")
|
2019-04-20 05:22:49 +00:00
|
|
|
return None
|
2019-04-03 07:03:58 +00:00
|
|
|
elif len(args['lines_meta_data_csv']) > 0:
|
|
|
|
vm_ids = [vm_id.strip() for vm_id in args['lines_meta_data_csv'].split(",")]
|
2019-04-03 04:12:48 +00:00
|
|
|
if len(vm_ids) == 1:
|
2019-04-03 07:03:58 +00:00
|
|
|
args['order'] = HostingOrder.objects.get(vm_id=vm_ids[0])
|
2019-04-03 04:12:48 +00:00
|
|
|
else:
|
|
|
|
logger.debug(
|
|
|
|
"More than one VM_ID"
|
2019-04-03 07:03:58 +00:00
|
|
|
"for MonthlyHostingBill {}".format(args['invoice_id'])
|
2019-04-03 04:12:48 +00:00
|
|
|
)
|
2019-04-18 06:13:56 +00:00
|
|
|
logger.debug("VM_IDS={}".format(','.join(vm_ids)))
|
2019-04-03 07:03:58 +00:00
|
|
|
logger.debug("Not importing invoices")
|
2019-04-20 05:22:49 +00:00
|
|
|
return None
|
2019-04-03 07:03:58 +00:00
|
|
|
else:
|
|
|
|
logger.debug("Neither subscription id nor vm_id available")
|
|
|
|
logger.debug("Can't import invoice")
|
2019-04-20 05:41:07 +00:00
|
|
|
return None
|
2019-11-04 06:35:55 +00:00
|
|
|
if args['order'] is None:
|
|
|
|
logger.error(
|
|
|
|
"Order is None for {}".format(args['invoice_id']))
|
|
|
|
return None
|
2019-04-03 07:03:58 +00:00
|
|
|
instance = cls.objects.create(
|
|
|
|
created=datetime.utcfromtimestamp(
|
|
|
|
args['created']).replace(tzinfo=pytz.utc),
|
|
|
|
receipt_number=(
|
|
|
|
args['receipt_number']
|
|
|
|
if args['receipt_number'] is not None else ''
|
|
|
|
),
|
2019-04-03 19:29:49 +00:00
|
|
|
invoice_number=(
|
|
|
|
args['invoice_number']
|
|
|
|
if args['invoice_number'] is not None else ''
|
|
|
|
),
|
2019-04-03 07:03:58 +00:00
|
|
|
paid_at=datetime.utcfromtimestamp(
|
|
|
|
args['paid_at']).replace(tzinfo=pytz.utc),
|
|
|
|
period_start=datetime.utcfromtimestamp(
|
|
|
|
args['period_start']).replace(tzinfo=pytz.utc),
|
|
|
|
period_end=datetime.utcfromtimestamp(
|
|
|
|
args['period_end']).replace(tzinfo=pytz.utc),
|
|
|
|
billing_reason=args['billing_reason'],
|
|
|
|
discount=args['discount'],
|
|
|
|
total=args['total'],
|
|
|
|
lines_data_count=args['lines_data_count'],
|
|
|
|
invoice_id=args['invoice_id'],
|
|
|
|
lines_meta_data_csv=args['lines_meta_data_csv'],
|
2019-04-03 07:12:14 +00:00
|
|
|
customer=args['customer'],
|
|
|
|
order=args['order'],
|
2019-04-03 07:03:58 +00:00
|
|
|
subscription_ids_csv=args['subscription_ids_csv'],
|
|
|
|
)
|
|
|
|
|
2019-04-13 11:50:09 +00:00
|
|
|
if 'line_items' in args:
|
2019-04-13 11:42:04 +00:00
|
|
|
line_items = args['line_items']
|
|
|
|
for item in line_items:
|
2019-04-20 10:48:13 +00:00
|
|
|
stripe_plan = None
|
|
|
|
if item.type == "subscription" or item.type == "invoiceitem":
|
|
|
|
# Check stripe plan and prepare it for linking to bill item
|
|
|
|
stripe_plan_id = item.plan.id
|
|
|
|
try:
|
|
|
|
stripe_plan = StripePlan.objects.get(
|
2019-04-20 12:12:45 +00:00
|
|
|
stripe_plan_id=stripe_plan_id
|
2019-04-20 10:48:13 +00:00
|
|
|
)
|
|
|
|
except StripePlan.DoesNotExist as dne:
|
|
|
|
logger.error(
|
|
|
|
"StripePlan %s doesn't exist" % stripe_plan_id
|
|
|
|
)
|
|
|
|
if stripe_plan_id is not None:
|
|
|
|
# Create Stripe Plan because we don't have it
|
|
|
|
stripe_plan = StripePlan.objects.create(
|
|
|
|
stripe_plan_id=stripe_plan_id,
|
|
|
|
stripe_plan_name=item.plan.name,
|
|
|
|
amount=item.plan.amount,
|
|
|
|
interval=item.plan.interval
|
|
|
|
)
|
|
|
|
logger.debug("Creatd StripePlan " + stripe_plan_id)
|
2019-04-13 11:42:04 +00:00
|
|
|
line_item_instance = HostingBillLineItem.objects.create(
|
|
|
|
monthly_hosting_bill=instance,
|
|
|
|
amount=item.amount,
|
2019-04-13 12:40:20 +00:00
|
|
|
# description seems to be set to null in the Stripe
|
|
|
|
# response for an invoice
|
|
|
|
description="" if item.description is None else item.description,
|
2019-04-13 11:42:04 +00:00
|
|
|
discountable=item.discountable,
|
|
|
|
metadata=json.dumps(item.metadata),
|
|
|
|
period_start=datetime.utcfromtimestamp(item.period.start).replace(tzinfo=pytz.utc), period_end=datetime.utcfromtimestamp(item.period.end).replace(tzinfo=pytz.utc),
|
|
|
|
proration=item.proration,
|
|
|
|
quantity=item.quantity,
|
2019-04-13 12:34:32 +00:00
|
|
|
# Strange that line item does not have unit_amount but api
|
|
|
|
# states that it is present
|
|
|
|
# https://stripe.com/docs/api/invoiceitems/object#invoiceitem_object-unit_amount
|
|
|
|
# So, for the time being I set the unit_amount to 0 if not
|
|
|
|
# found in the line item
|
2019-04-20 10:48:13 +00:00
|
|
|
unit_amount=item.unit_amount if hasattr(item, "unit_amount") else 0,
|
|
|
|
stripe_plan=stripe_plan
|
2019-04-13 11:42:04 +00:00
|
|
|
)
|
2019-04-13 12:43:39 +00:00
|
|
|
line_item_instance.assign_permissions(instance.customer.user)
|
2019-04-03 07:24:25 +00:00
|
|
|
instance.assign_permissions(instance.customer.user)
|
2019-04-03 04:12:48 +00:00
|
|
|
return instance
|
|
|
|
|
2019-04-03 18:29:33 +00:00
|
|
|
def total_in_chf(self):
|
|
|
|
"""
|
|
|
|
Returns amount in chf. The total amount in this model is in cents.
|
|
|
|
Hence we multiply it by 0.01 to obtain the result
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return self.total * 0.01
|
|
|
|
|
2019-04-03 20:48:56 +00:00
|
|
|
def discount_in_chf(self):
|
|
|
|
"""
|
|
|
|
Returns discount in chf.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return self.discount * 0.01
|
|
|
|
|
|
|
|
def get_vm_id(self):
|
|
|
|
"""
|
|
|
|
Returns the VM_ID metadata if set in this MHB else returns None
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return_value = None
|
|
|
|
if len(self.lines_meta_data_csv) > 0:
|
|
|
|
vm_ids = [vm_id.strip() for vm_id in
|
|
|
|
self.lines_meta_data_csv.split(",")]
|
2019-04-20 08:00:50 +00:00
|
|
|
unique_vm_ids=set(vm_ids)
|
|
|
|
unique_vm_ids.discard("")
|
|
|
|
if len(unique_vm_ids) == 1:
|
|
|
|
vm_id = unique_vm_ids.pop()
|
|
|
|
logger.debug("Getting invoice for {}".format(vm_id))
|
|
|
|
return vm_id
|
2019-04-03 20:48:56 +00:00
|
|
|
else:
|
|
|
|
logger.debug(
|
|
|
|
"More than one VM_ID"
|
|
|
|
"for MonthlyHostingBill {}".format(self.invoice_id)
|
|
|
|
)
|
2019-04-20 08:00:50 +00:00
|
|
|
logger.debug("VM_IDS={}".format(unique_vm_ids))
|
2019-04-03 20:48:56 +00:00
|
|
|
return return_value
|
|
|
|
|
2019-04-13 13:43:27 +00:00
|
|
|
def get_period_start(self):
|
|
|
|
"""
|
|
|
|
Return the period start of the invoice for the line items
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
items = HostingBillLineItem.objects.filter(monthly_hosting_bill=self)
|
|
|
|
if len(items) > 0:
|
|
|
|
return items[0].period_start
|
|
|
|
else:
|
|
|
|
return self.period_start
|
|
|
|
|
|
|
|
def get_period_end(self):
|
|
|
|
"""
|
|
|
|
Return the period end of the invoice for the line items
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
items = HostingBillLineItem.objects.filter(monthly_hosting_bill=self)
|
|
|
|
if len(items) > 0:
|
|
|
|
return items[0].period_end
|
|
|
|
else:
|
|
|
|
return self.period_end
|
|
|
|
|
2019-04-02 07:18:15 +00:00
|
|
|
|
2019-04-13 11:42:04 +00:00
|
|
|
class HostingBillLineItem(AssignPermissionsMixin, models.Model):
|
|
|
|
"""
|
|
|
|
Corresponds to InvoiceItem object of Stripe
|
|
|
|
"""
|
2019-04-20 10:41:30 +00:00
|
|
|
monthly_hosting_bill = models.ForeignKey(MonthlyHostingBill,
|
|
|
|
on_delete=models.CASCADE)
|
|
|
|
stripe_plan = models.ForeignKey(StripePlan, null=True,
|
|
|
|
on_delete=models.CASCADE)
|
2019-10-26 05:02:49 +00:00
|
|
|
amount = models.IntegerField()
|
2019-04-13 11:42:04 +00:00
|
|
|
description = models.CharField(max_length=255)
|
|
|
|
discountable = models.BooleanField()
|
|
|
|
metadata = models.CharField(max_length=128)
|
|
|
|
period_start = models.DateTimeField()
|
|
|
|
period_end = models.DateTimeField()
|
|
|
|
proration = models.BooleanField()
|
|
|
|
quantity = models.PositiveIntegerField()
|
|
|
|
unit_amount = models.PositiveIntegerField()
|
|
|
|
permissions = ('view_hostingbilllineitem',)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('view_hostingbilllineitem', 'View Monthly Hosting Bill Line Item'),
|
|
|
|
)
|
|
|
|
|
2019-04-20 16:50:46 +00:00
|
|
|
def amount_in_chf(self):
|
|
|
|
"""
|
|
|
|
Returns amount in chf. The amount in this model is in cents (as in
|
|
|
|
Stripe). Hence we multiply it by 0.01 to obtain the result
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return self.amount * 0.01
|
|
|
|
|
|
|
|
def unit_amount_in_chf(self):
|
|
|
|
"""
|
|
|
|
Returns unit amount in chf. If its 0, we obtain it from amount and
|
|
|
|
quantity.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
if self.unit_amount == 0:
|
|
|
|
return round((self.amount / self.quantity) * 0.01, 2)
|
|
|
|
else:
|
|
|
|
return self.unit_amount * 0.01
|
|
|
|
|
|
|
|
def get_item_detail_str(self):
|
|
|
|
"""
|
|
|
|
Returns line item html string representation
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
item_detail = ""
|
2019-04-22 09:40:00 +00:00
|
|
|
# metadata is a dict; a dict with nothing has two chars at least {}
|
|
|
|
if self.metadata is not None and len(self.metadata) > 2:
|
2019-04-20 16:50:46 +00:00
|
|
|
try:
|
|
|
|
vm_dict = json.loads(self.metadata)
|
|
|
|
item_detail = "VM ID: {}<br/>".format(vm_dict["VM_ID"])
|
|
|
|
except ValueError as ve:
|
|
|
|
logger.error(
|
|
|
|
"Could not parse VM in metadata {}. Detail {}".format(
|
|
|
|
self.metadata, str(ve)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
vm_conf = StripeUtils.get_vm_config_from_stripe_id(
|
|
|
|
self.stripe_plan.stripe_plan_id
|
|
|
|
)
|
2019-04-20 17:13:34 +00:00
|
|
|
if vm_conf is not None:
|
|
|
|
item_detail += ("<b>Cores</b>: {}<br/><b>RAM</b>: {} GB<br/>"
|
|
|
|
"<b>SSD</b>: {} GB<br/>").format(
|
|
|
|
vm_conf['cores'], int(float(vm_conf['ram'])),
|
|
|
|
vm_conf['ssd']
|
|
|
|
)
|
2019-04-20 16:50:46 +00:00
|
|
|
return item_detail
|
2019-04-13 11:42:04 +00:00
|
|
|
|
2019-04-22 09:08:53 +00:00
|
|
|
|
2017-09-24 18:30:45 +00:00
|
|
|
class VMDetail(models.Model):
|
|
|
|
user = models.ForeignKey(CustomUser)
|
|
|
|
vm_id = models.IntegerField(default=0)
|
|
|
|
disk_size = models.FloatField(default=0.0)
|
|
|
|
cores = models.FloatField(default=0.0)
|
|
|
|
memory = models.FloatField(default=0.0)
|
2019-05-08 21:42:03 +00:00
|
|
|
configuration = models.CharField(default='', max_length=128)
|
2017-09-24 18:30:45 +00:00
|
|
|
ipv4 = models.TextField(default='')
|
|
|
|
ipv6 = models.TextField(default='')
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
terminated_at = models.DateTimeField(null=True)
|
2017-09-30 12:25:49 +00:00
|
|
|
|
|
|
|
def end_date(self):
|
2017-09-30 12:35:02 +00:00
|
|
|
end_date = self.terminated_at if self.terminated_at else timezone.now()
|
2017-09-30 12:38:02 +00:00
|
|
|
months = relativedelta(end_date, self.created_at).months or 1
|
2017-09-30 12:40:34 +00:00
|
|
|
end_date = self.created_at + relativedelta(months=months, days=-1)
|
2017-09-30 12:25:49 +00:00
|
|
|
return end_date
|
2017-10-15 16:00:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
class UserCardDetail(AssignPermissionsMixin, models.Model):
|
|
|
|
permissions = ('view_usercarddetail',)
|
2017-10-15 19:21:36 +00:00
|
|
|
stripe_customer = models.ForeignKey(StripeCustomer)
|
2017-10-15 16:00:43 +00:00
|
|
|
last4 = models.CharField(max_length=4)
|
2018-08-21 12:42:01 +00:00
|
|
|
brand = models.CharField(max_length=128)
|
2017-10-21 11:27:35 +00:00
|
|
|
card_id = models.CharField(max_length=100, blank=True, default='')
|
2017-10-15 19:19:13 +00:00
|
|
|
fingerprint = models.CharField(max_length=100)
|
|
|
|
exp_month = models.IntegerField(null=False)
|
|
|
|
exp_year = models.IntegerField(null=False)
|
2017-10-15 18:06:56 +00:00
|
|
|
preferred = models.BooleanField(default=False)
|
2017-10-15 16:00:43 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('view_usercarddetail', 'View User Card'),
|
2017-10-20 22:00:21 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def create(cls, stripe_customer=None, last4=None, brand=None,
|
2017-10-26 22:40:38 +00:00
|
|
|
fingerprint=None, exp_month=None, exp_year=None, card_id=None,
|
|
|
|
preferred=False):
|
2017-10-20 22:00:21 +00:00
|
|
|
instance = cls.objects.create(
|
|
|
|
stripe_customer=stripe_customer, last4=last4, brand=brand,
|
2017-10-21 11:27:35 +00:00
|
|
|
fingerprint=fingerprint, exp_month=exp_month, exp_year=exp_year,
|
2017-10-26 22:40:38 +00:00
|
|
|
card_id=card_id, preferred=preferred
|
2017-10-20 22:00:21 +00:00
|
|
|
)
|
|
|
|
instance.assign_permissions(stripe_customer.user)
|
|
|
|
return instance
|
2017-10-21 19:27:21 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_all_cards_list(cls, stripe_customer):
|
2017-10-21 19:34:54 +00:00
|
|
|
"""
|
|
|
|
Get all the cards of the given customer as a list
|
|
|
|
|
|
|
|
:param stripe_customer: The StripeCustomer object
|
|
|
|
:return: A list of all cards; an empty list if the customer object is
|
|
|
|
None
|
|
|
|
"""
|
|
|
|
cards_list = []
|
|
|
|
if stripe_customer is None:
|
|
|
|
return cards_list
|
2017-10-21 19:27:21 +00:00
|
|
|
user_card_details = UserCardDetail.objects.filter(
|
|
|
|
stripe_customer_id=stripe_customer.id
|
2017-10-28 20:32:35 +00:00
|
|
|
).order_by('-preferred', 'id')
|
2017-10-21 19:27:21 +00:00
|
|
|
for card in user_card_details:
|
|
|
|
cards_list.append({
|
2017-10-28 18:45:00 +00:00
|
|
|
'last4': card.last4, 'brand': card.brand, 'id': card.id,
|
2019-07-09 13:17:54 +00:00
|
|
|
'exp_year': card.exp_year,
|
2019-07-09 13:33:09 +00:00
|
|
|
'exp_month': '{:02d}'.format(card.exp_month),
|
2017-10-28 18:45:00 +00:00
|
|
|
'preferred': card.preferred
|
2017-10-21 19:27:21 +00:00
|
|
|
})
|
|
|
|
return cards_list
|
2017-10-26 15:56:12 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_or_create_user_card_detail(cls, stripe_customer, card_details):
|
|
|
|
"""
|
|
|
|
A method that checks if a UserCardDetail object exists already
|
|
|
|
based upon the given card_details and creates it for the given
|
|
|
|
customer if it does not exist. It returns the UserCardDetail object
|
|
|
|
matching the given card_details if it exists.
|
|
|
|
|
|
|
|
:param stripe_customer: The given StripeCustomer object to whom the
|
|
|
|
card object should belong to
|
|
|
|
:param card_details: A dictionary identifying a given card
|
|
|
|
:return: UserCardDetail object
|
|
|
|
"""
|
|
|
|
try:
|
2018-07-03 21:23:02 +00:00
|
|
|
if ('fingerprint' in card_details and 'exp_month' in card_details
|
|
|
|
and 'exp_year' in card_details):
|
|
|
|
card_detail = UserCardDetail.objects.get(
|
|
|
|
stripe_customer=stripe_customer,
|
|
|
|
fingerprint=card_details['fingerprint'],
|
|
|
|
exp_month=card_details['exp_month'],
|
|
|
|
exp_year=card_details['exp_year']
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise UserCardDetail.DoesNotExist()
|
2017-10-26 15:56:12 +00:00
|
|
|
except UserCardDetail.DoesNotExist:
|
2017-10-28 13:26:15 +00:00
|
|
|
preferred = False
|
|
|
|
if 'preferred' in card_details:
|
|
|
|
preferred = card_details['preferred']
|
2017-10-26 15:56:12 +00:00
|
|
|
card_detail = UserCardDetail.create(
|
|
|
|
stripe_customer=stripe_customer,
|
|
|
|
last4=card_details['last4'],
|
|
|
|
brand=card_details['brand'],
|
|
|
|
fingerprint=card_details['fingerprint'],
|
|
|
|
exp_month=card_details['exp_month'],
|
|
|
|
exp_year=card_details['exp_year'],
|
2017-10-28 13:26:15 +00:00
|
|
|
card_id=card_details['card_id'],
|
|
|
|
preferred=preferred
|
2017-10-26 15:56:12 +00:00
|
|
|
)
|
|
|
|
return card_detail
|
2017-10-26 22:40:38 +00:00
|
|
|
|
2017-10-28 20:18:54 +00:00
|
|
|
@staticmethod
|
|
|
|
def set_default_card(stripe_api_cus_id, stripe_source_id):
|
2017-10-26 22:40:38 +00:00
|
|
|
"""
|
|
|
|
Sets the given stripe source as the default source for the given
|
|
|
|
Stripe customer
|
|
|
|
:param stripe_api_cus_id: Stripe customer id
|
|
|
|
:param stripe_source_id: The Stripe source id
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
stripe_utils = StripeUtils()
|
|
|
|
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
|
|
|
cu = cus_response['response_object']
|
|
|
|
cu.default_source = stripe_source_id
|
|
|
|
cu.save()
|
2017-10-28 20:18:54 +00:00
|
|
|
UserCardDetail.save_default_card_local(
|
|
|
|
stripe_api_cus_id, stripe_source_id
|
|
|
|
)
|
2017-10-26 22:40:38 +00:00
|
|
|
|
2017-10-28 20:18:54 +00:00
|
|
|
@staticmethod
|
|
|
|
def set_default_card_from_stripe(stripe_api_cus_id):
|
2017-10-26 22:40:38 +00:00
|
|
|
stripe_utils = StripeUtils()
|
|
|
|
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
|
|
|
cu = cus_response['response_object']
|
|
|
|
default_source = cu.default_source
|
2017-10-28 13:14:42 +00:00
|
|
|
if default_source is not None:
|
2017-10-28 20:18:54 +00:00
|
|
|
UserCardDetail.save_default_card_local(
|
|
|
|
stripe_api_cus_id, default_source
|
|
|
|
)
|
2017-10-26 22:40:38 +00:00
|
|
|
|
2017-10-28 20:18:54 +00:00
|
|
|
@staticmethod
|
|
|
|
def save_default_card_local(stripe_api_cus_id, card_id):
|
2017-10-26 22:40:38 +00:00
|
|
|
stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id)
|
|
|
|
user_card_detail = UserCardDetail.objects.get(
|
|
|
|
stripe_customer=stripe_cust, card_id=card_id
|
|
|
|
)
|
2017-10-28 14:47:10 +00:00
|
|
|
for card in stripe_cust.usercarddetail_set.all():
|
|
|
|
card.preferred = False
|
|
|
|
card.save()
|
2017-10-26 22:40:38 +00:00
|
|
|
user_card_detail.preferred = True
|
2017-10-28 14:47:10 +00:00
|
|
|
user_card_detail.save()
|
2017-10-28 16:28:14 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2017-10-29 20:31:11 +00:00
|
|
|
def get_user_card_details(stripe_customer, card_details):
|
2017-10-28 16:28:14 +00:00
|
|
|
"""
|
|
|
|
A utility function to check whether a StripeCustomer is already
|
|
|
|
associated with the card having given details
|
2017-10-29 20:31:11 +00:00
|
|
|
|
2017-10-28 16:28:14 +00:00
|
|
|
:param stripe_customer:
|
|
|
|
:param card_details:
|
2018-07-05 07:58:57 +00:00
|
|
|
:return: The UserCardDetails object if it exists, None otherwise
|
2017-10-28 16:28:14 +00:00
|
|
|
"""
|
|
|
|
try:
|
2017-10-29 20:31:11 +00:00
|
|
|
ucd = UserCardDetail.objects.get(
|
2017-10-28 16:28:14 +00:00
|
|
|
stripe_customer=stripe_customer,
|
|
|
|
fingerprint=card_details['fingerprint'],
|
|
|
|
exp_month=card_details['exp_month'],
|
|
|
|
exp_year=card_details['exp_year']
|
|
|
|
)
|
2017-10-29 20:31:11 +00:00
|
|
|
return ucd
|
2017-10-28 16:28:14 +00:00
|
|
|
except UserCardDetail.DoesNotExist:
|
2018-07-05 07:58:57 +00:00
|
|
|
return None
|
2019-11-15 05:31:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
class VATRates(AssignPermissionsMixin, models.Model):
|
|
|
|
start_date = models.DateField(blank=True, null=True)
|
|
|
|
stop_date = models.DateField(blank=True, null=True)
|
|
|
|
territory_codes = models.TextField(blank=True, default='')
|
|
|
|
currency_code = models.CharField(max_length=10)
|
|
|
|
rate = models.FloatField()
|
|
|
|
rate_type = models.TextField(blank=True, default='')
|
|
|
|
description = models.TextField(blank=True, default='')
|