Handle edge cases of last day of the month of billing period.
This commit is contained in:
parent
dc722ec17a
commit
2b06652a5b
|
@ -21,29 +21,29 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
"""
|
"""
|
||||||
Separates all the logic of billing handling from the model allowing to better
|
Separates all the logic of billing handling from the model allowing to better
|
||||||
customize its behaviout
|
customize its behaviout
|
||||||
|
|
||||||
Relax and enjoy the journey.
|
Relax and enjoy the journey.
|
||||||
"""
|
"""
|
||||||
_PLAN = 'plan'
|
_PLAN = 'plan'
|
||||||
_COMPENSATION = 'compensation'
|
_COMPENSATION = 'compensation'
|
||||||
_PREPAY = 'prepay'
|
_PREPAY = 'prepay'
|
||||||
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, service):
|
||||||
self.service = service
|
self.service = service
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.service, attr)
|
return getattr(self.service, attr)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_choices(cls):
|
def get_choices(cls):
|
||||||
choices = super(ServiceHandler, cls).get_choices()
|
choices = super(ServiceHandler, cls).get_choices()
|
||||||
return [('', _("Default"))] + choices
|
return [('', _("Default"))] + choices
|
||||||
|
|
||||||
def validate_content_type(self, service):
|
def validate_content_type(self, service):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validate_expression(self, service, method):
|
def validate_expression(self, service, method):
|
||||||
try:
|
try:
|
||||||
obj = service.content_type.model_class().objects.all()[0]
|
obj = service.content_type.model_class().objects.all()[0]
|
||||||
|
@ -53,24 +53,24 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
bool(getattr(self, method)(obj))
|
bool(getattr(self, method)(obj))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValidationError(format_exception(exc))
|
raise ValidationError(format_exception(exc))
|
||||||
|
|
||||||
def validate_match(self, service):
|
def validate_match(self, service):
|
||||||
if not service.match:
|
if not service.match:
|
||||||
service.match = 'True'
|
service.match = 'True'
|
||||||
self.validate_expression(service, 'matches')
|
self.validate_expression(service, 'matches')
|
||||||
|
|
||||||
def validate_metric(self, service):
|
def validate_metric(self, service):
|
||||||
self.validate_expression(service, 'get_metric')
|
self.validate_expression(service, 'get_metric')
|
||||||
|
|
||||||
def validate_order_description(self, service):
|
def validate_order_description(self, service):
|
||||||
self.validate_expression(service, 'get_order_description')
|
self.validate_expression(service, 'get_order_description')
|
||||||
|
|
||||||
def get_content_type(self):
|
def get_content_type(self):
|
||||||
if not self.model:
|
if not self.model:
|
||||||
return self.content_type
|
return self.content_type
|
||||||
app_label, model = self.model.split('.')
|
app_label, model = self.model.split('.')
|
||||||
return ContentType.objects.get_by_natural_key(app_label, model.lower())
|
return ContentType.objects.get_by_natural_key(app_label, model.lower())
|
||||||
|
|
||||||
def get_expression_context(self, instance):
|
def get_expression_context(self, instance):
|
||||||
return {
|
return {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
@ -85,14 +85,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
'log10': math.log10,
|
'log10': math.log10,
|
||||||
'Decimal': decimal.Decimal,
|
'Decimal': decimal.Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
def matches(self, instance):
|
def matches(self, instance):
|
||||||
if not self.match:
|
if not self.match:
|
||||||
# Blank expressions always evaluate True
|
# Blank expressions always evaluate True
|
||||||
return True
|
return True
|
||||||
safe_locals = self.get_expression_context(instance)
|
safe_locals = self.get_expression_context(instance)
|
||||||
return eval(self.match, safe_locals)
|
return eval(self.match, safe_locals)
|
||||||
|
|
||||||
def get_ignore_delta(self):
|
def get_ignore_delta(self):
|
||||||
if self.ignore_period == self.NEVER:
|
if self.ignore_period == self.NEVER:
|
||||||
return None
|
return None
|
||||||
|
@ -104,14 +104,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
return datetime.timedelta(months=value)
|
return datetime.timedelta(months=value)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown unit %s" % unit)
|
raise ValueError("Unknown unit %s" % unit)
|
||||||
|
|
||||||
def get_order_ignore(self, order):
|
def get_order_ignore(self, order):
|
||||||
""" service trial delta """
|
""" service trial delta """
|
||||||
ignore_delta = self.get_ignore_delta()
|
ignore_delta = self.get_ignore_delta()
|
||||||
if ignore_delta and (order.cancelled_on-ignore_delta).date() <= order.registered_on:
|
if ignore_delta and (order.cancelled_on-ignore_delta).date() <= order.registered_on:
|
||||||
return True
|
return True
|
||||||
return order.ignore
|
return order.ignore
|
||||||
|
|
||||||
def get_ignore(self, instance):
|
def get_ignore(self, instance):
|
||||||
if self.ignore_superusers:
|
if self.ignore_superusers:
|
||||||
account = getattr(instance, 'account', instance)
|
account = getattr(instance, 'account', instance)
|
||||||
|
@ -120,7 +120,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if 'superuser' in settings.SERVICES_IGNORE_ACCOUNT_TYPE and account.is_superuser:
|
if 'superuser' in settings.SERVICES_IGNORE_ACCOUNT_TYPE and account.is_superuser:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_metric(self, instance):
|
def get_metric(self, instance):
|
||||||
if self.metric:
|
if self.metric:
|
||||||
safe_locals = self.get_expression_context(instance)
|
safe_locals = self.get_expression_context(instance)
|
||||||
|
@ -128,7 +128,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
return eval(self.metric, safe_locals)
|
return eval(self.metric, safe_locals)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise type(exc)("'%s' evaluating metric for '%s' service" % (exc, self.service))
|
raise type(exc)("'%s' evaluating metric for '%s' service" % (exc, self.service))
|
||||||
|
|
||||||
def get_order_description(self, instance):
|
def get_order_description(self, instance):
|
||||||
safe_locals = self.get_expression_context(instance)
|
safe_locals = self.get_expression_context(instance)
|
||||||
account = getattr(instance, 'account', instance)
|
account = getattr(instance, 'account', instance)
|
||||||
|
@ -136,7 +136,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if not self.order_description:
|
if not self.order_description:
|
||||||
return '%s: %s' % (ugettext(self.description), instance)
|
return '%s: %s' % (ugettext(self.description), instance)
|
||||||
return eval(self.order_description, safe_locals)
|
return eval(self.order_description, safe_locals)
|
||||||
|
|
||||||
def get_billing_point(self, order, bp=None, **options):
|
def get_billing_point(self, order, bp=None, **options):
|
||||||
cachable = bool(self.billing_point == self.FIXED_DATE and not options.get('fixed_point'))
|
cachable = bool(self.billing_point == self.FIXED_DATE and not options.get('fixed_point'))
|
||||||
if not cachable or bp is None:
|
if not cachable or bp is None:
|
||||||
|
@ -151,7 +151,10 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
else:
|
else:
|
||||||
date = timezone.now().date()
|
date = timezone.now().date()
|
||||||
if self.billing_point == self.ON_REGISTER:
|
if self.billing_point == self.ON_REGISTER:
|
||||||
day = order.registered_on.day
|
# handle edge cases of last day of the month:
|
||||||
|
# e.g. on March is 31 but on April 30
|
||||||
|
last_day_of_month = calendar.monthrange(date.year, date.month)[1]
|
||||||
|
day = min(last_day_of_month, order.registered_on.day)
|
||||||
elif self.billing_point == self.FIXED_DATE:
|
elif self.billing_point == self.FIXED_DATE:
|
||||||
day = 1
|
day = 1
|
||||||
else:
|
else:
|
||||||
|
@ -171,6 +174,11 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
year = bp.year - relativedelta.relativedelta(years=1)
|
year = bp.year - relativedelta.relativedelta(years=1)
|
||||||
if bp.month >= month:
|
if bp.month >= month:
|
||||||
year = bp.year + 1
|
year = bp.year + 1
|
||||||
|
|
||||||
|
# handle edge cases of last day of the month:
|
||||||
|
# e.g. on March is 31 but on April 30
|
||||||
|
last_day_of_month = calendar.monthrange(year,month)[1]
|
||||||
|
day = min(last_day_of_month, day)
|
||||||
bp = datetime.date(year=year, month=month, day=day)
|
bp = datetime.date(year=year, month=month, day=day)
|
||||||
elif self.billing_period == self.NEVER:
|
elif self.billing_period == self.NEVER:
|
||||||
bp = order.registered_on
|
bp = order.registered_on
|
||||||
|
@ -179,7 +187,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
|
if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
|
||||||
bp = order.cancelled_on
|
bp = order.cancelled_on
|
||||||
return bp
|
return bp
|
||||||
|
|
||||||
# def aligned(self, date):
|
# def aligned(self, date):
|
||||||
# if self.granularity == self.DAILY:
|
# if self.granularity == self.DAILY:
|
||||||
# return date
|
# return date
|
||||||
|
@ -188,7 +196,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
# elif self.granularity == self.ANUAL:
|
# elif self.granularity == self.ANUAL:
|
||||||
# return datetime.date(year=date.year, month=1, day=1)
|
# return datetime.date(year=date.year, month=1, day=1)
|
||||||
# raise NotImplementedError
|
# raise NotImplementedError
|
||||||
|
|
||||||
def get_price_size(self, ini, end):
|
def get_price_size(self, ini, end):
|
||||||
rdelta = relativedelta.relativedelta(end, ini)
|
rdelta = relativedelta.relativedelta(end, ini)
|
||||||
anual_prepay_of_monthly_pricing = bool(
|
anual_prepay_of_monthly_pricing = bool(
|
||||||
|
@ -211,7 +219,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
size = round(size, 2)
|
size = round(size, 2)
|
||||||
return decimal.Decimal(str(size))
|
return decimal.Decimal(str(size))
|
||||||
|
|
||||||
def get_pricing_slots(self, ini, end):
|
def get_pricing_slots(self, ini, end):
|
||||||
day = 1
|
day = 1
|
||||||
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
|
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
|
||||||
|
@ -235,7 +243,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if next >= end:
|
if next >= end:
|
||||||
break
|
break
|
||||||
ini = next
|
ini = next
|
||||||
|
|
||||||
def get_pricing_rdelta(self):
|
def get_pricing_rdelta(self):
|
||||||
period = self.get_pricing_period()
|
period = self.get_pricing_period()
|
||||||
if period == self.MONTHLY:
|
if period == self.MONTHLY:
|
||||||
|
@ -244,13 +252,13 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
return relativedelta.relativedelta(years=1)
|
return relativedelta.relativedelta(years=1)
|
||||||
elif period == self.NEVER:
|
elif period == self.NEVER:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def generate_discount(self, line, dtype, price):
|
def generate_discount(self, line, dtype, price):
|
||||||
line.discounts.append(AttrDict(**{
|
line.discounts.append(AttrDict(**{
|
||||||
'type': dtype,
|
'type': dtype,
|
||||||
'total': price,
|
'total': price,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
|
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
|
||||||
"""
|
"""
|
||||||
discounts: extra discounts to apply
|
discounts: extra discounts to apply
|
||||||
|
@ -263,7 +271,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
else:
|
else:
|
||||||
raise AttributeError("WTF is '%s'?" % dates)
|
raise AttributeError("WTF is '%s'?" % dates)
|
||||||
discounts = discounts or ()
|
discounts = discounts or ()
|
||||||
|
|
||||||
size = self.get_price_size(ini, end)
|
size = self.get_price_size(ini, end)
|
||||||
if not computed:
|
if not computed:
|
||||||
price = price * size
|
price = price * size
|
||||||
|
@ -277,7 +285,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
'metric': metric,
|
'metric': metric,
|
||||||
'discounts': [],
|
'discounts': [],
|
||||||
})
|
})
|
||||||
|
|
||||||
if subtotal > price:
|
if subtotal > price:
|
||||||
plan_discount = price-subtotal
|
plan_discount = price-subtotal
|
||||||
self.generate_discount(line, self._PLAN, plan_discount)
|
self.generate_discount(line, self._PLAN, plan_discount)
|
||||||
|
@ -290,7 +298,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if dprice:
|
if dprice:
|
||||||
self.generate_discount(line, dtype, dprice)
|
self.generate_discount(line, dtype, dprice)
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def assign_compensations(self, givers, receivers, **options):
|
def assign_compensations(self, givers, receivers, **options):
|
||||||
compensations = []
|
compensations = []
|
||||||
for order in givers:
|
for order in givers:
|
||||||
|
@ -313,7 +321,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if hasattr(order, 'new_billed_until'):
|
if hasattr(order, 'new_billed_until'):
|
||||||
order.billed_until = order.new_billed_until
|
order.billed_until = order.new_billed_until
|
||||||
order.save(update_fields=['billed_until'])
|
order.save(update_fields=['billed_until'])
|
||||||
|
|
||||||
def apply_compensations(self, order, only_beyond=False):
|
def apply_compensations(self, order, only_beyond=False):
|
||||||
dsize = 0
|
dsize = 0
|
||||||
ini = order.billed_until or order.registered_on
|
ini = order.billed_until or order.registered_on
|
||||||
|
@ -339,7 +347,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
new_end = cend
|
new_end = cend
|
||||||
dsize += self.get_price_size(comp.ini, cend)
|
dsize += self.get_price_size(comp.ini, cend)
|
||||||
return dsize, new_end
|
return dsize, new_end
|
||||||
|
|
||||||
def get_register_or_renew_events(self, porders, ini, end):
|
def get_register_or_renew_events(self, porders, ini, end):
|
||||||
counter = 0
|
counter = 0
|
||||||
for order in porders:
|
for order in porders:
|
||||||
|
@ -354,7 +362,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if registered != order.billed_until and order.billed_until > ini and order.billed_until <= end:
|
if registered != order.billed_until and order.billed_until > ini and order.billed_until <= end:
|
||||||
counter += 1
|
counter += 1
|
||||||
return counter
|
return counter
|
||||||
|
|
||||||
def bill_concurrent_orders(self, account, porders, rates, ini, end):
|
def bill_concurrent_orders(self, account, porders, rates, ini, end):
|
||||||
# Concurrent
|
# Concurrent
|
||||||
# Get pricing orders
|
# Get pricing orders
|
||||||
|
@ -403,7 +411,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
order, price, ini, end, discounts=discounts, computed=True)
|
order, price, ini, end, discounts=discounts, computed=True)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def bill_registered_or_renew_events(self, account, porders, rates):
|
def bill_registered_or_renew_events(self, account, porders, rates):
|
||||||
# Before registration
|
# Before registration
|
||||||
lines = []
|
lines = []
|
||||||
|
@ -431,7 +439,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def bill_with_orders(self, orders, account, **options):
|
def bill_with_orders(self, orders, account, **options):
|
||||||
# For the "boundary conditions" just think that:
|
# For the "boundary conditions" just think that:
|
||||||
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
||||||
|
@ -458,7 +466,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
end = max(end, bp)
|
end = max(end, bp)
|
||||||
orders_.append(order)
|
orders_.append(order)
|
||||||
orders = orders_
|
orders = orders_
|
||||||
|
|
||||||
# Compensation
|
# Compensation
|
||||||
related_orders = account.orders.filter(service=self.service)
|
related_orders = account.orders.filter(service=self.service)
|
||||||
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
||||||
|
@ -504,7 +512,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def bill_with_metric(self, orders, account, **options):
|
def bill_with_metric(self, orders, account, **options):
|
||||||
lines = []
|
lines = []
|
||||||
bp = None
|
bp = None
|
||||||
|
@ -512,7 +520,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
prepay_discount = 0
|
prepay_discount = 0
|
||||||
bp = self.get_billing_point(order, bp=bp, **options)
|
bp = self.get_billing_point(order, bp=bp, **options)
|
||||||
recharged_until = datetime.date.min
|
recharged_until = datetime.date.min
|
||||||
|
|
||||||
if (self.billing_period != self.NEVER and
|
if (self.billing_period != self.NEVER and
|
||||||
self.get_pricing_period() == self.NEVER and
|
self.get_pricing_period() == self.NEVER and
|
||||||
self.payment_style == self.PREPAY and order.billed_on):
|
self.payment_style == self.PREPAY and order.billed_on):
|
||||||
|
@ -633,7 +641,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
# Last processed metric for futrue recharges
|
# Last processed metric for futrue recharges
|
||||||
order.new_billed_metric = metric
|
order.new_billed_metric = metric
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def generate_bill_lines(self, orders, account, **options):
|
def generate_bill_lines(self, orders, account, **options):
|
||||||
if options.get('proforma', False):
|
if options.get('proforma', False):
|
||||||
options['commit'] = False
|
options['commit'] = False
|
||||||
|
|
Loading…
Reference in a new issue