++work
This commit is contained in:
parent
2e74661702
commit
50fd9e1f37
2 changed files with 103 additions and 230 deletions
|
@ -20,8 +20,8 @@ class Command(BaseCommand):
|
||||||
if line_count == 0:
|
if line_count == 0:
|
||||||
line_count += 1
|
line_count += 1
|
||||||
obj, created = VATRate.objects.get_or_create(
|
obj, created = VATRate.objects.get_or_create(
|
||||||
start_date=row["start_date"],
|
starting_date=row["start_date"],
|
||||||
stop_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
ending_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
||||||
territory_codes=row["territory_codes"],
|
territory_codes=row["territory_codes"],
|
||||||
currency_code=row["currency_code"],
|
currency_code=row["currency_code"],
|
||||||
rate=row["rate"],
|
rate=row["rate"],
|
||||||
|
|
|
@ -18,7 +18,6 @@ from calendar import monthrange
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import uncloud_pay.stripe
|
import uncloud_pay.stripe
|
||||||
from uncloud_pay.helpers import beginning_of_month, end_of_month
|
|
||||||
from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES
|
from uncloud_pay import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS, COUNTRIES
|
||||||
from uncloud.models import UncloudModel, UncloudStatus
|
from uncloud.models import UncloudModel, UncloudStatus
|
||||||
|
|
||||||
|
@ -64,12 +63,9 @@ def start_after(a_date):
|
||||||
def default_payment_delay():
|
def default_payment_delay():
|
||||||
return timezone.now() + BILL_PAYMENT_DELAY
|
return timezone.now() + BILL_PAYMENT_DELAY
|
||||||
|
|
||||||
|
|
||||||
class Currency(models.TextChoices):
|
class Currency(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
We don't support months are years, because they vary in length.
|
Possible currencies to be billed
|
||||||
This is not only complicated, but also unfair to the user, as the user pays the same
|
|
||||||
amount for different durations.
|
|
||||||
"""
|
"""
|
||||||
CHF = 'CHF', _('Swiss Franc')
|
CHF = 'CHF', _('Swiss Franc')
|
||||||
EUR = 'EUR', _('Euro')
|
EUR = 'EUR', _('Euro')
|
||||||
|
@ -97,12 +93,32 @@ def get_balance_for_user(user):
|
||||||
0)
|
0)
|
||||||
return payments - bills
|
return payments - bills
|
||||||
|
|
||||||
|
###
|
||||||
|
# Stripe
|
||||||
|
|
||||||
class StripeCustomer(models.Model):
|
class StripeCustomer(models.Model):
|
||||||
owner = models.OneToOneField( get_user_model(),
|
owner = models.OneToOneField( get_user_model(),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
stripe_id = models.CharField(max_length=32)
|
stripe_id = models.CharField(max_length=32)
|
||||||
|
|
||||||
|
###
|
||||||
|
# Hosting company configuration
|
||||||
|
|
||||||
|
class HostingProvider(models.Model):
|
||||||
|
"""
|
||||||
|
A class resembling who is running this uncloud instance.
|
||||||
|
This might change over time so we allow starting/ending dates
|
||||||
|
|
||||||
|
This also defines the taxation rules
|
||||||
|
|
||||||
|
WIP.
|
||||||
|
"""
|
||||||
|
starting_date = models.DateField()
|
||||||
|
ending_date = models.DateField()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Payments and Payment Methods.
|
# Payments and Payment Methods.
|
||||||
|
|
||||||
|
@ -393,6 +409,55 @@ class Product(UncloudModel):
|
||||||
'one_time_price_per_unit': 0,
|
'one_time_price_per_unit': 0,
|
||||||
'recurring_price_per_unit': 15/1000
|
'recurring_price_per_unit': 15/1000
|
||||||
},
|
},
|
||||||
|
'additional_ipv4_address':
|
||||||
|
{ 'min': 0,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 8
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True })
|
||||||
|
|
||||||
|
obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v2",
|
||||||
|
description="A standard virtual machine",
|
||||||
|
currency=Currency.CHF,
|
||||||
|
config={
|
||||||
|
'features': {
|
||||||
|
'base':
|
||||||
|
{ 'min': 1,
|
||||||
|
'max': 1,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 1
|
||||||
|
},
|
||||||
|
'cores':
|
||||||
|
{ 'min': 1,
|
||||||
|
'max': 48,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 3
|
||||||
|
},
|
||||||
|
'ram_gb':
|
||||||
|
{ 'min': 1,
|
||||||
|
'max': 256,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 4
|
||||||
|
},
|
||||||
|
'ssd_gb':
|
||||||
|
{ 'min': 10,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 0.35
|
||||||
|
},
|
||||||
|
'hdd_gb':
|
||||||
|
{ 'min': 0,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 15/1000
|
||||||
|
},
|
||||||
|
'additional_ipv4_address':
|
||||||
|
{ 'min': 0,
|
||||||
|
'one_time_price_per_unit': 0,
|
||||||
|
'recurring_price_per_unit': 9
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -544,6 +609,8 @@ class Product(UncloudModel):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# FIXME: This logic needs to be phased out / replaced by product specific (?)
|
||||||
|
# proportions. Maybe using the RecurringPeriod table to link the possible discounts/add ups
|
||||||
|
|
||||||
if self.default_recurring_period == RecurringPeriod.PER_365D:
|
if self.default_recurring_period == RecurringPeriod.PER_365D:
|
||||||
if requested_period == RecurringPeriod.PER_365D:
|
if requested_period == RecurringPeriod.PER_365D:
|
||||||
|
@ -669,21 +736,26 @@ class Order(models.Model):
|
||||||
|
|
||||||
return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds)
|
return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds)
|
||||||
|
|
||||||
@property
|
|
||||||
def next_cancel_or_downgrade_date(self):
|
def next_cancel_or_downgrade_date(self, until_when=None):
|
||||||
"""
|
"""
|
||||||
Return the next proper ending date after n times the
|
Return the next proper ending date after n times the
|
||||||
recurring_period, where n is an integer that applies for downgrading
|
recurring_period, where n is an integer that applies for downgrading
|
||||||
or cancelling.
|
or cancelling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.recurring_period.seconds > 0:
|
if not until_when:
|
||||||
now = timezone.now()
|
until_when = timezone.now()
|
||||||
delta = now - self.starting_date
|
|
||||||
|
|
||||||
num_times = ceil(delta.total_seconds() / self.recurring_period)
|
if until_when < self.starting_date:
|
||||||
|
raise ValidationError("Cannot end before start of start of order")
|
||||||
|
|
||||||
next_date = self.starting_date + datetime.timedelta(seconds= num_times * self.recurring_period)
|
if self.recurring_period.duration_seconds > 0:
|
||||||
|
delta = until_when - self.starting_date
|
||||||
|
|
||||||
|
num_times = ceil(delta.total_seconds() / self.recurring_period.duration_seconds)
|
||||||
|
|
||||||
|
next_date = self.starting_date + datetime.timedelta(seconds=num_times * self.recurring_period.duration_seconds)
|
||||||
else:
|
else:
|
||||||
next_date = self.starting_date
|
next_date = self.starting_date
|
||||||
|
|
||||||
|
@ -694,12 +766,13 @@ class Order(models.Model):
|
||||||
Determine the ending date given a specific bill
|
Determine the ending date given a specific bill
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If the order is quit, charge the final amount (?)
|
# If the order is quit, charge the final amount / finish (????)
|
||||||
|
# Probably not a good idea -- FIXME :continue until usual
|
||||||
if self.ending_date:
|
if self.ending_date:
|
||||||
this_ending_date = self.ending_date
|
this_ending_date = self.ending_date
|
||||||
else:
|
else:
|
||||||
if self.earliest_ending_date > bill.ending_date:
|
if self.next_cancel_or_downgrade_date(bill.ending_date) > bill.ending_date:
|
||||||
this_ending_date = self.earliest_ending_date
|
this_ending_date = self.next_cancel_or_downgrade_date(bill.ending_date)
|
||||||
else:
|
else:
|
||||||
this_ending_date = bill.ending_date
|
this_ending_date = bill.ending_date
|
||||||
|
|
||||||
|
@ -796,6 +869,7 @@ class Order(models.Model):
|
||||||
def create_bill_record(self, bill):
|
def create_bill_record(self, bill):
|
||||||
br = None
|
br = None
|
||||||
|
|
||||||
|
# Note: check for != 0 not > 0, as we allow discounts to be expressed with < 0
|
||||||
if self.one_time_price != 0 and self.billrecord_set.count() == 0:
|
if self.one_time_price != 0 and self.billrecord_set.count() == 0:
|
||||||
br = BillRecord.objects.create(bill=bill,
|
br = BillRecord.objects.create(bill=bill,
|
||||||
order=self,
|
order=self,
|
||||||
|
@ -808,7 +882,6 @@ class Order(models.Model):
|
||||||
|
|
||||||
if br:
|
if br:
|
||||||
self.update_bill_record_for_recurring_order(br, bill)
|
self.update_bill_record_for_recurring_order(br, bill)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
br = self.create_new_bill_record_for_recurring_order(bill)
|
br = self.create_new_bill_record_for_recurring_order(bill)
|
||||||
|
|
||||||
|
@ -948,7 +1021,9 @@ class Bill(models.Model):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
editable=True,
|
editable=True,
|
||||||
null=False)
|
null=False)
|
||||||
|
|
||||||
# FIXME: editable=True -> is in the admin, but also editable in DRF
|
# FIXME: editable=True -> is in the admin, but also editable in DRF
|
||||||
|
# Maybe filter fields in the serializer?
|
||||||
|
|
||||||
is_final = models.BooleanField(default=False)
|
is_final = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@ -960,9 +1035,6 @@ class Bill(models.Model):
|
||||||
name='one_bill_per_month_per_user')
|
name='one_bill_per_month_per_user')
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Bill {self.owner}-{self.id}"
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""
|
"""
|
||||||
Close/finish a bill
|
Close/finish a bill
|
||||||
|
@ -1001,7 +1073,6 @@ class Bill(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_next_bill_for_user_address(cls, billing_address, ending_date=None):
|
def create_next_bill_for_user_address(cls, billing_address, ending_date=None):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create the next bill for a specific billing address of a user
|
Create the next bill for a specific billing address of a user
|
||||||
"""
|
"""
|
||||||
|
@ -1021,6 +1092,12 @@ class Bill(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create_bill(cls, billing_address, ending_date=None):
|
def get_or_create_bill(cls, billing_address, ending_date=None):
|
||||||
|
"""
|
||||||
|
Get / reuse last bill if it is not yet closed
|
||||||
|
|
||||||
|
Create bill, if there is no bill or if bill is closed.
|
||||||
|
"""
|
||||||
|
|
||||||
last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last()
|
last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last()
|
||||||
|
|
||||||
all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
|
all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
|
||||||
|
@ -1056,163 +1133,8 @@ class Bill(models.Model):
|
||||||
|
|
||||||
return bill
|
return bill
|
||||||
|
|
||||||
# @classmethod
|
def __str__(self):
|
||||||
# def create_bill_records_for_recurring_orders(cls, bill):
|
return f"Bill {self.owner}-{self.id}"
|
||||||
# """
|
|
||||||
# Create or update bill records for recurring orders for the
|
|
||||||
# given bill
|
|
||||||
# """
|
|
||||||
|
|
||||||
# owner = bill.owner
|
|
||||||
# billing_address = bill.billing_address
|
|
||||||
|
|
||||||
# all_orders = Order.objects.filter(owner=owner,
|
|
||||||
# billing_address=billing_address).order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME)
|
|
||||||
|
|
||||||
# for order in all_orders:
|
|
||||||
# bill_record_for_this_bill = BillRecord.objects.filter(bill=bill,
|
|
||||||
# order=order).first()
|
|
||||||
# if bill_record_for_this_bill:
|
|
||||||
# cls.update_bill_record_for_this_bill(bill_record_for_this_bill,
|
|
||||||
# order,
|
|
||||||
# bill)
|
|
||||||
|
|
||||||
# else:
|
|
||||||
# cls.create_new_bill_record_for_this_bill(order, bill)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_next_bill_for_user_address_old(cls,
|
|
||||||
owner,
|
|
||||||
billing_address,
|
|
||||||
ending_date=None):
|
|
||||||
"""
|
|
||||||
Filtering ideas (TBD):
|
|
||||||
If order is replaced, it should not be added anymore if it has been billed "last time"
|
|
||||||
If order has ended and finally charged, do not charge anymore
|
|
||||||
|
|
||||||
Find out the last billrecord for the order, if there is none, bill from the starting date
|
|
||||||
"""
|
|
||||||
|
|
||||||
last_bill = cls.objects.filter(owner=owner, billing_address=billing_address).order_by('id').last()
|
|
||||||
|
|
||||||
# it is important to sort orders here, as bill records will be
|
|
||||||
# created (and listed) in this order
|
|
||||||
all_orders = Order.objects.filter(owner=owner,
|
|
||||||
billing_address=billing_address).order_by('id')
|
|
||||||
first_order = all_orders.first()
|
|
||||||
|
|
||||||
bill = None
|
|
||||||
ending_date = None
|
|
||||||
|
|
||||||
# Get date & bill from previous bill, if it exists
|
|
||||||
if last_bill:
|
|
||||||
if not last_bill.is_final:
|
|
||||||
bill = last_bill
|
|
||||||
starting_date = last_bill.starting_date
|
|
||||||
|
|
||||||
# FIXME: take given (parameter) or existing ending_date?
|
|
||||||
ending_date = bill.ending_date
|
|
||||||
else:
|
|
||||||
starting_date = last_bill.ending_date + datetime.timedelta(seconds=1)
|
|
||||||
else:
|
|
||||||
if first_order:
|
|
||||||
starting_date = first_order.starting_date
|
|
||||||
else:
|
|
||||||
starting_date = timezone.now()
|
|
||||||
|
|
||||||
if not ending_date:
|
|
||||||
ending_date = end_of_month(starting_date)
|
|
||||||
|
|
||||||
if not bill:
|
|
||||||
bill = cls.objects.create(
|
|
||||||
owner=owner,
|
|
||||||
starting_date=starting_date,
|
|
||||||
ending_date=ending_date,
|
|
||||||
billing_address=billing_address)
|
|
||||||
|
|
||||||
for order in all_orders:
|
|
||||||
if order.is_one_time:
|
|
||||||
# this code should be ok, but needs to be double checked
|
|
||||||
if order.billrecord_set.count() == 0:
|
|
||||||
br = BillRecord.objects.create(bill=bill,
|
|
||||||
order=order,
|
|
||||||
starting_date=order.starting_date,
|
|
||||||
ending_date=order.ending_date)
|
|
||||||
|
|
||||||
else: # recurring orders
|
|
||||||
bill_record_for_this_bill = BillRecord.objects.filter(bill=bill,
|
|
||||||
order=order).first()
|
|
||||||
|
|
||||||
# This bill already has a bill record for the order
|
|
||||||
# We potentially need to update the ending_date if the ending_date
|
|
||||||
# of the bill changed.
|
|
||||||
if bill_record_for_this_bill:
|
|
||||||
# we may need to adjust it, but let's do this logic another time
|
|
||||||
|
|
||||||
# If the order has an ending date set, we might need to adjust the bill_record
|
|
||||||
if order.ending_date:
|
|
||||||
if bill_record_for_this_bill.ending_date != order.ending_date:
|
|
||||||
bill_record_for_this_bill.ending_date = order.ending_date
|
|
||||||
|
|
||||||
else:
|
|
||||||
# recurring, not terminated, should go until at least end of bill
|
|
||||||
if bill_record_for_this_bill.ending_date < bill.ending_date:
|
|
||||||
bill_record_for_this_bill.ending_date = bill.ending_date
|
|
||||||
|
|
||||||
|
|
||||||
bill_record_for_this_bill.save()
|
|
||||||
|
|
||||||
else: # No bill record in this bill for this order yet
|
|
||||||
# Find out when billed last time
|
|
||||||
last_bill_record = BillRecord.objects.filter(order=order).order_by('id').last()
|
|
||||||
|
|
||||||
# Default starting date
|
|
||||||
this_starting_date=order.starting_date
|
|
||||||
|
|
||||||
# Skip billing again, if we have been processed for this bill duration
|
|
||||||
if last_bill_record:
|
|
||||||
if last_bill_record.ending_date >= bill.ending_date:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If the order ended and has been fully billed - do not process it
|
|
||||||
# anymore
|
|
||||||
if order.ending_date:
|
|
||||||
if last_bill_record.ending_date == order.ending_date:
|
|
||||||
# FIXME: maybe mark order for not processing anymore?
|
|
||||||
# I imagina a boolean column, once this code is stable and
|
|
||||||
# verified
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Catch programming bugs if the last bill_record was
|
|
||||||
# created incorrectly - should never be entered!
|
|
||||||
if order.ending_date < last_bill_record.ending_date:
|
|
||||||
raise ValidationError(f"Order {order.id} ends before last bill record {last_bill_record.id}")
|
|
||||||
|
|
||||||
# Start right after last billing run
|
|
||||||
this_starting_date = last_bill_record.ending_date + datetime.timedelta(seconds=1)
|
|
||||||
|
|
||||||
|
|
||||||
# If the order is already terminated, use that date instead of bill date
|
|
||||||
if order.ending_date:
|
|
||||||
this_ending_date = order.ending_date
|
|
||||||
else:
|
|
||||||
if order.earliest_ending_date > bill.ending_date:
|
|
||||||
this_ending_date = order.earliest_ending_date
|
|
||||||
else:
|
|
||||||
# bill at maximum for this billing period
|
|
||||||
this_ending_date = bill.ending_date
|
|
||||||
|
|
||||||
# And finally create a new billrecord!
|
|
||||||
br = BillRecord.objects.create(bill=bill,
|
|
||||||
order=order,
|
|
||||||
starting_date=this_starting_date,
|
|
||||||
ending_date=this_ending_date)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return bill
|
|
||||||
|
|
||||||
|
|
||||||
class BillRecord(models.Model):
|
class BillRecord(models.Model):
|
||||||
|
@ -1261,10 +1183,10 @@ class BillRecord(models.Model):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProductToRecurringPeriod(models.Model):
|
class ProductToRecurringPeriod(models.Model):
|
||||||
"""
|
"""
|
||||||
Intermediate manytomany mapping class
|
Intermediate manytomany mapping class that allows storing the default recurring period
|
||||||
|
for a product
|
||||||
"""
|
"""
|
||||||
|
|
||||||
recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE)
|
recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE)
|
||||||
|
@ -1283,52 +1205,3 @@ class ProductToRecurringPeriod(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.product} - {self.recurring_period} (default: {self.is_default})"
|
return f"{self.product} - {self.recurring_period} (default: {self.is_default})"
|
||||||
|
|
||||||
|
|
||||||
# # Sample products included into uncloud
|
|
||||||
# class SampleOneTimeProduct(models.Model):
|
|
||||||
# """
|
|
||||||
# Products are usually more complex, but this product shows how easy
|
|
||||||
# it can be to create your own one time product.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# default_recurring_period = RecurringPeriod.ONE_TIME
|
|
||||||
|
|
||||||
# ot_price = models.IntegerField(default=5)
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def one_time_price(self):
|
|
||||||
# return self.ot_price
|
|
||||||
|
|
||||||
# class SampleRecurringProduct(models.Model):
|
|
||||||
# """
|
|
||||||
# Products are usually more complex, but this product shows how easy
|
|
||||||
# it can be to create your own recurring fee product.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# default_recurring_period = RecurringPeriod.PER_30D
|
|
||||||
|
|
||||||
# rc_price = models.IntegerField(default=10)
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def recurring_price(self):
|
|
||||||
# return self.rc_price
|
|
||||||
|
|
||||||
# class SampleRecurringProductOneTimeFee(models.Model):
|
|
||||||
# """
|
|
||||||
# Products are usually more complex, but this product shows how easy
|
|
||||||
# it can be to create your own one time + recurring fee product.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# default_recurring_period = RecurringPeriod.PER_30D
|
|
||||||
|
|
||||||
# ot_price = models.IntegerField(default=5)
|
|
||||||
# rc_price = models.IntegerField(default=10)
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def one_time_price(self):
|
|
||||||
# return self.ot_price
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def recurring_price(self):
|
|
||||||
# return self.rc_price
|
|
||||||
|
|
Loading…
Reference in a new issue