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
|
||||
def generate_for(year, month, user):
|
||||
# /!\ We exclusively work on the specified year and month.
|
||||
generated_bills = []
|
||||
|
||||
# Default values for next bill (if any). Only saved at the end of
|
||||
# this method, if relevant.
|
||||
next_bill = Bill(owner=user,
|
||||
starting_date=beginning_of_month(year, month),
|
||||
ending_date=end_of_month(year, month),
|
||||
creation_date=timezone.now(),
|
||||
due_date=timezone.now() + BILL_PAYMENT_DELAY)
|
||||
# Default values for next bill (if any).
|
||||
starting_date=beginning_of_month(year, month)
|
||||
ending_date=end_of_month(year, month)
|
||||
creation_date=timezone.now()
|
||||
|
||||
# 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(
|
||||
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)
|
||||
|
||||
# Check if there is already a bill covering the order and period pair:
|
||||
# * Get latest bill by ending_date: previous_bill.ending_date
|
||||
# * If previous_bill.ending_date is before next_bill.ending_date, a new
|
||||
# bill has to be generated.
|
||||
unpaid_orders = []
|
||||
# * For monthly bills: if previous_bill.ending_date is before
|
||||
# (next_bill) ending_date, a new bill has to be generated.
|
||||
# * 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:
|
||||
try:
|
||||
previous_bill = order.bill.latest('ending_date')
|
||||
except ObjectDoesNotExist:
|
||||
previous_bill = None
|
||||
|
||||
if previous_bill == None or previous_bill.ending_date < next_bill.ending_date:
|
||||
unpaid_orders.append(order)
|
||||
# FIXME: control flow is confusing in this block.
|
||||
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.
|
||||
if len(unpaid_orders) > 0:
|
||||
next_bill.save()
|
||||
# Store for bill generation. One bucket per day of month with a starting bill.
|
||||
# bucket is a reference here, no need to reassign.
|
||||
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
|
||||
# the two end-objects are saved in database.
|
||||
for order in unpaid_orders:
|
||||
order.bill.add(next_bill)
|
||||
for order in unpaid_orders['monthly_or_less']:
|
||||
order.bill.add(next_monthly_bill)
|
||||
|
||||
# TODO: use logger.
|
||||
print("Generated bill {} (amount: {}) for user {}."
|
||||
.format(next_bill.uuid, next_bill.total, user))
|
||||
print("Generated monthly bill {} (amount: {}) for 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.
|
||||
return None
|
||||
# Handle yearly bills starting on working month.
|
||||
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
|
||||
def get_unpaid_for(user):
|
||||
|
@ -323,7 +378,7 @@ class BillRecord():
|
|||
billed_from = self.order.starting_date
|
||||
|
||||
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.
|
||||
raise Exception('Impossible billing delta!')
|
||||
|
||||
|
@ -331,11 +386,15 @@ class BillRecord():
|
|||
|
||||
# TODO: refactor this thing?
|
||||
# TODO: weekly
|
||||
# TODO: yearly
|
||||
if self.recurring_period == RecurringPeriod.PER_MONTH:
|
||||
if self.recurring_period == RecurringPeriod.PER_YEAR:
|
||||
# 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))
|
||||
|
||||
# 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
|
||||
self.bill.starting_date.month != self.bill.ending_date.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.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:
|
||||
days = ceil(billed_delta / timedelta(days=1))
|
||||
return Decimal(days)
|
||||
|
|
|
@ -72,6 +72,8 @@ class VMProduct(Product):
|
|||
return self.cores * 3 + self.ram_in_gb * 4
|
||||
elif recurring_period == RecurringPeriod.PER_HOUR:
|
||||
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:
|
||||
raise Exception('Invalid recurring period for VM Product pricing.')
|
||||
|
||||
|
@ -88,7 +90,8 @@ class VMProduct(Product):
|
|||
@staticmethod
|
||||
def allowed_recurring_periods():
|
||||
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))
|
||||
|
||||
class VMWithOSProduct(VMProduct):
|
||||
|
|
Loading…
Reference in a new issue