diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py index 5e93479e..39942379 100644 --- a/orchestra/apps/orders/billing.py +++ b/orchestra/apps/orders/billing.py @@ -46,6 +46,7 @@ class BillsBackend(object): def format_period(self, ini, end): ini = ini.strftime("%b, %Y") end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y") + # TODO if diff is less than a month: write the month only if ini == end: return ini return _("{ini} to {end}").format(ini=ini, end=end) diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 5054b2d9..6a7b2eda 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -1,3 +1,4 @@ +import datetime import decimal import logging import sys @@ -167,8 +168,9 @@ class Order(models.Model): metrics = self.metrics.filter(updated_on__lt=end, updated_on__gte=ini) elif len(args) == 1: date = args[0] - metrics = self.metrics.filter(updated_on__year=date.year, - updated_on__month=date.month, updated_on__day=date.day) + date = datetime.date(year=date.year, month=date.month, day=date.day) + date += datetime.timedelta(days=1) + metrics = self.metrics.filter(updated_on__lt=date) elif not args: return self.metrics.latest('updated_on').value else: diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py index 0b1a522d..2dde24ea 100644 --- a/orchestra/apps/orders/tests/functional_tests/tests.py +++ b/orchestra/apps/orders/tests/functional_tests/tests.py @@ -330,23 +330,24 @@ class TrafficBillingTest(BaseTrafficBillingTest): class TrafficPrepayBillingTest(BaseTrafficBillingTest): - METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(service__name='traffic prepay').last(), 'amount', 0), 0)" + METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(is_active=True, service__name='traffic prepay').last(), 'amount', 0), 0)" def create_prepay_service(self): service = Service.objects.create( description="Traffic prepay", content_type=ContentType.objects.get_for_model(Miscellaneous), match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'traffic prepay'", - billing_period=Service.ANUAL, - billing_point=Service.FIXED_DATE, + billing_period=Service.MONTHLY, + # make sure full months are always paid + billing_point=Service.ON_REGISTER, is_fee=False, metric="miscellaneous.amount", - pricing_period=Service.BILLING_PERIOD, + pricing_period=Service.NEVER, rate_algorithm=Service.STEP_PRICE, - on_cancel=Service.NOTHING, # TODO on_register == NOTHING or make on_cancel generic + on_cancel=Service.NOTHING, payment_style=Service.PREPAY, tax=0, - nominal_price=5 + nominal_price=50 ) return service @@ -361,37 +362,43 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest): service = self.create_traffic_service() prepay_service = self.create_prepay_service() account = self.create_account() - self.create_traffic_resource() - prepay = self.create_prepay(10, account=account) - self.report_traffic(account, timezone.now(), 10**9) + now = timezone.now() + + prepay = self.create_prepay(10, account=account) + bill = account.orders.bill(proforma=True)[0] + self.assertEqual(10*50, bill.get_total()) + + self.report_traffic(account, timezone.now(), 10**10) + with freeze_time(now+relativedelta(months=1)): + bill = account.orders.bill(proforma=True, new_open=True)[0] + self.assertEqual(2*10*50 + 0*10, bill.get_total()) + + # TODO dateutils.relativedelta is buggy with fakedatetime + # TODO RuntimeWarning: DateTimeField MetricStorage.updated_on received a naive + self.report_traffic(account, timezone.now(), 10**10) + with freeze_time(now+relativedelta(months=1)): + bill = account.orders.bill(proforma=True, new_open=True)[0] + self.assertEqual(2*10*50 + 0*10, bill.get_total()) + + self.report_traffic(account, timezone.now(), 10**10) + with freeze_time(now+relativedelta(months=1)): + bill = account.orders.bill(proforma=True, new_open=True)[0] + self.assertEqual(2*10*50 + (30-10-10)*10, bill.get_total()) + + with freeze_time(now+relativedelta(months=2)): + self.report_traffic(account, timezone.now(), 10**11) + with freeze_time(now+relativedelta(months=1)): + bill = account.orders.bill(proforma=True, new_open=True)[0] + self.assertEqual(2*10*50 + (30-10-10)*10, bill.get_total()) + + with freeze_time(now+relativedelta(months=3)): + bill = account.orders.bill(proforma=True, new_open=True)[0] + self.assertEqual(4*10*50 + (30-10-10)*10 + (100-10-10)*10, bill.get_total()) - print prepay_service.orders.all() # TODO metric on the current day! how to solve it consistently? # TODO prepay doesnt allow for discount - - # move into the past - # TODO with patch.object(timezone, 'now', return_value=now+relativedelta(years=1)): - delta = datetime.timedelta(days=60) - date = (timezone.now()-delta).date() - order = service.orders.get() - order.registered_on = date - order.save() - - metric = order.metrics.latest() - metric.updated_on -= delta - metric.save() - - bills = service.orders.bill(proforma=True) - self.assertEqual(0, bills[0].get_total()) - - self.report_traffic(account, date, 10**10*9) - metric = order.metrics.latest() - metric.updated_on -= delta - metric.save() - - bills = service.orders.bill(proforma=True) - self.assertEqual((90-10-10)*10, bills[0].get_total()) + class MailboxBillingTest(BaseBillingTest): @@ -583,3 +590,6 @@ class PlanBillingTest(BaseBillingTest): def test_plan(self): pass + + +# TODO web disk size diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 16fc7708..5c4a4686 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -73,8 +73,7 @@ class ServiceHandler(plugins.Plugin): day = 1 else: raise NotImplementedError(msg) - bp = datetime.datetime(year=date.year, month=date.month, day=day, - tzinfo=timezone.get_current_timezone()).date() + bp = datetime.date(year=date.year, month=date.month, day=day) elif self.billing_period == self.ANUAL: if self.billing_point == self.ON_REGISTER: month = order.registered_on.month @@ -89,16 +88,24 @@ class ServiceHandler(plugins.Plugin): year = bp.year - relativedelta.relativedelta(years=1) if bp.month >= month: year = bp.year + 1 - bp = datetime.datetime(year=year, month=month, day=day, - tzinfo=timezone.get_current_timezone()).date() + bp = datetime.date(year=year, month=month, day=day) elif self.billing_period == self.NEVER: bp = order.registered_on else: raise NotImplementedError(msg) if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp: - return order.cancelled_on + bp = order.cancelled_on return bp +# 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 + def get_price_size(self, ini, end): rdelta = relativedelta.relativedelta(end, ini) if self.billing_period == self.MONTHLY: @@ -126,11 +133,9 @@ class ServiceHandler(plugins.Plugin): period = self.get_pricing_period() rdelta = self.get_pricing_rdelta() if period == self.MONTHLY: - ini = datetime.datetime(year=ini.year, month=ini.month, day=day, - tzinfo=timezone.get_current_timezone()).date() + ini = datetime.date(year=ini.year, month=ini.month, day=day) elif period == self.ANUAL: - ini = datetime.datetime(year=ini.year, month=month, day=day, - tzinfo=timezone.get_current_timezone()).date() + ini = datetime.date(year=ini.year, month=month, day=day) elif period == self.NEVER: yield ini, end raise StopIteration @@ -246,12 +251,13 @@ class ServiceHandler(plugins.Plugin): for order in porders: bu = getattr(order, 'new_billed_until', order.billed_until) if bu: - if order.registered_on > ini and order.registered_on <= end: + registered = order.registered_on + if registered > ini and registered <= end: counter += 1 - if order.registered_on != bu and bu > ini and bu <= end: + if registered != bu and bu > ini and bu <= end: counter += 1 if order.billed_until and order.billed_until != bu: - if order.registered_on != 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 return counter @@ -331,6 +337,7 @@ class ServiceHandler(plugins.Plugin): # In most cases: # ini >= registered_date, end < registered_date # boundary lookup and exclude cancelled and billed + # TODO service.payment_style == self.POSTPAY no discounts no shit on_cancel orders_ = [] bp = None ini = datetime.date.max @@ -339,7 +346,7 @@ class ServiceHandler(plugins.Plugin): cini = order.registered_on if order.billed_until: # exclude cancelled and billed - if self.on_cancel != self.REFOUND: + if self.on_cancel != self.REFUND: if order.cancelled_on and order.billed_until > order.cancelled_on: continue cini = order.billed_until @@ -413,20 +420,22 @@ class ServiceHandler(plugins.Plugin): order.new_billed_until = bp if self.get_pricing_period() == self.NEVER: # Changes (Mailbox disk-like) - for ini, end, metric in order.get_metric(ini, bp, changes=True): + for cini, cend, metric in order.get_metric(ini, bp, changes=True): price = self.get_price(order, metric) - lines.append(self.generate_line(order, price, ini, end, metric=metric)) - else: + lines.append(self.generate_line(order, price, cini, cend, metric=metric)) + elif self.get_pricing_period() == self.billing_period: # pricing_slots (Traffic-like) - for ini, end in self.get_pricing_slots(ini, bp): - metric = order.get_metric(ini, end) + for cini, cend in self.get_pricing_slots(ini, bp): + metric = order.get_metric(cini, cend) price = self.get_price(order, metric) - lines.append(self.generate_line(order, price, ini, end, metric=metric)) + lines.append(self.generate_line(order, price, cini, cend, metric=metric)) + else: + raise NotImplementedError else: # One-time billing if order.billed_until: continue - date = order.registered_on + date = timezone.now().date() order.new_billed_until = date if self.get_pricing_period() == self.NEVER: # get metric (Job-like) diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index fef937ad..17b70f7f 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -77,6 +77,7 @@ autodiscover('handlers') class Service(models.Model): NEVER = '' +# DAILY = 'DAILY' MONTHLY = 'MONTHLY' ANUAL = 'ANUAL' TEN_DAYS = 'TEN_DAYS' @@ -90,7 +91,7 @@ class Service(models.Model): NOTHING = 'NOTHING' DISCOUNT = 'DISCOUNT' COMPENSATE = 'COMPENSATE' - REFOUND = 'REFOUND' + REFUND = 'REFUND' PREPAY = 'PREPAY' POSTPAY = 'POSTPAY' STEP_PRICE = 'STEP_PRICE' @@ -175,9 +176,27 @@ class Service(models.Model): (NOTHING, _("Nothing")), (DISCOUNT, _("Discount")), (COMPENSATE, _("Compensat")), - (REFOUND, _("Refound")), + (REFUND, _("Refund")), ), default=DISCOUNT) +# on_broken_period = models.CharField(_("on broken period", max_length=16, +# help_text=_("Defines the billing behaviour when periods are incomplete on register and on cancel"), +# choices=( +# (NOTHING, _("Nothing, period is atomic")), +# (DISCOUNT, _("Bill partially")), +# (COMPENSATE, _("Compensate on cancel")), +# (REFUND, _("Refund on cancel")), +# ), +# default=DISCOUNT) +# granularity = models.CharField(_("granularity"), max_length=16, +# help_text=_("Defines the minimum size a period can be broken into"), +# choices=( +# (DAILY, _("One day")), +# (MONTHLY, _("One month")), +# (ANUAL, _("One year")), +# ), +# default=DAILY, +# ) payment_style = models.CharField(_("payment style"), max_length=16, help_text=_("Designates whether this service should be paid after " "consumtion (postpay/on demand) or prepaid"), @@ -194,14 +213,14 @@ class Service(models.Model): # (ONE_MONTH, _("One month")), # ), # default=NEVER) -# refound_period = models.CharField(_("refound period"), max_length=16, -# help_text=_("Period in which automatic refound will be performed on " +# refund_period = models.CharField(_("refund period"), max_length=16, +# help_text=_("Period in which automatic refund will be performed on " # "service cancellation"), # choices=( -# (NEVER, _("Never refound")), +# (NEVER, _("Never refund")), # (TEN_DAYS, _("Ten days")), # (ONE_MONTH, _("One month")), -# (ALWAYS, _("Always refound")), +# (ALWAYS, _("Always refund")), # ), # default=NEVER, blank=True)