Make recurring period a database model
- For easier handling (foreignkeys, many2many) - For higher flexibility (users can define their own periods)
This commit is contained in:
parent
58883765d7
commit
992c7c551e
11 changed files with 588 additions and 362 deletions
|
|
@ -64,21 +64,6 @@ def start_after(a_date):
|
|||
def default_payment_delay():
|
||||
return timezone.now() + BILL_PAYMENT_DELAY
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
||||
class RecurringPeriod(models.IntegerChoices):
|
||||
"""
|
||||
We don't support months are years, because they 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_30D = 30*24*3600, _('Per 30 days')
|
||||
PER_WEEK = 7*24*3600, _('Per Week')
|
||||
PER_DAY = 24*3600, _('Per Day')
|
||||
PER_HOUR = 3600, _('Per Hour')
|
||||
PER_MINUTE = 60, _('Per Minute')
|
||||
PER_SECOND = 1, _('Per Second')
|
||||
ONE_TIME = 0, _('Onetime')
|
||||
|
||||
class Currency(models.TextChoices):
|
||||
"""
|
||||
|
|
@ -236,6 +221,41 @@ class PaymentMethod(models.Model):
|
|||
# non-primary method.
|
||||
pass
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
||||
class RecurringPeriodChoices(models.IntegerChoices):
|
||||
"""
|
||||
This is an old class and being superseeded by the database model below
|
||||
"""
|
||||
PER_365D = 365*24*3600, _('Per 365 days')
|
||||
PER_30D = 30*24*3600, _('Per 30 days')
|
||||
PER_WEEK = 7*24*3600, _('Per Week')
|
||||
PER_DAY = 24*3600, _('Per Day')
|
||||
PER_HOUR = 3600, _('Per Hour')
|
||||
PER_MINUTE = 60, _('Per Minute')
|
||||
PER_SECOND = 1, _('Per Second')
|
||||
ONE_TIME = 0, _('Onetime')
|
||||
|
||||
# RecurringPeriods
|
||||
class RecurringPeriod(models.Model):
|
||||
"""
|
||||
Available recurring periods.
|
||||
By default seeded from RecurringPeriodChoices
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
duration_seconds = models.IntegerField(unique=True)
|
||||
|
||||
@classmethod
|
||||
def populate_db_defaults(cls):
|
||||
for (seconds, name) in RecurringPeriodChoices.choices:
|
||||
obj, created = cls.objects.get_or_create(name=name,
|
||||
defaults={ 'duration_seconds': seconds })
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.duration_seconds})"
|
||||
|
||||
|
||||
###
|
||||
# Bills.
|
||||
|
||||
|
|
@ -313,7 +333,11 @@ class Product(UncloudModel):
|
|||
description = models.CharField(max_length=1024)
|
||||
|
||||
config = models.JSONField()
|
||||
default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D)
|
||||
|
||||
# default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D)
|
||||
default_recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True)
|
||||
|
||||
|
||||
currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)
|
||||
|
||||
@property
|
||||
|
|
@ -409,7 +433,6 @@ class Product(UncloudModel):
|
|||
self.create_order(when_to_start, recurring_period)
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def recurring_price(self):
|
||||
""" implement correct values in the child class """
|
||||
|
|
@ -564,8 +587,9 @@ class Order(models.Model):
|
|||
ending_date = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
# FIXME: ensure the period is defined in the product
|
||||
recurring_period = models.IntegerField(choices = RecurringPeriod.choices,
|
||||
default = RecurringPeriod.PER_30D)
|
||||
# recurring_period = models.IntegerField(choices = RecurringPeriod.choices,
|
||||
# default = RecurringPeriod.PER_30D)
|
||||
recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True)
|
||||
|
||||
one_time_price = models.DecimalField(default=0.0,
|
||||
max_digits=AMOUNT_MAX_DIGITS,
|
||||
|
|
@ -591,7 +615,6 @@ class Order(models.Model):
|
|||
blank=True,
|
||||
null=True)
|
||||
|
||||
|
||||
should_be_billed = models.BooleanField(default=True)
|
||||
|
||||
@property
|
||||
|
|
@ -700,6 +723,29 @@ class Order(models.Model):
|
|||
self.ending_date = end_before(new_order.starting_date)
|
||||
self.save()
|
||||
|
||||
def update_order(self, config, starting_date=None):
|
||||
"""
|
||||
Updating an order means creating a new order and reference the previous order
|
||||
"""
|
||||
|
||||
if not starting_date:
|
||||
starting_date = timezone.now()
|
||||
|
||||
new_order = self.__class__(owner=self.owner,
|
||||
billing_address=self.billing_address,
|
||||
product=self.product,
|
||||
starting_date=starting_date,
|
||||
config=config)
|
||||
|
||||
(new_order_one_time_price, new_order_recurring_price) = new_order.prices
|
||||
|
||||
new_order.replaces = self
|
||||
new_order.save()
|
||||
|
||||
self.ending_date = end_before(new_order.starting_date)
|
||||
self.save()
|
||||
|
||||
|
||||
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")
|
||||
|
|
@ -778,12 +824,14 @@ class Order(models.Model):
|
|||
is_recurring_record=True)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@property
|
||||
def prices(self):
|
||||
one_time_price = 0
|
||||
recurring_price = 0
|
||||
|
||||
# FIXME: support amount independent one time prices
|
||||
# FIXME: support a base price
|
||||
# FIXME: adjust to the selected recurring_period
|
||||
|
||||
if 'features' in self.product.config:
|
||||
for feature in self.product.config['features']:
|
||||
|
|
@ -794,12 +842,14 @@ class Order(models.Model):
|
|||
one_time_price += self.product.config['features'][feature]['one_time_price'] * self.config['features'][feature]
|
||||
recurring_price += self.product.config['features'][feature]['recurring_price'] * self.config['features'][feature]
|
||||
|
||||
return (one_time_price, recurring_price)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Calculate the price of the order when we create it
|
||||
# IMMUTABLE fields -- need to create new order to modify them
|
||||
# However this is not enforced here...
|
||||
if self._state.adding:
|
||||
self.one_time_price = one_time_price
|
||||
self.recurring_price = recurring_price
|
||||
(self.one_time_price, self.recurring_price) = self.prices
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
|
@ -1137,50 +1187,50 @@ class BillRecord(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
# Sample products included into uncloud
|
||||
class SampleOneTimeProduct(models.Model):
|
||||
"""
|
||||
Products are usually more complex, but this product shows how easy
|
||||
it can be to create your own one time product.
|
||||
"""
|
||||
# # Sample products included into uncloud
|
||||
# class SampleOneTimeProduct(models.Model):
|
||||
# """
|
||||
# Products are usually more complex, but this product shows how easy
|
||||
# it can be to create your own one time product.
|
||||
# """
|
||||
|
||||
default_recurring_period = RecurringPeriod.ONE_TIME
|
||||
# default_recurring_period = RecurringPeriod.ONE_TIME
|
||||
|
||||
ot_price = models.IntegerField(default=5)
|
||||
# ot_price = models.IntegerField(default=5)
|
||||
|
||||
@property
|
||||
def one_time_price(self):
|
||||
return self.ot_price
|
||||
# @property
|
||||
# def one_time_price(self):
|
||||
# return self.ot_price
|
||||
|
||||
class SampleRecurringProduct(models.Model):
|
||||
"""
|
||||
Products are usually more complex, but this product shows how easy
|
||||
it can be to create your own recurring fee product.
|
||||
"""
|
||||
# class SampleRecurringProduct(models.Model):
|
||||
# """
|
||||
# Products are usually more complex, but this product shows how easy
|
||||
# it can be to create your own recurring fee product.
|
||||
# """
|
||||
|
||||
default_recurring_period = RecurringPeriod.PER_30D
|
||||
# default_recurring_period = RecurringPeriod.PER_30D
|
||||
|
||||
rc_price = models.IntegerField(default=10)
|
||||
# rc_price = models.IntegerField(default=10)
|
||||
|
||||
@property
|
||||
def recurring_price(self):
|
||||
return self.rc_price
|
||||
# @property
|
||||
# def recurring_price(self):
|
||||
# return self.rc_price
|
||||
|
||||
class SampleRecurringProductOneTimeFee(models.Model):
|
||||
"""
|
||||
Products are usually more complex, but this product shows how easy
|
||||
it can be to create your own one time + recurring fee product.
|
||||
"""
|
||||
# class SampleRecurringProductOneTimeFee(models.Model):
|
||||
# """
|
||||
# Products are usually more complex, but this product shows how easy
|
||||
# it can be to create your own one time + recurring fee product.
|
||||
# """
|
||||
|
||||
default_recurring_period = RecurringPeriod.PER_30D
|
||||
# default_recurring_period = RecurringPeriod.PER_30D
|
||||
|
||||
ot_price = models.IntegerField(default=5)
|
||||
rc_price = models.IntegerField(default=10)
|
||||
# ot_price = models.IntegerField(default=5)
|
||||
# rc_price = models.IntegerField(default=10)
|
||||
|
||||
@property
|
||||
def one_time_price(self):
|
||||
return self.ot_price
|
||||
# @property
|
||||
# def one_time_price(self):
|
||||
# return self.ot_price
|
||||
|
||||
@property
|
||||
def recurring_price(self):
|
||||
return self.rc_price
|
||||
# @property
|
||||
# def recurring_price(self):
|
||||
# return self.rc_price
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue