Initial yearly billing implementation
This commit is contained in:
parent
c086dbd357
commit
d089d06264
2 changed files with 94 additions and 29 deletions
|
@ -219,51 +219,106 @@ class Bill(models.Model):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_for(year, month, user):
|
def generate_for(year, month, user):
|
||||||
# /!\ We exclusively work on the specified year and month.
|
# /!\ We exclusively work on the specified year and month.
|
||||||
|
generated_bills = []
|
||||||
|
|
||||||
# Default values for next bill (if any). Only saved at the end of
|
# Default values for next bill (if any).
|
||||||
# this method, if relevant.
|
starting_date=beginning_of_month(year, month)
|
||||||
next_bill = Bill(owner=user,
|
ending_date=end_of_month(year, month)
|
||||||
starting_date=beginning_of_month(year, month),
|
creation_date=timezone.now()
|
||||||
ending_date=end_of_month(year, month),
|
|
||||||
creation_date=timezone.now(),
|
|
||||||
due_date=timezone.now() + BILL_PAYMENT_DELAY)
|
|
||||||
|
|
||||||
# Select all orders active on the request period.
|
# Select all orders active on the request period (i.e. starting on or after starting_date).
|
||||||
orders = Order.objects.filter(
|
orders = Order.objects.filter(
|
||||||
Q(ending_date__gt=next_bill.starting_date) | Q(ending_date__isnull=True),
|
Q(ending_date__gte=starting_date) | Q(ending_date__isnull=True),
|
||||||
owner=user)
|
owner=user)
|
||||||
|
|
||||||
# Check if there is already a bill covering the order and period pair:
|
# Check if there is already a bill covering the order and period pair:
|
||||||
# * Get latest bill by ending_date: previous_bill.ending_date
|
# * Get latest bill by ending_date: previous_bill.ending_date
|
||||||
# * If previous_bill.ending_date is before next_bill.ending_date, a new
|
# * For monthly bills: if previous_bill.ending_date is before
|
||||||
# bill has to be generated.
|
# (next_bill) ending_date, a new bill has to be generated.
|
||||||
unpaid_orders = []
|
# * For yearly bill: if previous_bill.ending_date is on working
|
||||||
|
# month, generate new bill.
|
||||||
|
unpaid_orders = { 'monthly_or_less': [], 'yearly': {}}
|
||||||
for order in orders:
|
for order in orders:
|
||||||
try:
|
try:
|
||||||
previous_bill = order.bill.latest('ending_date')
|
previous_bill = order.bill.latest('ending_date')
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
previous_bill = None
|
previous_bill = None
|
||||||
|
|
||||||
if previous_bill == None or previous_bill.ending_date < next_bill.ending_date:
|
# FIXME: control flow is confusing in this block.
|
||||||
unpaid_orders.append(order)
|
if order.recurring_period == RecurringPeriod.PER_YEAR:
|
||||||
|
# We ignore anything smaller than a day in here.
|
||||||
|
next_yearly_bill_start_on = None
|
||||||
|
if previous_bill == None:
|
||||||
|
next_yearly_bill_start_on = (order.starting_date + timedelta(days=1)).date()
|
||||||
|
elif previous_bill.ending_date <= ending_date:
|
||||||
|
next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)).date()
|
||||||
|
|
||||||
# Commit next_bill if it there are 'unpaid' orders.
|
# Store for bill generation. One bucket per day of month with a starting bill.
|
||||||
if len(unpaid_orders) > 0:
|
# bucket is a reference here, no need to reassign.
|
||||||
next_bill.save()
|
if next_yearly_bill_start_on:
|
||||||
|
bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on)
|
||||||
|
if bucket == None:
|
||||||
|
unpaid_orders['yearly'][next_yearly_bill_start_on] = [order]
|
||||||
|
else:
|
||||||
|
unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order]
|
||||||
|
else:
|
||||||
|
if previous_bill == None or previous_bill.ending_date <= ending_date:
|
||||||
|
unpaid_orders['monthly_or_less'].append(order)
|
||||||
|
|
||||||
|
# Handle working month's billing.
|
||||||
|
if len(unpaid_orders['monthly_or_less']) > 0:
|
||||||
|
# TODO: PREPAID billing is not supported yet.
|
||||||
|
prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY
|
||||||
|
postpaid_due_date = max(creation_date, ending_date) + BILL_PAYMENT_DELAY
|
||||||
|
|
||||||
|
next_monthly_bill = Bill.objects.create(owner=user,
|
||||||
|
creation_date=creation_date,
|
||||||
|
starting_date=starting_date.datetime(), # FIXME: this is a hack!
|
||||||
|
ending_date=ending_date,
|
||||||
|
due_date=postpaid_due_date)
|
||||||
|
|
||||||
# It is not possible to register many-to-many relationship before
|
# It is not possible to register many-to-many relationship before
|
||||||
# the two end-objects are saved in database.
|
# the two end-objects are saved in database.
|
||||||
for order in unpaid_orders:
|
for order in unpaid_orders['monthly_or_less']:
|
||||||
order.bill.add(next_bill)
|
order.bill.add(next_monthly_bill)
|
||||||
|
|
||||||
# TODO: use logger.
|
# TODO: use logger.
|
||||||
print("Generated bill {} (amount: {}) for user {}."
|
print("Generated monthly bill {} (amount: {}) for user {}."
|
||||||
.format(next_bill.uuid, next_bill.total, user))
|
.format(next_monthly_bill.uuid, next_monthly_bill.total, user))
|
||||||
|
|
||||||
return next_bill
|
# Add to output.
|
||||||
|
generated_bills.append(next_monthly_bill)
|
||||||
|
|
||||||
# Return None if no bill was created.
|
# Handle yearly bills starting on working month.
|
||||||
return None
|
if len(unpaid_orders['yearly']) > 0:
|
||||||
|
|
||||||
|
# For every starting date, generate new bill.
|
||||||
|
for next_yearly_bill_start_on in unpaid_orders['yearly']:
|
||||||
|
# No postpaid for yearly payments.
|
||||||
|
prepaid_due_date = min(creation_date.date(), next_yearly_bill_start_on) + BILL_PAYMENT_DELAY
|
||||||
|
# FIXME: a year is not exactly 365 days...
|
||||||
|
ending_date = next_yearly_bill_start_on + timedelta(days=365)
|
||||||
|
|
||||||
|
next_yearly_bill = Bill.objects.create(owner=user,
|
||||||
|
creation_date=creation_date,
|
||||||
|
starting_date=next_yearly_bill_start_on,
|
||||||
|
ending_date=ending_date,
|
||||||
|
due_date=prepaid_due_date)
|
||||||
|
|
||||||
|
# It is not possible to register many-to-many relationship before
|
||||||
|
# the two end-objects are saved in database.
|
||||||
|
for order in unpaid_orders['yearly'][next_yearly_bill_start_on]:
|
||||||
|
order.bill.add(next_yearly_bill)
|
||||||
|
|
||||||
|
# TODO: use logger.
|
||||||
|
print("Generated yearly bill {} (amount: {}) for user {}."
|
||||||
|
.format(next_yearly_bill.uuid, next_yearly_bill.total, user))
|
||||||
|
|
||||||
|
# Add to output.
|
||||||
|
generated_bills.append(next_yearly_bill)
|
||||||
|
|
||||||
|
# Return generated (monthly + yearly) bills.
|
||||||
|
return generated_bills
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_unpaid_for(user):
|
def get_unpaid_for(user):
|
||||||
|
@ -323,7 +378,7 @@ class BillRecord():
|
||||||
billed_from = self.order.starting_date
|
billed_from = self.order.starting_date
|
||||||
|
|
||||||
if billed_from > billed_until:
|
if billed_from > billed_until:
|
||||||
# TODO: think about and check edges cases. This should not be
|
# TODO: think about and check edge cases. This should not be
|
||||||
# possible.
|
# possible.
|
||||||
raise Exception('Impossible billing delta!')
|
raise Exception('Impossible billing delta!')
|
||||||
|
|
||||||
|
@ -331,11 +386,15 @@ class BillRecord():
|
||||||
|
|
||||||
# TODO: refactor this thing?
|
# TODO: refactor this thing?
|
||||||
# TODO: weekly
|
# TODO: weekly
|
||||||
# TODO: yearly
|
if self.recurring_period == RecurringPeriod.PER_YEAR:
|
||||||
if self.recurring_period == RecurringPeriod.PER_MONTH:
|
# Should always be one, but let's be careful.
|
||||||
|
# FIXME: a year is not exactly 365 days...
|
||||||
|
years = (billed_delta / timedelta(days=365))
|
||||||
|
return Decimal(years)
|
||||||
|
elif self.recurring_period == RecurringPeriod.PER_MONTH:
|
||||||
days = ceil(billed_delta / timedelta(days=1))
|
days = ceil(billed_delta / timedelta(days=1))
|
||||||
|
|
||||||
# XXX: we assume monthly bills for now.
|
# Monthly bills always cover one single month.
|
||||||
if (self.bill.starting_date.year != self.bill.starting_date.year or
|
if (self.bill.starting_date.year != self.bill.starting_date.year or
|
||||||
self.bill.starting_date.month != self.bill.ending_date.month):
|
self.bill.starting_date.month != self.bill.ending_date.month):
|
||||||
raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'.
|
raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'.
|
||||||
|
@ -346,6 +405,9 @@ class BillRecord():
|
||||||
self.bill.starting_date.year,
|
self.bill.starting_date.year,
|
||||||
self.bill.starting_date.month)
|
self.bill.starting_date.month)
|
||||||
return Decimal(days / days_in_month)
|
return Decimal(days / days_in_month)
|
||||||
|
elif self.recurring_period == RecurringPeriod.PER_WEEK:
|
||||||
|
weeks = ceil(billed_delta / timedelta(week=1))
|
||||||
|
return Decimal(weeks)
|
||||||
elif self.recurring_period == RecurringPeriod.PER_DAY:
|
elif self.recurring_period == RecurringPeriod.PER_DAY:
|
||||||
days = ceil(billed_delta / timedelta(days=1))
|
days = ceil(billed_delta / timedelta(days=1))
|
||||||
return Decimal(days)
|
return Decimal(days)
|
||||||
|
|
|
@ -72,6 +72,8 @@ class VMProduct(Product):
|
||||||
return self.cores * 3 + self.ram_in_gb * 4
|
return self.cores * 3 + self.ram_in_gb * 4
|
||||||
elif recurring_period == RecurringPeriod.PER_HOUR:
|
elif recurring_period == RecurringPeriod.PER_HOUR:
|
||||||
return self.cores * 4.0/(30 * 24) + self.ram_in_gb * 4.5/(30* 24)
|
return self.cores * 4.0/(30 * 24) + self.ram_in_gb * 4.5/(30* 24)
|
||||||
|
elif recurring_period == RecurringPeriod.PER_YEAR:
|
||||||
|
return (self.cores * 2.5 + self.ram_in_gb * 3.5) * 12
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid recurring period for VM Product pricing.')
|
raise Exception('Invalid recurring period for VM Product pricing.')
|
||||||
|
|
||||||
|
@ -88,7 +90,8 @@ class VMProduct(Product):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def allowed_recurring_periods():
|
def allowed_recurring_periods():
|
||||||
return list(filter(
|
return list(filter(
|
||||||
lambda pair: pair[0] in [RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR],
|
lambda pair: pair[0] in [RecurringPeriod.PER_YEAR,
|
||||||
|
RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR],
|
||||||
RecurringPeriod.choices))
|
RecurringPeriod.choices))
|
||||||
|
|
||||||
class VMWithOSProduct(VMProduct):
|
class VMWithOSProduct(VMProduct):
|
||||||
|
|
Loading…
Reference in a new issue