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',
|
||||
related_name='replaced_by',
|
||||
on_delete=models.PROTECT,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True)
|
||||
|
||||
depends_on = models.ForeignKey('self',
|
||||
related_name='parent_of',
|
||||
on_delete=models.PROTECT,
|
||||
on_delete=models.CASCADE,
|
||||
blank=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
|
||||
def count_billed(self):
|
||||
"""
|
||||
|
@ -328,14 +339,6 @@ class Order(models.Model):
|
|||
|
||||
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
|
||||
def is_recurring(self):
|
||||
return not self.recurring_period == RecurringPeriod.ONE_TIME
|
||||
|
@ -344,37 +347,46 @@ class Order(models.Model):
|
|||
def is_one_time(self):
|
||||
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):
|
||||
if self.ending_date and self.ending_date < self.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)
|
||||
|
||||
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):
|
||||
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):
|
||||
"""
|
||||
A bill is a representation of usage at a specific time
|
||||
"""
|
||||
owner = models.ForeignKey(get_user_model(),
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
|
@ -382,6 +394,7 @@ class Bill(models.Model):
|
|||
starting_date = models.DateTimeField(default=start_of_this_month)
|
||||
ending_date = models.DateTimeField()
|
||||
due_date = models.DateField(default=default_payment_delay)
|
||||
|
||||
is_final = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
|
@ -395,6 +408,11 @@ class Bill(models.Model):
|
|||
def __str__(self):
|
||||
return f"Bill {self.owner}-{self.id}"
|
||||
|
||||
@property
|
||||
def billing_address(self):
|
||||
pass
|
||||
# if self.order_set.all
|
||||
|
||||
@property
|
||||
def sum(self):
|
||||
bill_records = BillRecord.objects.filter(bill=self)
|
||||
|
@ -402,8 +420,11 @@ class Bill(models.Model):
|
|||
|
||||
|
||||
@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()
|
||||
|
||||
# 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')
|
||||
first_order = all_orders.first()
|
||||
|
||||
|
@ -424,13 +445,10 @@ class Bill(models.Model):
|
|||
else:
|
||||
starting_date = timezone.now()
|
||||
|
||||
|
||||
if not ending_date:
|
||||
ending_date = end_of_month(starting_date)
|
||||
|
||||
# create new bill, if previous is closed/does not exist
|
||||
if not bill:
|
||||
|
||||
bill = cls.objects.create(
|
||||
owner=owner,
|
||||
starting_date=starting_date,
|
||||
|
@ -438,24 +456,96 @@ class Bill(models.Model):
|
|||
|
||||
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=starting_date,
|
||||
ending_date=ending_date)
|
||||
starting_date=order.starting_date,
|
||||
ending_date=order.ending_date)
|
||||
|
||||
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,
|
||||
order=order,
|
||||
starting_date=this_starting_date,
|
||||
ending_date=this_ending_date)
|
||||
|
||||
|
||||
br = BillRecord.objects.create(bill=bill,
|
||||
order=order,
|
||||
starting_date=starting_date,
|
||||
ending_date=ending_date)
|
||||
|
||||
# Filtering ideas:
|
||||
# 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
|
||||
|
||||
|
||||
return bill
|
||||
|
||||
@classmethod
|
||||
|
@ -467,66 +557,13 @@ class Bill(models.Model):
|
|||
|
||||
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),
|
||||
# Q(starting_date__lte=self.ending_date),
|
||||
|
||||
# FIXME below: only check for active orders
|
||||
|
||||
# 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 order in Order.objects.filter(owner=owner,
|
||||
# bill_records=None):
|
||||
|
||||
# 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
|
||||
|
||||
if self.one_time_price > 0:
|
||||
|
||||
one_time_order = Order.objects.create(owner=self.owner,
|
||||
billing_address=billing_address,
|
||||
starting_date=when_to_start,
|
||||
|
|
Loading…
Reference in a new issue