Implement the whole billing logic
The major part has been written!
This commit is contained in:
parent
d7c0c40926
commit
e169b8c1d1
1 changed files with 133 additions and 97 deletions
|
@ -308,17 +308,28 @@ class Order(models.Model):
|
||||||
|
|
||||||
replaces = models.ForeignKey('self',
|
replaces = models.ForeignKey('self',
|
||||||
related_name='replaced_by',
|
related_name='replaced_by',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.CASCADE,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
|
|
||||||
depends_on = models.ForeignKey('self',
|
depends_on = models.ForeignKey('self',
|
||||||
related_name='parent_of',
|
related_name='parent_of',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.CASCADE,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earliest_ending_date(self):
|
||||||
|
"""
|
||||||
|
Recurring orders cannot end before finishing at least one recurring period.
|
||||||
|
|
||||||
|
One time orders have a recurring period of 0, so this work universally
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.starting_date + timedelta(seconds=self.recurring_period)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def count_billed(self):
|
def count_billed(self):
|
||||||
"""
|
"""
|
||||||
|
@ -328,14 +339,6 @@ class Order(models.Model):
|
||||||
|
|
||||||
return sum([ br.quantity for br in self.bill_records.all() ])
|
return sum([ br.quantity for br in self.bill_records.all() ])
|
||||||
|
|
||||||
|
|
||||||
def active_before(self, ending_date):
|
|
||||||
# Was this order started before the specified ending date?
|
|
||||||
if self.starting_date <= ending_date:
|
|
||||||
if self.ending_date:
|
|
||||||
if self.ending_date > ending_date:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recurring(self):
|
def is_recurring(self):
|
||||||
return not self.recurring_period == RecurringPeriod.ONE_TIME
|
return not self.recurring_period == RecurringPeriod.ONE_TIME
|
||||||
|
@ -344,37 +347,46 @@ class Order(models.Model):
|
||||||
def is_one_time(self):
|
def is_one_time(self):
|
||||||
return not self.is_recurring
|
return not self.is_recurring
|
||||||
|
|
||||||
@property
|
|
||||||
def is_terminated(self):
|
|
||||||
return self.ending_date != None and self.ending_date < timezone.now()
|
|
||||||
|
|
||||||
def is_terminated_at(self, a_date):
|
|
||||||
return self.ending_date != None and self.ending_date < timezone.now()
|
|
||||||
|
|
||||||
def terminate(self):
|
|
||||||
if not self.is_terminated:
|
|
||||||
self.ending_date = timezone.now()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
# Trigger initial bill generation at order creation.
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.ending_date and self.ending_date < self.starting_date:
|
if self.ending_date and self.ending_date < self.starting_date:
|
||||||
raise ValidationError("End date cannot be before starting date")
|
raise ValidationError("End date cannot be before starting date")
|
||||||
|
|
||||||
|
if self.ending_date and self.ending_date < self.earliest_ending_date:
|
||||||
|
raise ValidationError("Ending date is before minimum duration (starting_date + recurring period)")
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def generate_initial_bill(self):
|
|
||||||
return Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def records(self):
|
|
||||||
return OrderRecord.objects.filter(order=self)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Order {self.owner}-{self.id}"
|
return f"{self.description} (order {self.id})"
|
||||||
|
|
||||||
|
|
||||||
|
# def active_before(self, ending_date):
|
||||||
|
# # Was this order started before the specified ending date?
|
||||||
|
# if self.starting_date <= ending_date:
|
||||||
|
# if self.ending_date:
|
||||||
|
# if self.ending_date > ending_date:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
# Termination needs to be verified, maybe also include checking depending orders
|
||||||
|
# @property
|
||||||
|
# def is_terminated_now(self):
|
||||||
|
# return self.is_terminated_at(timezone.now())
|
||||||
|
|
||||||
|
# def is_terminated_at(self, a_date):
|
||||||
|
# return self.ending_date != None and self.ending_date <= a_date
|
||||||
|
|
||||||
|
# def terminate(self):
|
||||||
|
# if not self.is_terminated:
|
||||||
|
# self.ending_date = timezone.now()
|
||||||
|
# self.save()
|
||||||
|
|
||||||
|
|
||||||
class Bill(models.Model):
|
class Bill(models.Model):
|
||||||
|
"""
|
||||||
|
A bill is a representation of usage at a specific time
|
||||||
|
"""
|
||||||
owner = models.ForeignKey(get_user_model(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.CASCADE)
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
@ -382,6 +394,7 @@ class Bill(models.Model):
|
||||||
starting_date = models.DateTimeField(default=start_of_this_month)
|
starting_date = models.DateTimeField(default=start_of_this_month)
|
||||||
ending_date = models.DateTimeField()
|
ending_date = models.DateTimeField()
|
||||||
due_date = models.DateField(default=default_payment_delay)
|
due_date = models.DateField(default=default_payment_delay)
|
||||||
|
|
||||||
is_final = models.BooleanField(default=False)
|
is_final = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -395,6 +408,11 @@ class Bill(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Bill {self.owner}-{self.id}"
|
return f"Bill {self.owner}-{self.id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def billing_address(self):
|
||||||
|
pass
|
||||||
|
# if self.order_set.all
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sum(self):
|
def sum(self):
|
||||||
bill_records = BillRecord.objects.filter(bill=self)
|
bill_records = BillRecord.objects.filter(bill=self)
|
||||||
|
@ -402,8 +420,11 @@ class Bill(models.Model):
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_next_bill_for_user(cls, owner):
|
def create_next_bill_for_user(cls, owner, ending_date=None):
|
||||||
last_bill = cls.objects.filter(owner=owner).order_by('id').last()
|
last_bill = cls.objects.filter(owner=owner).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).order_by('id')
|
all_orders = Order.objects.filter(owner=owner).order_by('id')
|
||||||
first_order = all_orders.first()
|
first_order = all_orders.first()
|
||||||
|
|
||||||
|
@ -424,13 +445,10 @@ class Bill(models.Model):
|
||||||
else:
|
else:
|
||||||
starting_date = timezone.now()
|
starting_date = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
if not ending_date:
|
if not ending_date:
|
||||||
ending_date = end_of_month(starting_date)
|
ending_date = end_of_month(starting_date)
|
||||||
|
|
||||||
# create new bill, if previous is closed/does not exist
|
|
||||||
if not bill:
|
if not bill:
|
||||||
|
|
||||||
bill = cls.objects.create(
|
bill = cls.objects.create(
|
||||||
owner=owner,
|
owner=owner,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
|
@ -438,24 +456,96 @@ class Bill(models.Model):
|
||||||
|
|
||||||
for order in all_orders:
|
for order in all_orders:
|
||||||
if order.is_one_time:
|
if order.is_one_time:
|
||||||
|
# this code should be ok, but needs to be double checked
|
||||||
if order.billrecord_set.count() == 0:
|
if order.billrecord_set.count() == 0:
|
||||||
br = BillRecord.objects.create(bill=bill,
|
br = BillRecord.objects.create(bill=bill,
|
||||||
order=order,
|
order=order,
|
||||||
starting_date=starting_date,
|
starting_date=order.starting_date,
|
||||||
ending_date=ending_date)
|
ending_date=order.ending_date)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Bill all recurring orders -- filter in the next iteration :-)
|
# Bill all 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 the order yet
|
||||||
|
|
||||||
|
# Find out whether it was already billed for the billing period of
|
||||||
|
# this bill
|
||||||
|
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,
|
br = BillRecord.objects.create(bill=bill,
|
||||||
order=order,
|
order=order,
|
||||||
starting_date=starting_date,
|
starting_date=this_starting_date,
|
||||||
ending_date=ending_date)
|
ending_date=this_ending_date)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Filtering ideas:
|
# Filtering ideas:
|
||||||
# If order is replaced, it should not be added anymore if it has been billed "last time"
|
# 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
|
# 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
|
||||||
|
|
||||||
|
|
||||||
return bill
|
return bill
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -467,66 +557,13 @@ class Bill(models.Model):
|
||||||
|
|
||||||
cls.create_next_bill_for_user(owner)
|
cls.create_next_bill_for_user(owner)
|
||||||
|
|
||||||
def assign_orders_to_bill(self, owner, year, month):
|
|
||||||
"""
|
|
||||||
Generate a bill for the specific month of a user.
|
|
||||||
|
|
||||||
First handle all one time orders
|
|
||||||
|
|
||||||
FIXME:
|
|
||||||
|
|
||||||
- limit this to active users in the future! (2020-05-23)
|
|
||||||
"""
|
|
||||||
|
|
||||||
"""
|
|
||||||
Find all one time orders that have a starting date that falls into this month
|
|
||||||
recurring_period=RecurringPeriod.ONE_TIME,
|
|
||||||
|
|
||||||
Can we do this even for recurring / all of them
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# FIXME: add something to check whether the order should be billed at all - i.e. a marker that
|
|
||||||
# disables searching -> optimization for later
|
|
||||||
# Create the initial bill record
|
|
||||||
# FIXME: maybe limit not even to starting/ending date, but to empty_bill record -- to be fixed in the future
|
|
||||||
# for order in Order.objects.filter(Q(starting_date__gte=self.starting_date),
|
# for order in Order.objects.filter(Q(starting_date__gte=self.starting_date),
|
||||||
# Q(starting_date__lte=self.ending_date),
|
# Q(starting_date__lte=self.ending_date),
|
||||||
|
|
||||||
# FIXME below: only check for active orders
|
#for order in Order.objects.filter(owner=owner,
|
||||||
|
# bill_records=None):
|
||||||
# Ensure all orders of that owner have at least one bill record
|
|
||||||
for order in Order.objects.filter(owner=owner,
|
|
||||||
bill_records=None):
|
|
||||||
|
|
||||||
bill_record = BillRecord.objects.create(bill=self,
|
|
||||||
quantity=1,
|
|
||||||
starting_date=order.starting_date,
|
|
||||||
ending_date=order.starting_date + timedelta(seconds=order.recurring_period))
|
|
||||||
|
|
||||||
|
|
||||||
# For each recurring order get the usage and bill it
|
# For each recurring order get the usage and bill it
|
||||||
for order in Order.objects.filter(~Q(recurring_period=RecurringPeriod.ONE_TIME),
|
|
||||||
Q(starting_date__lt=self.starting_date),
|
|
||||||
owner=owner):
|
|
||||||
|
|
||||||
if order.recurring_period > 0: # avoid div/0 - these are one time payments
|
|
||||||
|
|
||||||
# How much time will have passed by the end of the billing cycle
|
|
||||||
td = self.ending_date - order.starting_date
|
|
||||||
|
|
||||||
# How MANY times it will have been used by then
|
|
||||||
used_times = ceil(td / timedelta(seconds=order.recurring_period))
|
|
||||||
|
|
||||||
billed_times = len(order.bills)
|
|
||||||
|
|
||||||
# How many times it WAS billed -- can also be inferred from the bills that link to it!
|
|
||||||
if used_times > billed_times:
|
|
||||||
billing_times = used_times - billed_times
|
|
||||||
|
|
||||||
# ALSO REGISTER THE TIME PERIOD!
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -599,7 +636,6 @@ class Product(UncloudModel):
|
||||||
one_time_order = None
|
one_time_order = None
|
||||||
|
|
||||||
if self.one_time_price > 0:
|
if self.one_time_price > 0:
|
||||||
|
|
||||||
one_time_order = Order.objects.create(owner=self.owner,
|
one_time_order = Order.objects.create(owner=self.owner,
|
||||||
billing_address=billing_address,
|
billing_address=billing_address,
|
||||||
starting_date=when_to_start,
|
starting_date=when_to_start,
|
||||||
|
|
Loading…
Reference in a new issue