++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:
|
||||
line_count += 1
|
||||
obj, created = VATRate.objects.get_or_create(
|
||||
start_date=row["start_date"],
|
||||
stop_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
||||
starting_date=row["start_date"],
|
||||
ending_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
||||
territory_codes=row["territory_codes"],
|
||||
currency_code=row["currency_code"],
|
||||
rate=row["rate"],
|
||||
|
|
|
@ -18,7 +18,6 @@ from calendar import monthrange
|
|||
from decimal import Decimal
|
||||
|
||||
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.models import UncloudModel, UncloudStatus
|
||||
|
||||
|
@ -64,12 +63,9 @@ def start_after(a_date):
|
|||
def default_payment_delay():
|
||||
return timezone.now() + BILL_PAYMENT_DELAY
|
||||
|
||||
|
||||
class Currency(models.TextChoices):
|
||||
"""
|
||||
We don't support months are years, because they vary in length.
|
||||
This is not only complicated, but also unfair to the user, as the user pays the same
|
||||
amount for different durations.
|
||||
Possible currencies to be billed
|
||||
"""
|
||||
CHF = 'CHF', _('Swiss Franc')
|
||||
EUR = 'EUR', _('Euro')
|
||||
|
@ -97,12 +93,32 @@ def get_balance_for_user(user):
|
|||
0)
|
||||
return payments - bills
|
||||
|
||||
###
|
||||
# Stripe
|
||||
|
||||
class StripeCustomer(models.Model):
|
||||
owner = models.OneToOneField( get_user_model(),
|
||||
primary_key=True,
|
||||
on_delete=models.CASCADE)
|
||||
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.
|
||||
|
||||
|
@ -393,6 +409,55 @@ class Product(UncloudModel):
|
|||
'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': 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 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)
|
||||
|
||||
@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
|
||||
recurring_period, where n is an integer that applies for downgrading
|
||||
or cancelling.
|
||||
"""
|
||||
|
||||
if self.recurring_period.seconds > 0:
|
||||
now = timezone.now()
|
||||
delta = now - self.starting_date
|
||||
if not until_when:
|
||||
until_when = timezone.now()
|
||||
|
||||
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:
|
||||
next_date = self.starting_date
|
||||
|
||||
|
@ -694,12 +766,13 @@ class Order(models.Model):
|
|||
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:
|
||||
this_ending_date = self.ending_date
|
||||
else:
|
||||
if self.earliest_ending_date > bill.ending_date:
|
||||
this_ending_date = self.earliest_ending_date
|
||||
if self.next_cancel_or_downgrade_date(bill.ending_date) > bill.ending_date:
|
||||
this_ending_date = self.next_cancel_or_downgrade_date(bill.ending_date)
|
||||
else:
|
||||
this_ending_date = bill.ending_date
|
||||
|
||||
|
@ -796,6 +869,7 @@ class Order(models.Model):
|
|||
def create_bill_record(self, bill):
|
||||
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:
|
||||
br = BillRecord.objects.create(bill=bill,
|
||||
order=self,
|
||||
|
@ -808,7 +882,6 @@ class Order(models.Model):
|
|||
|
||||
if br:
|
||||
self.update_bill_record_for_recurring_order(br, bill)
|
||||
|
||||
else:
|
||||
br = self.create_new_bill_record_for_recurring_order(bill)
|
||||
|
||||
|
@ -948,7 +1021,9 @@ class Bill(models.Model):
|
|||
on_delete=models.CASCADE,
|
||||
editable=True,
|
||||
null=False)
|
||||
|
||||
# FIXME: editable=True -> is in the admin, but also editable in DRF
|
||||
# Maybe filter fields in the serializer?
|
||||
|
||||
is_final = models.BooleanField(default=False)
|
||||
|
||||
|
@ -960,9 +1035,6 @@ class Bill(models.Model):
|
|||
name='one_bill_per_month_per_user')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Bill {self.owner}-{self.id}"
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close/finish a bill
|
||||
|
@ -1001,7 +1073,6 @@ class Bill(models.Model):
|
|||
|
||||
@classmethod
|
||||
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
|
||||
"""
|
||||
|
@ -1021,6 +1092,12 @@ class Bill(models.Model):
|
|||
|
||||
@classmethod
|
||||
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()
|
||||
|
||||
all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
|
||||
|
@ -1056,163 +1133,8 @@ class Bill(models.Model):
|
|||
|
||||
return bill
|
||||
|
||||
# @classmethod
|
||||
# def create_bill_records_for_recurring_orders(cls, bill):
|
||||
# """
|
||||
# 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
|
||||
def __str__(self):
|
||||
return f"Bill {self.owner}-{self.id}"
|
||||
|
||||
|
||||
class BillRecord(models.Model):
|
||||
|
@ -1261,10 +1183,10 @@ class BillRecord(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
@ -1283,52 +1205,3 @@ class ProductToRecurringPeriod(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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