forked from uncloud/uncloud
in the middle of restructering
This commit is contained in:
parent
5d1eaaf0af
commit
a3f3ca8cf9
2 changed files with 103 additions and 386 deletions
|
@ -31,6 +31,11 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
||||||
class RecurringPeriod(models.IntegerChoices):
|
class RecurringPeriod(models.IntegerChoices):
|
||||||
|
"""
|
||||||
|
We don't support months are years, because the vary in length.
|
||||||
|
This is not only complicated, but also unfair to the user, as the user pays the same
|
||||||
|
amount for different durations.
|
||||||
|
"""
|
||||||
PER_365D = 365*24*3600, _('Per 365 days')
|
PER_365D = 365*24*3600, _('Per 365 days')
|
||||||
PER_30D = 30*24*3600, _('Per 30 days')
|
PER_30D = 30*24*3600, _('Per 30 days')
|
||||||
PER_WEEK = 7*24*3600, _('Per Week')
|
PER_WEEK = 7*24*3600, _('Per Week')
|
||||||
|
@ -40,14 +45,13 @@ class RecurringPeriod(models.IntegerChoices):
|
||||||
PER_SECOND = 1, _('Per Second')
|
PER_SECOND = 1, _('Per Second')
|
||||||
ONE_TIME = 0, _('Onetime')
|
ONE_TIME = 0, _('Onetime')
|
||||||
|
|
||||||
|
|
||||||
class CountryField(models.CharField):
|
class CountryField(models.CharField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs.setdefault('choices', COUNTRIES)
|
kwargs.setdefault('choices', COUNTRIES)
|
||||||
kwargs.setdefault('default', 'CH')
|
kwargs.setdefault('default', 'CH')
|
||||||
kwargs.setdefault('max_length', 2)
|
kwargs.setdefault('max_length', 2)
|
||||||
|
|
||||||
super(CountryField, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_internal_type(self):
|
def get_internal_type(self):
|
||||||
return "CharField"
|
return "CharField"
|
||||||
|
@ -246,7 +250,23 @@ class VATRate(models.Model):
|
||||||
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
|
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
class BillNico(models.Model):
|
class BillRecord(models.Model):
|
||||||
|
"""
|
||||||
|
Entry of a bill, dynamically generated from an order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bill = models.ForeignKey(Bill, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# How many times the order has been used in this record
|
||||||
|
usage_count = models.IntegerField(default=1)
|
||||||
|
|
||||||
|
# The timeframe the bill record is for can (and probably often will) differ
|
||||||
|
# from the bill time
|
||||||
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
starting_date = models.DateTimeField()
|
||||||
|
ending_date = models.DateTimeField()
|
||||||
|
|
||||||
|
class Bill(models.Model):
|
||||||
""" FIXME:
|
""" FIXME:
|
||||||
Bill needs to be unique in the triple (owner, year, month)
|
Bill needs to be unique in the triple (owner, year, month)
|
||||||
"""
|
"""
|
||||||
|
@ -262,12 +282,20 @@ class BillNico(models.Model):
|
||||||
|
|
||||||
valid = models.BooleanField(default=True)
|
valid = models.BooleanField(default=True)
|
||||||
|
|
||||||
@staticmethod
|
# billing address and vat rate is the same for the whole bill
|
||||||
def create_all_bills():
|
# @property
|
||||||
|
# def vat_rate(self):
|
||||||
|
# return Decimal(VATRate.get_for_country(self.bill.billing_address.country))
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_all_bills(cls):
|
||||||
for owner in get_user_model().objects.all():
|
for owner in get_user_model().objects.all():
|
||||||
# mintime = time of first order
|
# mintime = time of first order
|
||||||
# maxtime = time of last order
|
# maxtime = time of last order
|
||||||
# iterate month based through it
|
# iterate month based through it
|
||||||
|
|
||||||
|
cls.assign_orders_to_bill(owner, year, month)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def assign_orders_to_bill(self, owner, year, month):
|
def assign_orders_to_bill(self, owner, year, month):
|
||||||
|
@ -291,359 +319,55 @@ class BillNico(models.Model):
|
||||||
|
|
||||||
# FIXME: add something to check whether the order should be billed at all - i.e. a marker that
|
# FIXME: add something to check whether the order should be billed at all - i.e. a marker that
|
||||||
# disables searching -> optimization for later
|
# disables searching -> optimization for later
|
||||||
for order in Order.objects.filter(Q(starting_date__gte=self.starting_date),
|
# Create the initial bill record
|
||||||
Q(starting_date__lte=self.ending_date),
|
# FIXME: maybe limit not even to starting/ending date, but to empty_bill record -- to be fixed in the future
|
||||||
owner=owner):
|
# for order in Order.objects.filter(Q(starting_date__gte=self.starting_date),
|
||||||
|
# Q(starting_date__lte=self.ending_date),
|
||||||
|
|
||||||
# order.bill.add(this_bill)
|
# FIXME below: only check for active orders
|
||||||
pass
|
|
||||||
|
# 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,
|
||||||
|
usage_count=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
|
||||||
Find all recurring orders that did not start in this time frame, but need
|
|
||||||
to be billed in this time frame.
|
|
||||||
|
|
||||||
This is:
|
|
||||||
- order starting time before our starting time
|
|
||||||
- order start time + (x * (the_period)) is inside our time frame, x must be integer
|
|
||||||
test cases:
|
|
||||||
+ 365days:
|
|
||||||
time_since_last_billed = self.starting_or_ending_date - order.last_bill_date
|
|
||||||
periods =
|
|
||||||
[ we could in theory add this as a property to the order: next
|
|
||||||
"""
|
|
||||||
for order in Order.objects.filter(~Q(recurring_period=RecurringPeriod.ONE_TIME),
|
for order in Order.objects.filter(~Q(recurring_period=RecurringPeriod.ONE_TIME),
|
||||||
Q(starting_date__lt=self.starting_date),
|
Q(starting_date__lt=self.starting_date),
|
||||||
owner=owner):
|
owner=owner):
|
||||||
|
|
||||||
if order.recurring_period > 0: # avoid div/0 - these are one time payments
|
if order.recurring_period > 0: # avoid div/0 - these are one time payments
|
||||||
pass
|
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
|
||||||
class Bill(models.Model):
|
billed_times = len(order.bills)
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
owner = models.ForeignKey(get_user_model(),
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
# How many times it WAS billed -- can also be inferred from the bills that link to it!
|
||||||
starting_date = models.DateTimeField()
|
if used_times > billed_times:
|
||||||
ending_date = models.DateTimeField()
|
billing_times = used_times - billed_times
|
||||||
due_date = models.DateField()
|
|
||||||
|
|
||||||
valid = models.BooleanField(default=True)
|
# ALSO REGISTER THE TIME PERIOD!
|
||||||
|
pass
|
||||||
|
|
||||||
# Trigger product activation if bill paid at creation (from balance).
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super(Bill, self).save(*args, **kwargs)
|
|
||||||
if not self in Bill.get_unpaid_for(self.owner):
|
|
||||||
self.activate_products()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reference(self):
|
|
||||||
return "{}-{}".format(
|
|
||||||
self.owner.username,
|
|
||||||
self.creation_date.strftime("%Y-%m-%d-%H%M"))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def records(self):
|
|
||||||
bill_records = []
|
|
||||||
orders = Order.objects.filter(bill=self)
|
|
||||||
for order in orders:
|
|
||||||
bill_record = BillRecord(self, order)
|
|
||||||
bill_records.append(bill_record)
|
|
||||||
|
|
||||||
return bill_records
|
|
||||||
|
|
||||||
@property
|
|
||||||
def amount(self):
|
|
||||||
return reduce(lambda acc, record: acc + record.amount, self.records, 0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vat_amount(self):
|
|
||||||
return reduce(lambda acc, record: acc + record.vat_amount, self.records, 0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.amount + self.vat_amount
|
|
||||||
|
|
||||||
@property
|
|
||||||
def final(self):
|
|
||||||
# A bill is final when its ending date is passed, or when all of its
|
|
||||||
# orders have been terminated.
|
|
||||||
every_order_terminated = True
|
|
||||||
billing_period_is_over = self.ending_date < timezone.now()
|
|
||||||
for order in self.order_set.all():
|
|
||||||
every_order_terminated = every_order_terminated and order.is_terminated
|
|
||||||
|
|
||||||
return billing_period_is_over or every_order_terminated
|
|
||||||
|
|
||||||
def activate_products(self):
|
|
||||||
for order in self.order_set.all():
|
|
||||||
# FIXME: using __something might not be a good idea.
|
|
||||||
for product_class in Product.__subclasses__():
|
|
||||||
for product in product_class.objects.filter(order=order):
|
|
||||||
if product.status == UncloudStatus.AWAITING_PAYMENT:
|
|
||||||
product.status = UncloudStatus.PENDING
|
|
||||||
product.save()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def billing_address(self):
|
|
||||||
orders = Order.objects.filter(bill=self)
|
|
||||||
# The genrate_for method makes sure all the orders of a bill share the
|
|
||||||
# same billing address. TODO: It would be nice to enforce that somehow...
|
|
||||||
if orders:
|
|
||||||
return orders[0].billing_address
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# TODO: split this huuuge method!
|
|
||||||
@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).
|
|
||||||
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 (i.e. starting on or after starting_date).
|
|
||||||
orders = Order.objects.filter(
|
|
||||||
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
|
|
||||||
# * 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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# elif previous_bill.ending_date <= ending_date:
|
|
||||||
# next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1))
|
|
||||||
|
|
||||||
# # 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:
|
|
||||||
# # We want to group orders by date but keep using datetimes.
|
|
||||||
# next_yearly_bill_start_on = next_yearly_bill_start_on.replace(
|
|
||||||
# minute=0, hour=0, second=0, microsecond=0)
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# There should not be any bill linked to orders with different
|
|
||||||
# billing addresses.
|
|
||||||
per_address_orders = itertools.groupby(
|
|
||||||
unpaid_orders['monthly_or_less'],
|
|
||||||
lambda o: o.billing_address)
|
|
||||||
|
|
||||||
for addr, bill_orders in per_address_orders:
|
|
||||||
next_monthly_bill = Bill.objects.create(owner=user,
|
|
||||||
creation_date=creation_date,
|
|
||||||
starting_date=starting_date, # 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 bill_orders:
|
|
||||||
order.bill.add(next_monthly_bill)
|
|
||||||
|
|
||||||
logger.info("Generated monthly bill {} (amount: {}) for user {}."
|
|
||||||
.format(next_monthly_bill.uuid, next_monthly_bill.total, user))
|
|
||||||
|
|
||||||
# Add to output.
|
|
||||||
generated_bills.append(next_monthly_bill)
|
|
||||||
|
|
||||||
# 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, next_yearly_bill_start_on) + BILL_PAYMENT_DELAY
|
|
||||||
# Bump by one year, remove one day.
|
|
||||||
ending_date = next_yearly_bill_start_on.replace(
|
|
||||||
year=next_yearly_bill_start_on.year+1) - timedelta(days=1)
|
|
||||||
|
|
||||||
# There should not be any bill linked to orders with different
|
|
||||||
# billing addresses.
|
|
||||||
per_address_orders = itertools.groupby(
|
|
||||||
unpaid_orders['yearly'][next_yearly_bill_start_on],
|
|
||||||
lambda o: o.billing_address)
|
|
||||||
|
|
||||||
for addr, bill_orders in per_address_orders:
|
|
||||||
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 bill_orders:
|
|
||||||
order.bill.add(next_yearly_bill)
|
|
||||||
|
|
||||||
logger.info("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):
|
|
||||||
balance = get_balance_for_user(user)
|
|
||||||
unpaid_bills = []
|
|
||||||
# No unpaid bill if balance is positive.
|
|
||||||
if balance >= 0:
|
|
||||||
return unpaid_bills
|
|
||||||
else:
|
|
||||||
bills = Bill.objects.filter(
|
|
||||||
owner=user,
|
|
||||||
).order_by('-creation_date')
|
|
||||||
|
|
||||||
# Amount to be paid by the customer.
|
|
||||||
unpaid_balance = abs(balance)
|
|
||||||
for bill in bills:
|
|
||||||
if unpaid_balance <= 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
unpaid_balance -= bill.total
|
|
||||||
unpaid_bills.append(bill)
|
|
||||||
|
|
||||||
return unpaid_bills
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_overdue_for(user):
|
|
||||||
unpaid_bills = Bill.get_unpaid_for(user)
|
|
||||||
return list(filter(lambda bill: bill.due_date > timezone.now(), unpaid_bills))
|
|
||||||
|
|
||||||
class BillRecord():
|
|
||||||
"""
|
|
||||||
Entry of a bill, dynamically generated from an order.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bill, order):
|
|
||||||
self.bill = bill
|
|
||||||
self.order = order
|
|
||||||
self.recurring_price = order.recurring_price
|
|
||||||
self.recurring_period = order.recurring_period
|
|
||||||
self.description = order.description
|
|
||||||
|
|
||||||
if self.order.starting_date >= self.bill.starting_date:
|
|
||||||
self.one_time_price = order.one_time_price
|
|
||||||
else:
|
|
||||||
self.one_time_price = 0
|
|
||||||
|
|
||||||
# Set decimal context for amount computations.
|
|
||||||
# XXX: understand why we need +1 here.
|
|
||||||
decimal.getcontext().prec = AMOUNT_DECIMALS + 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def recurring_count(self):
|
|
||||||
# Compute billing delta.
|
|
||||||
billed_until = self.bill.ending_date
|
|
||||||
if self.order.ending_date != None and self.order.ending_date <= self.bill.ending_date:
|
|
||||||
billed_until = self.order.ending_date
|
|
||||||
|
|
||||||
billed_from = self.bill.starting_date
|
|
||||||
if self.order.starting_date > self.bill.starting_date:
|
|
||||||
billed_from = self.order.starting_date
|
|
||||||
|
|
||||||
if billed_from > billed_until:
|
|
||||||
# TODO: think about and check edge cases. This should not be
|
|
||||||
# possible.
|
|
||||||
raise Exception('Impossible billing delta!')
|
|
||||||
|
|
||||||
billed_delta = billed_until - billed_from
|
|
||||||
|
|
||||||
# TODO: refactor this thing?
|
|
||||||
# TODO: weekly
|
|
||||||
# if self.recurring_period == RecurringPeriod.PER_YEAR:
|
|
||||||
# # XXX: Should always be one => we do not bill for more than one year.
|
|
||||||
# # TODO: check billed_delta is ~365 days.
|
|
||||||
# return 1
|
|
||||||
# elif self.recurring_period == RecurringPeriod.PER_MONTH:
|
|
||||||
# days = ceil(billed_delta / timedelta(days=1))
|
|
||||||
|
|
||||||
# # 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.'.
|
|
||||||
# format(self.bill.uuid))
|
|
||||||
|
|
||||||
# # XXX: minumal length of monthly order is to be enforced somewhere else.
|
|
||||||
# (_, days_in_month) = monthrange(
|
|
||||||
# self.bill.starting_date.year,
|
|
||||||
# self.bill.starting_date.month)
|
|
||||||
# return round(days / days_in_month, AMOUNT_DECIMALS)
|
|
||||||
if self.recurring_period == RecurringPeriod.PER_WEEK:
|
|
||||||
weeks = ceil(billed_delta / timedelta(week=1))
|
|
||||||
return weeks
|
|
||||||
elif self.recurring_period == RecurringPeriod.PER_DAY:
|
|
||||||
days = ceil(billed_delta / timedelta(days=1))
|
|
||||||
return days
|
|
||||||
elif self.recurring_period == RecurringPeriod.PER_HOUR:
|
|
||||||
hours = ceil(billed_delta / timedelta(hours=1))
|
|
||||||
return hours
|
|
||||||
elif self.recurring_period == RecurringPeriod.PER_SECOND:
|
|
||||||
seconds = ceil(billed_delta / timedelta(seconds=1))
|
|
||||||
return seconds
|
|
||||||
elif self.recurring_period == RecurringPeriod.ONE_TIME:
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
raise Exception('Unsupported recurring period: {}.'.
|
|
||||||
format(self.order.recurring_period))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vat_rate(self):
|
|
||||||
return Decimal(VATRate.get_for_country(self.bill.billing_address.country))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vat_amount(self):
|
|
||||||
return self.amount * self.vat_rate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def amount(self):
|
|
||||||
return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.amount + self.vat_amount
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Orders.
|
# Orders.
|
||||||
|
|
||||||
# Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
|
|
||||||
# bills. Do **NOT** mutate then!
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
|
"""
|
||||||
|
Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
|
||||||
|
bills. Do **NOT** mutate then!
|
||||||
|
"""
|
||||||
|
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
owner = models.ForeignKey(get_user_model(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -658,11 +382,22 @@ class Order(models.Model):
|
||||||
ending_date = models.DateTimeField(blank=True,
|
ending_date = models.DateTimeField(blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
|
|
||||||
bill = models.ManyToManyField(Bill,
|
bill_records = models.ManyToManyField(BillRecord,
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True)
|
blank=True)
|
||||||
|
|
||||||
recurring_period = models.IntegerField(choices = RecurringPeriod.choices, default = RecurringPeriod.PER_30D)
|
@property
|
||||||
|
def count_billed(self):
|
||||||
|
"""
|
||||||
|
How many times this order was billed so far.
|
||||||
|
This logic is mainly thought to be for recurring bills, but also works for one time bills
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sum([ br.usage_count for br in self.bill_records.all() ])
|
||||||
|
|
||||||
|
|
||||||
|
recurring_period = models.IntegerField(choices = RecurringPeriod.choices,
|
||||||
|
default = RecurringPeriod.PER_30D)
|
||||||
|
|
||||||
one_time_price = models.DecimalField(default=0.0,
|
one_time_price = models.DecimalField(default=0.0,
|
||||||
max_digits=AMOUNT_MAX_DIGITS,
|
max_digits=AMOUNT_MAX_DIGITS,
|
||||||
|
@ -696,7 +431,6 @@ class Order(models.Model):
|
||||||
@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
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_terminated(self):
|
def is_terminated(self):
|
||||||
return self.ending_date != None and self.ending_date < timezone.now()
|
return self.ending_date != None and self.ending_date < timezone.now()
|
||||||
|
@ -709,9 +443,6 @@ class Order(models.Model):
|
||||||
self.ending_date = timezone.now()
|
self.ending_date = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_to_be_charged_in(year, month):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Trigger initial bill generation at order creation.
|
# 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:
|
||||||
|
@ -749,35 +480,6 @@ class Order(models.Model):
|
||||||
self.one_time_price,
|
self.one_time_price,
|
||||||
self.recurring_price)
|
self.recurring_price)
|
||||||
|
|
||||||
class OrderTimothee(models.Model):
|
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
owner = models.ForeignKey(get_user_model(),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
editable=False)
|
|
||||||
billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
# TODO: enforce ending_date - starting_date to be larger than recurring_period.
|
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
starting_date = models.DateTimeField(default=timezone.now)
|
|
||||||
ending_date = models.DateTimeField(blank=True,
|
|
||||||
null=True)
|
|
||||||
|
|
||||||
bill = models.ManyToManyField(Bill,
|
|
||||||
editable=False,
|
|
||||||
blank=True)
|
|
||||||
|
|
||||||
recurring_period = models.IntegerField(choices = RecurringPeriod.choices,
|
|
||||||
default = RecurringPeriod.PER_30D)
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def records(self):
|
def records(self):
|
||||||
return OrderRecord.objects.filter(order=self)
|
return OrderRecord.objects.filter(order=self)
|
||||||
|
@ -988,3 +690,16 @@ class Product(UncloudModel):
|
||||||
else:
|
else:
|
||||||
# FIXME: use the right type of exception here!
|
# FIXME: use the right type of exception here!
|
||||||
raise Exception("Did not implement the discounter for this case")
|
raise Exception("Did not implement the discounter for this case")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Interesting snippets
|
||||||
|
|
||||||
|
# # 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")
|
||||||
|
|
||||||
|
# super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner)
|
||||||
|
|
|
@ -5,7 +5,8 @@ from datetime import datetime, date, timedelta
|
||||||
from .models import *
|
from .models import *
|
||||||
from uncloud_service.models import GenericServiceProduct
|
from uncloud_service.models import GenericServiceProduct
|
||||||
|
|
||||||
class BillingTestCase(TestCase):
|
class NotABillingTC(TestCase):
|
||||||
|
#class BillingTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = get_user_model().objects.create(
|
self.user = get_user_model().objects.create(
|
||||||
username='jdoe',
|
username='jdoe',
|
||||||
|
@ -22,15 +23,16 @@ class BillingTestCase(TestCase):
|
||||||
description = "Test Product 1"
|
description = "Test Product 1"
|
||||||
|
|
||||||
# Three months: full, full, partial.
|
# Three months: full, full, partial.
|
||||||
starting_date = datetime.fromisoformat('2020-03-01')
|
# starting_date = datetime.fromisoformat('2020-03-01')
|
||||||
ending_date = datetime.fromisoformat('2020-05-08')
|
starting_date = datetime(2020,3,1)
|
||||||
|
ending_date = datetime(2020,5,8)
|
||||||
|
|
||||||
# Create order to be billed.
|
# Create order to be billed.
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
ending_date=ending_date,
|
ending_date=ending_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
recurring_price=recurring_price,
|
recurring_price=recurring_price,
|
||||||
one_time_price=one_time_price,
|
one_time_price=one_time_price,
|
||||||
description=description,
|
description=description,
|
||||||
|
@ -67,7 +69,7 @@ class BillingTestCase(TestCase):
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_YEAR,
|
recurring_period=RecurringPeriod.PER_365D,
|
||||||
recurring_price=recurring_price,
|
recurring_price=recurring_price,
|
||||||
one_time_price=one_time_price,
|
one_time_price=one_time_price,
|
||||||
description=description,
|
description=description,
|
||||||
|
@ -150,7 +152,7 @@ class ProductActivationTestCase(TestCase):
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
recurring_price=recurring_price,
|
recurring_price=recurring_price,
|
||||||
one_time_price=one_time_price,
|
one_time_price=one_time_price,
|
||||||
description=description,
|
description=description,
|
||||||
|
@ -205,12 +207,12 @@ class BillingAddressTestCase(TestCase):
|
||||||
order_01 = Order.objects.create(
|
order_01 = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
billing_address=self.billing_address_01)
|
billing_address=self.billing_address_01)
|
||||||
order_02 = Order.objects.create(
|
order_02 = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
billing_address=self.billing_address_01)
|
billing_address=self.billing_address_01)
|
||||||
|
|
||||||
# We need a single bill since we work with a single address.
|
# We need a single bill since we work with a single address.
|
||||||
|
@ -225,12 +227,12 @@ class BillingAddressTestCase(TestCase):
|
||||||
order_01 = Order.objects.create(
|
order_01 = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
billing_address=self.billing_address_01)
|
billing_address=self.billing_address_01)
|
||||||
order_02 = Order.objects.create(
|
order_02 = Order.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
recurring_period=RecurringPeriod.PER_MONTH,
|
recurring_period=RecurringPeriod.PER_30D,
|
||||||
billing_address=self.billing_address_02)
|
billing_address=self.billing_address_02)
|
||||||
|
|
||||||
# We need different bills since we work with different addresses.
|
# We need different bills since we work with different addresses.
|
||||||
|
|
Loading…
Reference in a new issue