2014-09-02 15:48:07 +00:00
|
|
|
import calendar
|
2014-09-03 13:56:02 +00:00
|
|
|
import datetime
|
2014-09-16 14:35:00 +00:00
|
|
|
import decimal
|
2015-04-02 16:14:55 +00:00
|
|
|
import math
|
2015-10-02 11:08:23 +00:00
|
|
|
from functools import cmp_to_key
|
2014-09-02 15:48:07 +00:00
|
|
|
|
2014-09-03 13:56:02 +00:00
|
|
|
from dateutil import relativedelta
|
2014-07-21 12:20:04 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2015-03-26 16:00:30 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2015-04-08 14:41:09 +00:00
|
|
|
from django.utils import timezone, translation
|
2023-11-17 12:25:13 +00:00
|
|
|
from django.utils.translation import gettext, gettext_lazy as _
|
2014-07-21 12:20:04 +00:00
|
|
|
|
2014-11-24 14:39:41 +00:00
|
|
|
from orchestra import plugins
|
2014-10-27 17:34:14 +00:00
|
|
|
from orchestra.utils.humanize import text2int
|
2015-10-02 11:08:23 +00:00
|
|
|
from orchestra.utils.python import AttrDict, format_exception
|
2014-07-21 12:20:04 +00:00
|
|
|
|
2014-09-14 09:52:45 +00:00
|
|
|
from . import settings, helpers
|
2014-09-02 15:48:07 +00:00
|
|
|
|
2014-07-21 12:20:04 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
2014-09-02 15:48:07 +00:00
|
|
|
"""
|
|
|
|
Separates all the logic of billing handling from the model allowing to better
|
|
|
|
customize its behaviout
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-26 10:38:50 +00:00
|
|
|
Relax and enjoy the journey.
|
2014-09-02 15:48:07 +00:00
|
|
|
"""
|
2015-05-26 12:59:16 +00:00
|
|
|
_PLAN = 'plan'
|
2015-04-20 14:23:10 +00:00
|
|
|
_COMPENSATION = 'compensation'
|
2015-09-10 10:34:07 +00:00
|
|
|
_PREPAY = 'prepay'
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 12:20:04 +00:00
|
|
|
model = None
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 12:20:04 +00:00
|
|
|
def __init__(self, service):
|
|
|
|
self.service = service
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 15:43:36 +00:00
|
|
|
def __getattr__(self, attr):
|
2023-11-17 12:25:13 +00:00
|
|
|
if attr.startswith('__'):
|
|
|
|
raise AttributeError(f'{self} does not have attribute {attr}')
|
2014-07-21 15:43:36 +00:00
|
|
|
return getattr(self.service, attr)
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 12:20:04 +00:00
|
|
|
@classmethod
|
2015-03-31 12:39:08 +00:00
|
|
|
def get_choices(cls):
|
2023-11-17 12:25:13 +00:00
|
|
|
choices = super().get_choices()
|
2014-07-21 12:20:04 +00:00
|
|
|
return [('', _("Default"))] + choices
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-11-05 20:22:01 +00:00
|
|
|
def validate_content_type(self, service):
|
|
|
|
pass
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def validate_expression(self, service, method):
|
2014-11-05 20:22:01 +00:00
|
|
|
try:
|
|
|
|
obj = service.content_type.model_class().objects.all()[0]
|
|
|
|
except IndexError:
|
|
|
|
return
|
|
|
|
try:
|
2015-04-20 14:23:10 +00:00
|
|
|
bool(getattr(self, method)(obj))
|
2015-05-08 14:05:57 +00:00
|
|
|
except Exception as exc:
|
2015-04-27 12:24:17 +00:00
|
|
|
raise ValidationError(format_exception(exc))
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def validate_match(self, service):
|
|
|
|
if not service.match:
|
|
|
|
service.match = 'True'
|
|
|
|
self.validate_expression(service, 'matches')
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-11-05 20:22:01 +00:00
|
|
|
def validate_metric(self, service):
|
2015-04-20 14:23:10 +00:00
|
|
|
self.validate_expression(service, 'get_metric')
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def validate_order_description(self, service):
|
|
|
|
self.validate_expression(service, 'get_order_description')
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 15:43:36 +00:00
|
|
|
def get_content_type(self):
|
|
|
|
if not self.model:
|
|
|
|
return self.content_type
|
|
|
|
app_label, model = self.model.split('.')
|
|
|
|
return ContentType.objects.get_by_natural_key(app_label, model.lower())
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def get_expression_context(self, instance):
|
|
|
|
return {
|
2014-10-04 09:29:18 +00:00
|
|
|
'instance': instance,
|
|
|
|
'obj': instance,
|
2023-11-17 12:25:13 +00:00
|
|
|
'gettext': gettext,
|
2015-04-20 14:23:10 +00:00
|
|
|
'handler': self,
|
|
|
|
'service': self.service,
|
2014-10-04 09:29:18 +00:00
|
|
|
instance._meta.model_name: instance,
|
2015-04-20 14:23:10 +00:00
|
|
|
'math': math,
|
|
|
|
'logsteps': lambda n, size=1: \
|
|
|
|
round(n/(decimal.Decimal(size*10**int(math.log10(max(n, 1))))))*size*10**int(math.log10(max(n, 1))),
|
|
|
|
'log10': math.log10,
|
|
|
|
'Decimal': decimal.Decimal,
|
2014-07-21 12:20:04 +00:00
|
|
|
}
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
def matches(self, instance):
|
|
|
|
if not self.match:
|
|
|
|
# Blank expressions always evaluate True
|
|
|
|
return True
|
|
|
|
safe_locals = self.get_expression_context(instance)
|
2014-07-21 12:20:04 +00:00
|
|
|
return eval(self.match, safe_locals)
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-10-24 10:16:46 +00:00
|
|
|
def get_ignore_delta(self):
|
|
|
|
if self.ignore_period == self.NEVER:
|
|
|
|
return None
|
|
|
|
value, unit = self.ignore_period.split('_')
|
|
|
|
value = text2int(value)
|
2014-10-28 09:51:27 +00:00
|
|
|
if unit.lower().startswith('day'):
|
|
|
|
return datetime.timedelta(days=value)
|
|
|
|
if unit.lower().startswith('month'):
|
|
|
|
return datetime.timedelta(months=value)
|
2014-10-24 10:16:46 +00:00
|
|
|
else:
|
|
|
|
raise ValueError("Unknown unit %s" % unit)
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-10-24 10:16:46 +00:00
|
|
|
def get_order_ignore(self, order):
|
|
|
|
""" service trial delta """
|
|
|
|
ignore_delta = self.get_ignore_delta()
|
|
|
|
if ignore_delta and (order.cancelled_on-ignore_delta).date() <= order.registered_on:
|
|
|
|
return True
|
|
|
|
return order.ignore
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-10-24 10:16:46 +00:00
|
|
|
def get_ignore(self, instance):
|
2015-04-02 16:14:55 +00:00
|
|
|
if self.ignore_superusers:
|
|
|
|
account = getattr(instance, 'account', instance)
|
2015-04-16 13:15:21 +00:00
|
|
|
if account.type in settings.SERVICES_IGNORE_ACCOUNT_TYPE:
|
|
|
|
return True
|
|
|
|
if 'superuser' in settings.SERVICES_IGNORE_ACCOUNT_TYPE and account.is_superuser:
|
|
|
|
return True
|
2015-04-02 16:14:55 +00:00
|
|
|
return False
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-07-21 12:20:04 +00:00
|
|
|
def get_metric(self, instance):
|
2014-07-21 15:43:36 +00:00
|
|
|
if self.metric:
|
2015-04-20 14:23:10 +00:00
|
|
|
safe_locals = self.get_expression_context(instance)
|
2015-04-02 16:14:55 +00:00
|
|
|
try:
|
|
|
|
return eval(self.metric, safe_locals)
|
2015-05-08 14:05:57 +00:00
|
|
|
except Exception as exc:
|
2015-10-15 22:31:54 +00:00
|
|
|
raise type(exc)("'%s' evaluating metric for '%s' service" % (exc, self.service))
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-10-23 15:38:46 +00:00
|
|
|
def get_order_description(self, instance):
|
2015-04-20 14:23:10 +00:00
|
|
|
safe_locals = self.get_expression_context(instance)
|
2015-04-08 14:41:09 +00:00
|
|
|
account = getattr(instance, 'account', instance)
|
|
|
|
with translation.override(account.language):
|
|
|
|
if not self.order_description:
|
2023-11-17 12:25:13 +00:00
|
|
|
return '%s: %s' % (gettext(self.description), instance)
|
2015-04-08 14:41:09 +00:00
|
|
|
return eval(self.order_description, safe_locals)
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-02 15:48:07 +00:00
|
|
|
def get_billing_point(self, order, bp=None, **options):
|
2015-05-25 19:16:07 +00:00
|
|
|
cachable = bool(self.billing_point == self.FIXED_DATE and not options.get('fixed_point'))
|
|
|
|
if not cachable or bp is None:
|
2015-05-12 14:04:20 +00:00
|
|
|
bp = options.get('billing_point') or timezone.now().date()
|
2014-09-02 15:48:07 +00:00
|
|
|
if not options.get('fixed_point'):
|
2014-09-03 13:56:02 +00:00
|
|
|
msg = ("Support for '%s' period and '%s' point is not implemented"
|
|
|
|
% (self.get_billing_period_display(), self.get_billing_point_display()))
|
|
|
|
if self.billing_period == self.MONTHLY:
|
2014-09-02 15:48:07 +00:00
|
|
|
date = bp
|
2014-09-03 13:56:02 +00:00
|
|
|
if self.payment_style == self.PREPAY:
|
|
|
|
date += relativedelta.relativedelta(months=1)
|
2014-09-22 15:59:53 +00:00
|
|
|
else:
|
|
|
|
date = timezone.now().date()
|
2014-09-03 13:56:02 +00:00
|
|
|
if self.billing_point == self.ON_REGISTER:
|
2021-03-31 10:11:53 +00:00
|
|
|
# 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)
|
2014-09-03 13:56:02 +00:00
|
|
|
elif self.billing_point == self.FIXED_DATE:
|
2014-09-02 15:48:07 +00:00
|
|
|
day = 1
|
2014-09-03 13:56:02 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError(msg)
|
2014-09-25 16:28:47 +00:00
|
|
|
bp = datetime.date(year=date.year, month=date.month, day=day)
|
2014-09-03 13:56:02 +00:00
|
|
|
elif self.billing_period == self.ANUAL:
|
|
|
|
if self.billing_point == self.ON_REGISTER:
|
2014-09-02 15:48:07 +00:00
|
|
|
month = order.registered_on.month
|
|
|
|
day = order.registered_on.day
|
2014-09-03 13:56:02 +00:00
|
|
|
elif self.billing_point == self.FIXED_DATE:
|
2014-09-17 10:32:29 +00:00
|
|
|
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
|
2014-09-02 15:48:07 +00:00
|
|
|
day = 1
|
2014-09-03 13:56:02 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError(msg)
|
2014-09-02 15:48:07 +00:00
|
|
|
year = bp.year
|
2014-09-03 13:56:02 +00:00
|
|
|
if self.payment_style == self.POSTPAY:
|
2014-09-22 15:59:53 +00:00
|
|
|
year = bp.year - relativedelta.relativedelta(years=1)
|
2014-09-02 15:48:07 +00:00
|
|
|
if bp.month >= month:
|
|
|
|
year = bp.year + 1
|
2021-03-31 10:11:53 +00:00
|
|
|
|
|
|
|
# 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)
|
2014-09-25 16:28:47 +00:00
|
|
|
bp = datetime.date(year=year, month=month, day=day)
|
2014-09-03 13:56:02 +00:00
|
|
|
elif self.billing_period == self.NEVER:
|
2014-09-02 15:48:07 +00:00
|
|
|
bp = order.registered_on
|
|
|
|
else:
|
2014-09-03 13:56:02 +00:00
|
|
|
raise NotImplementedError(msg)
|
|
|
|
if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
|
2014-09-25 16:28:47 +00:00
|
|
|
bp = order.cancelled_on
|
2014-09-02 15:48:07 +00:00
|
|
|
return bp
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-25 16:28:47 +00:00
|
|
|
# def aligned(self, date):
|
|
|
|
# if self.granularity == self.DAILY:
|
|
|
|
# return date
|
|
|
|
# elif self.granularity == self.MONTHLY:
|
|
|
|
# return datetime.date(year=date.year, month=date.month, day=1)
|
|
|
|
# elif self.granularity == self.ANUAL:
|
|
|
|
# return datetime.date(year=date.year, month=1, day=1)
|
|
|
|
# raise NotImplementedError
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def get_price_size(self, ini, end):
|
2014-09-02 15:48:07 +00:00
|
|
|
rdelta = relativedelta.relativedelta(end, ini)
|
2015-09-07 13:07:04 +00:00
|
|
|
anual_prepay_of_monthly_pricing = bool(
|
|
|
|
self.billing_period == self.ANUAL and
|
|
|
|
self.payment_style == self.PREPAY and
|
|
|
|
self.get_pricing_period() == self.MONTHLY)
|
|
|
|
if self.billing_period == self.MONTHLY or anual_prepay_of_monthly_pricing:
|
2014-09-16 14:35:00 +00:00
|
|
|
size = rdelta.years * 12
|
|
|
|
size += rdelta.months
|
2014-09-03 13:56:02 +00:00
|
|
|
days = calendar.monthrange(end.year, end.month)[1]
|
2015-04-01 15:49:21 +00:00
|
|
|
size += decimal.Decimal(str(rdelta.days))/days
|
2014-09-16 14:35:00 +00:00
|
|
|
elif self.billing_period == self.ANUAL:
|
2014-09-02 15:48:07 +00:00
|
|
|
size = rdelta.years
|
2015-04-01 15:49:21 +00:00
|
|
|
size += decimal.Decimal(str(rdelta.months))/12
|
2014-09-03 13:56:02 +00:00
|
|
|
days = 366 if calendar.isleap(end.year) else 365
|
2015-04-01 15:49:21 +00:00
|
|
|
size += decimal.Decimal(str(rdelta.days))/days
|
2014-09-16 14:35:00 +00:00
|
|
|
elif self.billing_period == self.NEVER:
|
2014-09-02 15:48:07 +00:00
|
|
|
size = 1
|
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
2015-07-30 16:43:12 +00:00
|
|
|
size = round(size, 2)
|
2015-04-01 15:49:21 +00:00
|
|
|
return decimal.Decimal(str(size))
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-02 15:48:07 +00:00
|
|
|
def get_pricing_slots(self, ini, end):
|
2014-09-22 15:59:53 +00:00
|
|
|
day = 1
|
|
|
|
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
|
|
|
|
if self.billing_point == self.ON_REGISTER:
|
|
|
|
day = ini.day
|
|
|
|
month = ini.month
|
2014-09-02 15:48:07 +00:00
|
|
|
period = self.get_pricing_period()
|
2014-09-23 16:23:36 +00:00
|
|
|
rdelta = self.get_pricing_rdelta()
|
2014-09-03 13:56:02 +00:00
|
|
|
if period == self.MONTHLY:
|
2014-09-25 16:28:47 +00:00
|
|
|
ini = datetime.date(year=ini.year, month=ini.month, day=day)
|
2014-09-03 13:56:02 +00:00
|
|
|
elif period == self.ANUAL:
|
2014-09-25 16:28:47 +00:00
|
|
|
ini = datetime.date(year=ini.year, month=month, day=day)
|
2014-09-03 13:56:02 +00:00
|
|
|
elif period == self.NEVER:
|
2014-09-02 15:48:07 +00:00
|
|
|
yield ini, end
|
|
|
|
raise StopIteration
|
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
|
|
|
while True:
|
|
|
|
next = ini + rdelta
|
2014-09-22 15:59:53 +00:00
|
|
|
yield ini, next
|
2014-09-02 15:48:07 +00:00
|
|
|
if next >= end:
|
|
|
|
break
|
|
|
|
ini = next
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-23 16:23:36 +00:00
|
|
|
def get_pricing_rdelta(self):
|
|
|
|
period = self.get_pricing_period()
|
|
|
|
if period == self.MONTHLY:
|
|
|
|
return relativedelta.relativedelta(months=1)
|
|
|
|
elif period == self.ANUAL:
|
|
|
|
return relativedelta.relativedelta(years=1)
|
|
|
|
elif period == self.NEVER:
|
|
|
|
return None
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def generate_discount(self, line, dtype, price):
|
2014-09-26 15:05:20 +00:00
|
|
|
line.discounts.append(AttrDict(**{
|
2014-09-16 14:35:00 +00:00
|
|
|
'type': dtype,
|
|
|
|
'total': price,
|
|
|
|
}))
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2015-05-27 14:05:25 +00:00
|
|
|
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
|
2015-05-28 09:43:57 +00:00
|
|
|
"""
|
2015-09-04 10:22:14 +00:00
|
|
|
discounts: extra discounts to apply
|
2015-05-28 09:43:57 +00:00
|
|
|
computed: price = price*size already performed
|
|
|
|
"""
|
2014-09-23 14:01:58 +00:00
|
|
|
if len(dates) == 2:
|
|
|
|
ini, end = dates
|
|
|
|
elif len(dates) == 1:
|
|
|
|
ini, end = dates[0], dates[0]
|
|
|
|
else:
|
2015-10-05 13:31:08 +00:00
|
|
|
raise AttributeError("WTF is '%s'?" % dates)
|
2015-05-27 14:05:25 +00:00
|
|
|
discounts = discounts or ()
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-23 14:01:58 +00:00
|
|
|
size = self.get_price_size(ini, end)
|
|
|
|
if not computed:
|
|
|
|
price = price * size
|
|
|
|
subtotal = self.nominal_price * size * metric
|
2014-09-26 15:05:20 +00:00
|
|
|
line = AttrDict(**{
|
2014-09-10 16:53:09 +00:00
|
|
|
'order': order,
|
|
|
|
'subtotal': subtotal,
|
|
|
|
'ini': ini,
|
|
|
|
'end': end,
|
2014-09-23 14:01:58 +00:00
|
|
|
'size': size,
|
|
|
|
'metric': metric,
|
2014-09-16 14:35:00 +00:00
|
|
|
'discounts': [],
|
2014-09-10 16:53:09 +00:00
|
|
|
})
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
if subtotal > price:
|
2015-09-04 10:22:14 +00:00
|
|
|
plan_discount = price-subtotal
|
|
|
|
self.generate_discount(line, self._PLAN, plan_discount)
|
|
|
|
subtotal += plan_discount
|
|
|
|
for dtype, dprice in discounts:
|
|
|
|
subtotal += dprice
|
2015-09-10 10:34:07 +00:00
|
|
|
# Prevent compensations/prepays to refund money
|
|
|
|
if dtype in (self._COMPENSATION, self._PREPAY) and subtotal < 0:
|
2015-09-04 10:22:14 +00:00
|
|
|
dprice -= subtotal
|
|
|
|
if dprice:
|
|
|
|
self.generate_discount(line, dtype, dprice)
|
2014-09-16 14:35:00 +00:00
|
|
|
return line
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-23 16:23:36 +00:00
|
|
|
def assign_compensations(self, givers, receivers, **options):
|
2014-09-16 14:35:00 +00:00
|
|
|
compensations = []
|
|
|
|
for order in givers:
|
|
|
|
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
|
|
|
interval = helpers.Interval(order.cancelled_on, order.billed_until, order)
|
2014-09-18 15:07:39 +00:00
|
|
|
compensations.append(interval)
|
2014-09-16 14:35:00 +00:00
|
|
|
for order in receivers:
|
|
|
|
if not order.billed_until or order.billed_until < order.new_billed_until:
|
|
|
|
# receiver
|
|
|
|
ini = order.billed_until or order.registered_on
|
|
|
|
end = order.cancelled_on or datetime.date.max
|
2014-09-19 14:47:25 +00:00
|
|
|
interval = helpers.Interval(ini, end)
|
|
|
|
compensations, used_compensations = helpers.compensate(interval, compensations)
|
2014-09-16 14:35:00 +00:00
|
|
|
order._compensations = used_compensations
|
|
|
|
for comp in used_compensations:
|
2014-09-19 14:47:25 +00:00
|
|
|
comp.order.new_billed_until = min(comp.order.billed_until, comp.ini,
|
|
|
|
getattr(comp.order, 'new_billed_until', datetime.date.max))
|
2014-09-23 16:23:36 +00:00
|
|
|
if options.get('commit', True):
|
2014-09-16 14:35:00 +00:00
|
|
|
for order in givers:
|
|
|
|
if hasattr(order, 'new_billed_until'):
|
|
|
|
order.billed_until = order.new_billed_until
|
2014-09-30 16:06:42 +00:00
|
|
|
order.save(update_fields=['billed_until'])
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-19 14:47:25 +00:00
|
|
|
def apply_compensations(self, order, only_beyond=False):
|
|
|
|
dsize = 0
|
|
|
|
ini = order.billed_until or order.registered_on
|
|
|
|
end = order.new_billed_until
|
|
|
|
beyond = end
|
|
|
|
cend = None
|
2016-04-06 19:00:16 +00:00
|
|
|
new_end = None
|
2014-09-19 14:47:25 +00:00
|
|
|
for comp in getattr(order, '_compensations', []):
|
|
|
|
intersect = comp.intersect(helpers.Interval(ini=ini, end=end))
|
|
|
|
if intersect:
|
|
|
|
cini, cend = intersect.ini, intersect.end
|
|
|
|
if comp.end > beyond:
|
|
|
|
cend = comp.end
|
2016-04-06 19:00:16 +00:00
|
|
|
new_end = cend
|
2014-09-19 14:47:25 +00:00
|
|
|
if only_beyond:
|
|
|
|
cini = beyond
|
2015-09-04 10:22:14 +00:00
|
|
|
elif only_beyond:
|
2014-09-19 14:47:25 +00:00
|
|
|
continue
|
|
|
|
dsize += self.get_price_size(cini, cend)
|
|
|
|
# Extend billing point a little bit to benefit from a substantial discount
|
|
|
|
elif comp.end > beyond and (comp.end-comp.ini).days > 3*(comp.ini-beyond).days:
|
|
|
|
cend = comp.end
|
2016-04-06 19:00:16 +00:00
|
|
|
new_end = cend
|
2014-09-19 14:47:25 +00:00
|
|
|
dsize += self.get_price_size(comp.ini, cend)
|
2016-04-06 19:00:16 +00:00
|
|
|
return dsize, new_end
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def get_register_or_renew_events(self, porders, ini, end):
|
|
|
|
counter = 0
|
|
|
|
for order in porders:
|
|
|
|
bu = getattr(order, 'new_billed_until', order.billed_until)
|
|
|
|
if bu:
|
2014-09-25 16:28:47 +00:00
|
|
|
registered = order.registered_on
|
|
|
|
if registered > ini and registered <= end:
|
2014-09-16 14:35:00 +00:00
|
|
|
counter += 1
|
2014-09-25 16:28:47 +00:00
|
|
|
if registered != bu and bu > ini and bu <= end:
|
2014-09-16 14:35:00 +00:00
|
|
|
counter += 1
|
|
|
|
if order.billed_until and order.billed_until != bu:
|
2014-09-25 16:28:47 +00:00
|
|
|
if registered != order.billed_until and order.billed_until > ini and order.billed_until <= end:
|
2014-09-16 14:35:00 +00:00
|
|
|
counter += 1
|
|
|
|
return counter
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-23 16:23:36 +00:00
|
|
|
def bill_concurrent_orders(self, account, porders, rates, ini, end):
|
2014-09-16 14:35:00 +00:00
|
|
|
# Concurrent
|
|
|
|
# Get pricing orders
|
|
|
|
priced = {}
|
|
|
|
for ini, end, orders in helpers.get_chunks(porders, ini, end):
|
|
|
|
size = self.get_price_size(ini, end)
|
|
|
|
metric = len(orders)
|
|
|
|
interval = helpers.Interval(ini=ini, end=end)
|
2014-09-22 15:59:53 +00:00
|
|
|
for position, order in enumerate(orders, start=1):
|
2014-09-16 14:35:00 +00:00
|
|
|
csize = 0
|
|
|
|
compensations = getattr(order, '_compensations', [])
|
2014-09-19 14:47:25 +00:00
|
|
|
# Compensations < new_billed_until
|
2014-09-16 14:35:00 +00:00
|
|
|
for comp in compensations:
|
|
|
|
intersect = comp.intersect(interval)
|
|
|
|
if intersect:
|
|
|
|
csize += self.get_price_size(intersect.ini, intersect.end)
|
|
|
|
price = self.get_price(account, metric, position=position, rates=rates)
|
2014-09-23 14:01:58 +00:00
|
|
|
cprice = price * csize
|
2015-09-04 10:22:14 +00:00
|
|
|
price = price * size
|
2014-09-23 11:13:50 +00:00
|
|
|
if order in priced:
|
2014-09-16 14:35:00 +00:00
|
|
|
priced[order][0] += price
|
|
|
|
priced[order][1] += cprice
|
|
|
|
else:
|
2015-09-23 12:22:32 +00:00
|
|
|
priced[order] = [price, cprice]
|
2014-09-16 14:35:00 +00:00
|
|
|
lines = []
|
2015-04-02 16:14:55 +00:00
|
|
|
for order, prices in priced.items():
|
2015-09-04 10:22:14 +00:00
|
|
|
if hasattr(order, 'new_billed_until'):
|
|
|
|
discounts = ()
|
|
|
|
# Generate lines and discounts from order.nominal_price
|
|
|
|
price, cprice = prices
|
|
|
|
a = order.id
|
|
|
|
# Compensations > new_billed_until
|
|
|
|
dsize, new_end = self.apply_compensations(order, only_beyond=True)
|
|
|
|
cprice += dsize*price
|
|
|
|
if cprice:
|
|
|
|
discounts = (
|
|
|
|
(self._COMPENSATION, -cprice),
|
|
|
|
)
|
|
|
|
if new_end:
|
|
|
|
size = self.get_price_size(order.new_billed_until, new_end)
|
|
|
|
price += price*size
|
|
|
|
order.new_billed_until = new_end
|
2016-04-06 19:00:16 +00:00
|
|
|
ini = order.billed_until or order.registered_on
|
|
|
|
end = new_end or order.new_billed_until
|
2015-09-04 10:22:14 +00:00
|
|
|
line = self.generate_line(
|
2016-04-06 19:00:16 +00:00
|
|
|
order, price, ini, end, discounts=discounts, computed=True)
|
2015-09-04 10:22:14 +00:00
|
|
|
lines.append(line)
|
2014-09-16 14:35:00 +00:00
|
|
|
return lines
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-23 16:23:36 +00:00
|
|
|
def bill_registered_or_renew_events(self, account, porders, rates):
|
2014-09-16 14:35:00 +00:00
|
|
|
# Before registration
|
|
|
|
lines = []
|
2014-09-23 16:23:36 +00:00
|
|
|
rdelta = self.get_pricing_rdelta()
|
|
|
|
if not rdelta:
|
|
|
|
raise NotImplementedError
|
2014-09-22 15:59:53 +00:00
|
|
|
for position, order in enumerate(porders, start=1):
|
2014-09-16 14:35:00 +00:00
|
|
|
if hasattr(order, 'new_billed_until'):
|
2014-09-19 14:47:25 +00:00
|
|
|
pend = order.billed_until or order.registered_on
|
|
|
|
pini = pend - rdelta
|
|
|
|
metric = self.get_register_or_renew_events(porders, pini, pend)
|
2015-04-14 14:29:22 +00:00
|
|
|
position = min(position, metric)
|
2014-09-16 14:35:00 +00:00
|
|
|
price = self.get_price(account, metric, position=position, rates=rates)
|
2014-09-19 14:47:25 +00:00
|
|
|
ini = order.billed_until or order.registered_on
|
|
|
|
end = order.new_billed_until
|
|
|
|
discounts = ()
|
|
|
|
dsize, new_end = self.apply_compensations(order)
|
|
|
|
if dsize:
|
2014-09-26 15:05:20 +00:00
|
|
|
discounts=(
|
|
|
|
(self._COMPENSATION, -dsize*price),
|
|
|
|
)
|
2014-09-19 14:47:25 +00:00
|
|
|
if new_end:
|
|
|
|
order.new_billed_until = new_end
|
|
|
|
end = new_end
|
2014-09-23 14:01:58 +00:00
|
|
|
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
2014-09-16 14:35:00 +00:00
|
|
|
lines.append(line)
|
2014-09-22 15:59:53 +00:00
|
|
|
return lines
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def bill_with_orders(self, orders, account, **options):
|
2014-09-14 09:52:45 +00:00
|
|
|
# For the "boundary conditions" just think that:
|
|
|
|
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
|
|
|
# In most cases:
|
|
|
|
# ini >= registered_date, end < registered_date
|
2014-09-19 14:47:25 +00:00
|
|
|
# boundary lookup and exclude cancelled and billed
|
|
|
|
orders_ = []
|
|
|
|
bp = None
|
2014-09-14 09:52:45 +00:00
|
|
|
ini = datetime.date.max
|
2014-09-16 14:35:00 +00:00
|
|
|
end = datetime.date.min
|
2014-09-14 09:52:45 +00:00
|
|
|
for order in orders:
|
|
|
|
cini = order.registered_on
|
|
|
|
if order.billed_until:
|
2014-09-19 14:47:25 +00:00
|
|
|
# exclude cancelled and billed
|
2014-09-25 16:28:47 +00:00
|
|
|
if self.on_cancel != self.REFUND:
|
2014-09-19 14:47:25 +00:00
|
|
|
if order.cancelled_on and order.billed_until > order.cancelled_on:
|
|
|
|
continue
|
2014-09-14 09:52:45 +00:00
|
|
|
cini = order.billed_until
|
|
|
|
bp = self.get_billing_point(order, bp=bp, **options)
|
2015-05-13 12:16:51 +00:00
|
|
|
if order.billed_until and order.billed_until >= bp:
|
|
|
|
continue
|
2014-09-14 09:52:45 +00:00
|
|
|
order.new_billed_until = bp
|
|
|
|
ini = min(ini, cini)
|
2014-09-16 14:35:00 +00:00
|
|
|
end = max(end, bp)
|
2014-09-19 14:47:25 +00:00
|
|
|
orders_.append(order)
|
|
|
|
orders = orders_
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-19 14:47:25 +00:00
|
|
|
# Compensation
|
2014-09-17 10:32:29 +00:00
|
|
|
related_orders = account.orders.filter(service=self.service)
|
2014-09-26 10:38:50 +00:00
|
|
|
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
2014-09-14 19:36:27 +00:00
|
|
|
# Get orders pending for compensation
|
2014-09-19 14:47:25 +00:00
|
|
|
givers = list(related_orders.givers(ini, end))
|
2015-04-14 14:29:22 +00:00
|
|
|
givers = sorted(givers, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
|
|
|
orders = sorted(orders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
2014-09-23 16:23:36 +00:00
|
|
|
self.assign_compensations(givers, orders, **options)
|
2014-09-16 14:35:00 +00:00
|
|
|
rates = self.get_rates(account)
|
2014-09-23 16:23:36 +00:00
|
|
|
has_billing_period = self.billing_period != self.NEVER
|
|
|
|
has_pricing_period = self.get_pricing_period() != self.NEVER
|
|
|
|
if rates and (has_billing_period or has_pricing_period):
|
|
|
|
concurrent = has_billing_period and not has_pricing_period
|
|
|
|
if not concurrent:
|
|
|
|
rdelta = self.get_pricing_rdelta()
|
|
|
|
ini -= rdelta
|
2014-09-19 14:47:25 +00:00
|
|
|
porders = related_orders.pricing_orders(ini, end)
|
2014-09-16 14:35:00 +00:00
|
|
|
porders = list(set(orders).union(set(porders)))
|
2015-04-14 14:29:22 +00:00
|
|
|
porders = sorted(porders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
2014-09-23 16:23:36 +00:00
|
|
|
if concurrent:
|
|
|
|
# Periodic billing with no pricing period
|
|
|
|
lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
|
2014-09-16 14:35:00 +00:00
|
|
|
else:
|
2014-09-23 16:23:36 +00:00
|
|
|
# Periodic and one-time billing with pricing period
|
|
|
|
lines = self.bill_registered_or_renew_events(account, porders, rates)
|
2014-09-14 22:00:00 +00:00
|
|
|
else:
|
2014-09-23 16:23:36 +00:00
|
|
|
# No rates optimization or one-time billing without pricing period
|
2014-09-16 14:35:00 +00:00
|
|
|
lines = []
|
|
|
|
price = self.nominal_price
|
|
|
|
# Calculate nominal price
|
|
|
|
for order in orders:
|
2014-09-14 19:36:27 +00:00
|
|
|
ini = order.billed_until or order.registered_on
|
2014-09-16 14:35:00 +00:00
|
|
|
end = order.new_billed_until
|
2014-09-19 14:47:25 +00:00
|
|
|
discounts = ()
|
|
|
|
dsize, new_end = self.apply_compensations(order)
|
|
|
|
if dsize:
|
2014-09-24 20:09:41 +00:00
|
|
|
discounts=(
|
2014-09-26 15:05:20 +00:00
|
|
|
(self._COMPENSATION, -dsize*price),
|
2014-09-24 20:09:41 +00:00
|
|
|
)
|
2014-09-19 14:47:25 +00:00
|
|
|
if new_end:
|
|
|
|
order.new_billed_until = new_end
|
|
|
|
end = new_end
|
2014-09-23 14:01:58 +00:00
|
|
|
line = self.generate_line(order, price, ini, end, discounts=discounts)
|
2014-09-16 14:35:00 +00:00
|
|
|
lines.append(line)
|
|
|
|
return lines
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def bill_with_metric(self, orders, account, **options):
|
2014-09-02 15:48:07 +00:00
|
|
|
lines = []
|
2014-09-22 15:59:53 +00:00
|
|
|
bp = None
|
2014-09-02 15:48:07 +00:00
|
|
|
for order in orders:
|
2015-07-30 16:43:12 +00:00
|
|
|
prepay_discount = 0
|
2014-09-25 19:42:34 +00:00
|
|
|
bp = self.get_billing_point(order, bp=bp, **options)
|
2016-04-06 19:00:16 +00:00
|
|
|
recharged_until = datetime.date.min
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-25 19:42:34 +00:00
|
|
|
if (self.billing_period != self.NEVER and
|
|
|
|
self.get_pricing_period() == self.NEVER and
|
|
|
|
self.payment_style == self.PREPAY and order.billed_on):
|
|
|
|
# Recharge
|
|
|
|
if self.payment_style == self.PREPAY and order.billed_on:
|
2015-07-30 16:43:12 +00:00
|
|
|
recharges = []
|
2014-09-25 19:42:34 +00:00
|
|
|
rini = order.billed_on
|
2015-05-26 19:49:09 +00:00
|
|
|
rend = min(bp, order.billed_until)
|
2015-09-07 13:07:04 +00:00
|
|
|
bmetric = order.billed_metric
|
|
|
|
if bmetric is None:
|
|
|
|
bmetric = order.get_metric(order.billed_on)
|
2016-11-29 21:11:24 +00:00
|
|
|
bsize = self.get_price_size(rini, rend)
|
2015-07-30 16:43:12 +00:00
|
|
|
prepay_discount = self.get_price(account, bmetric) * bsize
|
|
|
|
prepay_discount = round(prepay_discount, 2)
|
2015-05-26 19:49:09 +00:00
|
|
|
for cini, cend, metric in order.get_metric(rini, rend, changes=True):
|
2016-11-29 21:11:24 +00:00
|
|
|
cini = max(cini, rini)
|
2015-05-26 19:43:13 +00:00
|
|
|
size = self.get_price_size(cini, cend)
|
2015-05-26 19:49:09 +00:00
|
|
|
price = self.get_price(account, metric) * size
|
2015-05-26 19:43:13 +00:00
|
|
|
discounts = ()
|
2015-07-30 16:43:12 +00:00
|
|
|
discount = min(price, max(prepay_discount, 0))
|
|
|
|
prepay_discount -= price
|
|
|
|
if discount > 0:
|
2015-05-27 14:05:25 +00:00
|
|
|
price -= discount
|
2015-05-26 19:43:13 +00:00
|
|
|
discounts = (
|
2015-09-10 10:34:07 +00:00
|
|
|
(self._PREPAY, -discount),
|
2015-05-26 19:43:13 +00:00
|
|
|
)
|
2015-07-30 16:43:12 +00:00
|
|
|
# Don't overdload bills with lots of lines
|
|
|
|
if price > 0:
|
|
|
|
recharges.append((order, price, cini, cend, metric, discounts))
|
|
|
|
if prepay_discount < 0:
|
|
|
|
# User has prepaid less than the actual consumption
|
2015-05-27 14:05:25 +00:00
|
|
|
for order, price, cini, cend, metric, discounts in recharges:
|
2015-09-10 10:34:07 +00:00
|
|
|
if discounts:
|
|
|
|
price -= discounts[0][1]
|
2015-05-27 14:05:25 +00:00
|
|
|
line = self.generate_line(order, price, cini, cend, metric=metric,
|
|
|
|
computed=True, discounts=discounts)
|
|
|
|
lines.append(line)
|
2016-04-06 19:00:16 +00:00
|
|
|
recharged_until = cend
|
2014-09-23 16:23:36 +00:00
|
|
|
if order.billed_until and order.cancelled_on and order.cancelled_on >= order.billed_until:
|
2015-07-30 16:43:12 +00:00
|
|
|
# Cancelled order
|
2014-09-23 14:01:58 +00:00
|
|
|
continue
|
2014-09-23 11:13:50 +00:00
|
|
|
if self.billing_period != self.NEVER:
|
2014-09-23 16:23:36 +00:00
|
|
|
ini = order.billed_until or order.registered_on
|
2016-04-06 19:00:16 +00:00
|
|
|
# ini = max(order.billed_until or order.registered_on, recharged_until)
|
2014-09-23 16:23:36 +00:00
|
|
|
# Periodic billing
|
|
|
|
if bp <= ini:
|
2015-07-30 16:43:12 +00:00
|
|
|
# Already billed
|
2014-09-23 16:23:36 +00:00
|
|
|
continue
|
|
|
|
order.new_billed_until = bp
|
2014-09-23 11:13:50 +00:00
|
|
|
if self.get_pricing_period() == self.NEVER:
|
2014-09-23 16:23:36 +00:00
|
|
|
# Changes (Mailbox disk-like)
|
2014-09-25 16:28:47 +00:00
|
|
|
for cini, cend, metric in order.get_metric(ini, bp, changes=True):
|
2016-04-06 19:00:16 +00:00
|
|
|
cini = max(recharged_until, cini)
|
2015-04-05 22:34:47 +00:00
|
|
|
price = self.get_price(account, metric)
|
2015-07-30 16:43:12 +00:00
|
|
|
discounts = ()
|
|
|
|
# Since the current datamodel can't guarantee to retrieve the exact
|
|
|
|
# state for calculating prepay_discount (service price could have change)
|
|
|
|
# maybe is it better not to discount anything.
|
|
|
|
# discount = min(price, max(prepay_discount, 0))
|
|
|
|
# if discount > 0:
|
|
|
|
# price -= discount
|
|
|
|
# prepay_discount -= discount
|
|
|
|
# discounts = (
|
2016-04-06 19:00:16 +00:00
|
|
|
# (self._PREPAY, -discount),
|
2015-07-30 16:43:12 +00:00
|
|
|
# )
|
|
|
|
if metric > 0:
|
|
|
|
line = self.generate_line(order, price, cini, cend, metric=metric,
|
|
|
|
discounts=discounts)
|
|
|
|
lines.append(line)
|
2014-09-25 16:28:47 +00:00
|
|
|
elif self.get_pricing_period() == self.billing_period:
|
2014-09-23 16:23:36 +00:00
|
|
|
# pricing_slots (Traffic-like)
|
2014-09-25 19:42:34 +00:00
|
|
|
if self.payment_style == self.PREPAY:
|
2016-04-06 19:00:16 +00:00
|
|
|
raise NotImplementedError(
|
|
|
|
"Metric with prepay and pricing_period == billing_period")
|
2014-09-25 16:28:47 +00:00
|
|
|
for cini, cend in self.get_pricing_slots(ini, bp):
|
|
|
|
metric = order.get_metric(cini, cend)
|
2015-04-05 22:34:47 +00:00
|
|
|
price = self.get_price(account, metric)
|
2015-07-30 16:43:12 +00:00
|
|
|
discounts = ()
|
|
|
|
# discount = min(price, max(prepay_discount, 0))
|
|
|
|
# if discount > 0:
|
|
|
|
# price -= discount
|
|
|
|
# prepay_discount -= discount
|
|
|
|
# discounts = (
|
2015-09-10 10:34:07 +00:00
|
|
|
# (self._PREPAY, -discount),
|
2015-07-30 16:43:12 +00:00
|
|
|
# )
|
|
|
|
if metric > 0:
|
|
|
|
line = self.generate_line(order, price, cini, cend, metric=metric,
|
|
|
|
discounts=discounts)
|
|
|
|
lines.append(line)
|
2015-09-07 13:07:04 +00:00
|
|
|
elif self.get_pricing_period() in (self.MONTHLY, self.ANUAL):
|
|
|
|
if self.payment_style == self.PREPAY:
|
|
|
|
# Traffic Prepay
|
|
|
|
metric = order.get_metric(timezone.now().date())
|
|
|
|
if metric > 0:
|
|
|
|
price = self.get_price(account, metric)
|
|
|
|
for cini, cend in self.get_pricing_slots(ini, bp):
|
|
|
|
line = self.generate_line(order, price, cini, cend, metric=metric)
|
|
|
|
lines.append(line)
|
|
|
|
else:
|
2016-04-06 19:00:16 +00:00
|
|
|
raise NotImplementedError(
|
|
|
|
"Metric with postpay and pricing_period in (monthly, anual)")
|
2014-09-25 16:28:47 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
2014-09-23 11:13:50 +00:00
|
|
|
else:
|
2014-09-23 16:23:36 +00:00
|
|
|
# One-time billing
|
|
|
|
if order.billed_until:
|
|
|
|
continue
|
2014-09-25 16:28:47 +00:00
|
|
|
date = timezone.now().date()
|
2014-09-23 16:23:36 +00:00
|
|
|
order.new_billed_until = date
|
2014-09-23 11:13:50 +00:00
|
|
|
if self.get_pricing_period() == self.NEVER:
|
2014-09-23 16:23:36 +00:00
|
|
|
# get metric (Job-like)
|
|
|
|
metric = order.get_metric(date)
|
2015-04-05 22:34:47 +00:00
|
|
|
price = self.get_price(account, metric)
|
2015-07-30 16:43:12 +00:00
|
|
|
line = self.generate_line(order, price, date, metric=metric)
|
|
|
|
lines.append(line)
|
2014-09-23 11:13:50 +00:00
|
|
|
else:
|
|
|
|
raise NotImplementedError
|
2015-07-30 16:43:12 +00:00
|
|
|
# Last processed metric for futrue recharges
|
|
|
|
order.new_billed_metric = metric
|
2014-09-02 15:48:07 +00:00
|
|
|
return lines
|
2021-03-31 10:11:53 +00:00
|
|
|
|
2014-09-16 14:35:00 +00:00
|
|
|
def generate_bill_lines(self, orders, account, **options):
|
2014-09-22 15:59:53 +00:00
|
|
|
if options.get('proforma', False):
|
|
|
|
options['commit'] = False
|
2014-09-16 14:35:00 +00:00
|
|
|
if not self.metric:
|
|
|
|
lines = self.bill_with_orders(orders, account, **options)
|
|
|
|
else:
|
|
|
|
lines = self.bill_with_metric(orders, account, **options)
|
2014-09-23 16:23:36 +00:00
|
|
|
if options.get('commit', True):
|
2014-09-25 18:21:17 +00:00
|
|
|
now = timezone.now().date()
|
2014-09-23 16:23:36 +00:00
|
|
|
for line in lines:
|
2014-09-25 19:42:34 +00:00
|
|
|
order = line.order
|
|
|
|
order.billed_on = now
|
2015-07-30 16:43:12 +00:00
|
|
|
order.billed_metric = getattr(order, 'new_billed_metric', order.billed_metric)
|
2014-09-25 19:42:34 +00:00
|
|
|
order.billed_until = getattr(order, 'new_billed_until', order.billed_until)
|
2015-07-30 16:43:12 +00:00
|
|
|
order.save(update_fields=('billed_on', 'billed_until', 'billed_metric'))
|
2014-09-16 14:35:00 +00:00
|
|
|
return lines
|