From 8f1d05873c7a6fe612db98f0b531eef7e4c7af41 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 22 Sep 2014 15:59:53 +0000 Subject: [PATCH] Fixes on billing order with metric --- TODO.md | 9 +- orchestra/apps/bills/models.py | 3 + orchestra/apps/orders/billing.py | 12 +- orchestra/apps/orders/models.py | 26 +- .../orders/tests/functional_tests/tests.py | 227 +++++++++++++++++- orchestra/apps/payments/admin.py | 3 + orchestra/apps/resources/backends.py | 2 +- orchestra/apps/resources/helpers.py | 16 +- orchestra/apps/resources/models.py | 14 +- orchestra/apps/resources/tasks.py | 19 +- orchestra/apps/services/handlers.py | 48 ++-- orchestra/apps/services/models.py | 5 +- orchestra/apps/services/tests/test_handler.py | 2 +- orchestra/apps/users/models.py | 3 +- 14 files changed, 310 insertions(+), 79 deletions(-) diff --git a/TODO.md b/TODO.md index 1527fcd6..b7818ad0 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,6 @@ TODO * add `BackendLog` retry action * move invoice contact to invoices app? -* wrapper around reverse('admin:....') `link()` and `link_factory()` * PHPbBckendMiixin with get_php_ini * Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]` * rename account.user to primary_user @@ -40,11 +39,8 @@ TODO Remember that, as always with QuerySets, any subsequent chained methods which imply a different database query will ignore previously cached results, and retrieve data using a fresh database query. * profile select_related vs prefetch_related -* use HTTP OPTIONS instead of configuration endpoint, or rename to settings? * Log changes from rest api (serialized objects) - - * passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict * pip install pyinotify @@ -105,3 +101,8 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon * create log file at /var/log/orchestra.log and rotate + +* order.register_at + @property + def register_on(self): + return order.register_at.date() diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index 0bf3b2b8..fe96fe5b 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -62,6 +62,9 @@ class Bill(models.Model): objects = BillManager() + class Meta: + get_latest_by = 'created_on' + def __unicode__(self): return self.number diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py index 343af520..218f0e6f 100644 --- a/orchestra/apps/orders/billing.py +++ b/orchestra/apps/orders/billing.py @@ -10,23 +10,27 @@ class BillsBackend(object): bill = None bills = [] create_new = options.get('new_open', False) - is_proforma = options.get('is_proforma', False) + proforma = options.get('proforma', False) for line in lines: service = line.order.service # Create bill if needed if bill is None or service.is_fee: - if is_proforma: + if proforma: if create_new: bill = ProForma.objects.create(account=account) else: - bill, __ = ProForma.objects.get_or_create(account=account, is_open=True) + bill = ProForma.objects.filter(account=account, is_open=True).last() + if not bill: + bill = ProForma.objects.create(account=account, is_open=True) elif service.is_fee: bill = Fee.objects.create(account=account) else: if create_new: bill = Invoice.objects.create(account=account) else: - bill, __ = Invoice.objects.get_or_create(account=account, is_open=True) + bill = Invoice.objects.filter(account=account, is_open=True).last() + if not bill: + bill = Invoice.objects.create(account=account, is_open=True) bills.append(bill) # Create bill line billine = bill.lines.create( diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 38ba1255..69263dde 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -1,3 +1,4 @@ +import decimal import logging import sys @@ -122,13 +123,15 @@ class Order(models.Model): def update(self): instance = self.content_object handler = self.service.handler + metric = '' if handler.metric: metric = handler.get_metric(instance) if metric is not None: MetricStorage.store(self, metric) + metric = ', metric:{}'.format(metric) description = "{}: {}".format(handler.description, str(instance)) - logger.info("UPDATED order id: {id} description:{description}".format( - id=self.id, description=description)) + logger.info("UPDATED order id:{id}, description:{description}{metric}".format( + id=self.id, description=description, metric=metric)) if self.description != description: self.description = description self.save() @@ -143,10 +146,10 @@ class Order(models.Model): class MetricStorage(models.Model): - order = models.ForeignKey(Order, verbose_name=_("order")) - value = models.BigIntegerField(_("value")) - created_on = models.DateField(_("created on"), auto_now_add=True) - updated_on = models.DateField(_("updated on"), auto_now=True) + order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics') + value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) + created_on = models.DateField(_("created"), auto_now_add=True) + updated_on = models.DateTimeField(_("updated")) class Meta: get_latest_by = 'created_on' @@ -156,23 +159,24 @@ class MetricStorage(models.Model): @classmethod def store(cls, order, value): + now = timezone.now() try: metric = cls.objects.filter(order=order).latest() except cls.DoesNotExist: - cls.objects.create(order=order, value=value) + cls.objects.create(order=order, value=value, updated_on=now) else: if metric.value != value: - cls.objects.create(order=order, value=value) + cls.objects.create(order=order, value=value, updated_on=now) else: + metric.updated_on = now metric.save() @classmethod def get(cls, order, ini, end): try: - return cls.objects.filter(order=order, updated_on__lt=end, - updated_on__gte=ini).latest('updated_on').value + return order.metrics.filter(updated_on__lt=end, updated_on__gte=ini).latest('updated_on').value except cls.DoesNotExist: - return 0 + return decimal.Decimal(0) _excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecorder.Migration) diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py index bc8a6c52..7e1180e3 100644 --- a/orchestra/apps/orders/tests/functional_tests/tests.py +++ b/orchestra/apps/orders/tests/functional_tests/tests.py @@ -4,31 +4,28 @@ import sys from dateutil import relativedelta from django.contrib.contenttypes.models import ContentType +from django.db.models import F from django.utils import timezone from orchestra.apps.accounts.models import Account -from orchestra.apps.services.models import Service +from orchestra.apps.services.models import Service, Plan from orchestra.apps.services import settings as services_settings from orchestra.apps.users.models import User from orchestra.utils.tests import BaseTestCase, random_ascii -class BillingTests(BaseTestCase): - DEPENDENCIES = ( - 'orchestra.apps.services', - 'orchestra.apps.users', - 'orchestra.apps.users.roles.posix', - ) - +class BaseBillingTest(BaseTestCase): def create_account(self): account = Account.objects.create() user = User.objects.create_user(username='rata_palida', account=account) account.user = user account.save() return account - + + +class FTPBillingTest(BaseBillingTest): def create_ftp_service(self): - service = Service.objects.create( + return Service.objects.create( description="FTP Account", content_type=ContentType.objects.get_for_model(User), match='not user.is_main and user.has_posix()', @@ -36,19 +33,18 @@ class BillingTests(BaseTestCase): billing_point=Service.FIXED_DATE, is_fee=False, metric='', - pricing_period=Service.BILLING_PERIOD, + pricing_period=Service.NEVER, rate_algorithm=Service.STEP_PRICE, on_cancel=Service.DISCOUNT, payment_style=Service.PREPAY, tax=0, nominal_price=10, ) - return service def create_ftp(self, account=None): - username = '%s_ftp' % random_ascii(10) if not account: account = self.create_account() + username = '%s_ftp' % random_ascii(10) user = User.objects.create_user(username=username, account=account) POSIX = user._meta.get_field_by_name('posix')[0].model POSIX.objects.create(user=user) @@ -98,8 +94,12 @@ class BillingTests(BaseTestCase): user = self.create_ftp(account=account) first_bp = timezone.now().date() + relativedelta.relativedelta(years=2) bills = service.orders.bill(billing_point=first_bp, fixed_point=True) + self.assertEqual(1, service.orders.active().count()) user.delete() + self.assertEqual(0, service.orders.active().count()) user = self.create_ftp(account=account) + self.assertEqual(1, service.orders.active().count()) + self.assertEqual(2, service.orders.count()) bp = timezone.now().date() + relativedelta.relativedelta(years=1) bills = service.orders.bill(billing_point=bp, fixed_point=True, new_open=True) discount = bills[0].lines.order_by('id')[0].sublines.get() @@ -109,3 +109,204 @@ class BillingTests(BaseTestCase): order = service.orders.order_by('-id').first() self.assertEqual(first_bp, order.billed_until) self.assertEqual(decimal.Decimal(0), bills[0].get_total()) + +class DomainBillingTest(BaseBillingTest): + def create_domain_service(self): + from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous + service = Service.objects.create( + description="Domain .ES", + content_type=ContentType.objects.get_for_model(Miscellaneous), + match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'domain .es'", + billing_period=Service.ANUAL, + billing_point=Service.ON_REGISTER, + is_fee=False, + metric='', + pricing_period=Service.BILLING_PERIOD, + rate_algorithm=Service.STEP_PRICE, + on_cancel=Service.NOTHING, + payment_style=Service.PREPAY, + tax=0, + nominal_price=10 + ) + plan = Plan.objects.create(is_default=True, name='Default') + service.rates.create(plan=plan, quantity=1, price=0) + service.rates.create(plan=plan, quantity=2, price=10) + service.rates.create(plan=plan, quantity=4, price=9) + service.rates.create(plan=plan, quantity=6, price=6) + return service + + def create_domain(self, account=None): + from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous + if not account: + account = self.create_account() + domain_name = '%s.es' % random_ascii(10) + domain_service, __ = MiscService.objects.get_or_create(name='domain .es', description='Domain .ES') + return Miscellaneous.objects.create(service=domain_service, description=domain_name, account=account) + + def test_domain(self): + service = self.create_domain_service() + account = self.create_account() + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(0, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(10, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(20, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(29, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(38, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(44, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(50, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill() + self.assertEqual(56, bills[0].get_total()) + + def test_domain_proforma(self): + service = self.create_domain_service() + account = self.create_account() + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(0, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(10, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(20, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(29, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(38, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(44, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(50, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True, new_open=True) + self.assertEqual(56, bills[0].get_total()) + + def test_domain_cumulative(self): + service = self.create_domain_service() + account = self.create_account() + self.create_domain(account=account) + bills = service.orders.bill(proforma=True) + self.assertEqual(0, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True) + self.assertEqual(10, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(proforma=True) + self.assertEqual(30, bills[0].get_total()) + + def test_domain_new_open(self): + service = self.create_domain_service() + account = self.create_account() + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(0, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(10, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(10, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(9, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(9, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(6, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(6, bills[0].get_total()) + self.create_domain(account=account) + bills = service.orders.bill(new_open=True) + self.assertEqual(6, bills[0].get_total()) + + +class TrafficBillingTest(BaseBillingTest): + def create_traffic_service(self): + service = Service.objects.create( + description="Traffic", + content_type=ContentType.objects.get_for_model(Account), + match="account.is_active", + billing_period=Service.MONTHLY, + billing_point=Service.FIXED_DATE, + is_fee=False, + metric='account.resources.traffic.used', + pricing_period=Service.BILLING_PERIOD, + rate_algorithm=Service.STEP_PRICE, + on_cancel=Service.NOTHING, + payment_style=Service.POSTPAY, + tax=0, + nominal_price=10 + ) + plan = Plan.objects.create(is_default=True, name='Default') + service.rates.create(plan=plan, quantity=1, price=0) + service.rates.create(plan=plan, quantity=11, price=10) + return service + + def create_traffic_resource(self): + from orchestra.apps.resources.models import Resource + self.resource = Resource.objects.create( + name='traffic', + content_type=ContentType.objects.get_for_model(Account), + period=Resource.MONTHLY_SUM, + verbose_name='Account Traffic', + unit='GB', + scale=10**9, + ondemand=True, + monitors='FTPTraffic', + ) + return self.resource + + def report_traffic(self, account, date, value): + from orchestra.apps.resources.models import ResourceData, MonitorData + ct = ContentType.objects.get_for_model(Account) + object_id = account.pk + MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value, date=date) + data = ResourceData.get_or_create(account, self.resource) + data.update() + + def test_traffic(self): + service = self.create_traffic_service() + resource = self.create_traffic_resource() + account = self.create_account() + + self.report_traffic(account, timezone.now(), 10**9) + bills = service.orders.bill(commit=False) + self.assertEqual([(account, [])], bills) + + # Prepay + delta = datetime.timedelta(days=60) + date = (timezone.now()-delta).date() + order = service.orders.get() + order.registered_on = date + order.save() + + self.report_traffic(account, date, 10**9*9) + order.metrics.update(updated_on=F('updated_on')-delta) + bills = service.orders.bill(proforma=True) + self.assertEqual(0, bills[0].get_total()) + + self.report_traffic(account, date, 10**10*9) + order.metrics.filter(id=3).update(updated_on=F('updated_on')-delta) + bills = service.orders.bill(proforma=True) + self.assertEqual(900, bills[0].get_total()) diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index fe883f77..68473204 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -172,6 +172,9 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): display_transactions.short_description = _("Transactions") display_transactions.allow_tags = True + def has_add_permission(self, *args, **kwargs): + return False + def get_change_view_actions(self, obj=None): actions = super(TransactionProcessAdmin, self).get_change_view_actions() exclude = [] diff --git a/orchestra/apps/resources/backends.py b/orchestra/apps/resources/backends.py index 04644c5b..fae87831 100644 --- a/orchestra/apps/resources/backends.py +++ b/orchestra/apps/resources/backends.py @@ -52,7 +52,7 @@ class ServiceMonitor(ServiceBackend): return line.split() def store(self, log): - """ stores montirod values from stdout """ + """ stores monitored values from stdout """ from .models import MonitorData name = self.get_name() app_label, model_name = self.model.split('.') diff --git a/orchestra/apps/resources/helpers.py b/orchestra/apps/resources/helpers.py index 3baacb3e..be1d96ac 100644 --- a/orchestra/apps/resources/helpers.py +++ b/orchestra/apps/resources/helpers.py @@ -11,7 +11,7 @@ from .backends import ServiceMonitor def compute_resource_usage(data): """ Computes MonitorData.used based on related monitors """ - MonitorData = type(data) + from .models import MonitorData resource = data.resource today = timezone.now() result = 0 @@ -29,9 +29,7 @@ def compute_resource_usage(data): objects = monitor_model.objects.filter(**{fields: data.object_id}) pks = objects.values_list('id', flat=True) ct = ContentType.objects.get_for_model(monitor_model) - dataset = MonitorData.objects.filter(monitor=monitor, - content_type=ct, object_id__in=pks) - + dataset = MonitorData.objects.filter(monitor=monitor, content_type=ct, object_id__in=pks) # Process dataset according to resource.period if resource.period == resource.MONTHLY_AVG: try: @@ -39,11 +37,9 @@ def compute_resource_usage(data): except MonitorData.DoesNotExist: continue has_result = True - epoch = datetime(year=today.year, month=today.month, day=1, - tzinfo=timezone.utc) + epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc) total = (epoch-last.date).total_seconds() - dataset = dataset.filter(date__year=today.year, - date__month=today.month) + dataset = dataset.filter(date__year=today.year, date__month=today.month) for data in dataset: slot = (previous-data.date).total_seconds() result += data.value * slot/total @@ -62,7 +58,5 @@ def compute_resource_usage(data): continue has_result = True else: - msg = "%s support not implemented" % data.period - raise NotImplementedError(msg) - + raise NotImplementedError("%s support not implemented" % data.period) return result/resource.scale if has_result else None diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 49f224a5..ec4d0c91 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -2,11 +2,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio from django.contrib.contenttypes.models import ContentType from django.core import validators from django.db import models +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from djcelery.models import PeriodicTask, CrontabSchedule from orchestra.models import queryset, fields -from orchestra.utils.functional import cached from . import helpers from .backends import ServiceMonitor @@ -43,6 +43,7 @@ class Resource(models.Model): default=LAST, help_text=_("Operation used for aggregating this resource monitored" "data.")) + # TODO rename to on_deman ondemand = models.BooleanField(_("on demand"), default=False, help_text=_("If enabled the resource will not be pre-allocated, " "but allocated under the application demand")) @@ -79,6 +80,7 @@ class Resource(models.Model): return "{}-{}".format(str(self.content_type), self.name) def save(self, *args, **kwargs): +# created = not self.pk super(Resource, self).save(*args, **kwargs) # Create Celery periodic task name = 'monitor.%s' % str(self) @@ -98,6 +100,8 @@ class Resource(models.Model): elif task.crontab != self.crontab: task.crontab = self.crontab task.save() +# if created: +# create_resource_relation() def delete(self, *args, **kwargs): super(Resource, self).delete(*args, **kwargs) @@ -136,6 +140,13 @@ class ResourceData(models.Model): def get_used(self): return helpers.compute_resource_usage(self) + + def update(self, current=None): + if current is None: + current = self.get_used() + self.used = current or 0 + self.last_update = timezone.now() + self.save() class MonitorData(models.Model): @@ -169,7 +180,6 @@ def create_resource_relation(): resource = Resource.objects.get(content_type__model=model, name=attr, is_active=True) data = ResourceData(content_object=self.obj, resource=resource) - setattr(self, attr, data) return data def __get__(self, obj, cls): diff --git a/orchestra/apps/resources/tasks.py b/orchestra/apps/resources/tasks.py index 26f0553f..414e7edf 100644 --- a/orchestra/apps/resources/tasks.py +++ b/orchestra/apps/resources/tasks.py @@ -27,15 +27,22 @@ def monitor(resource_id): model = resource.content_type.model_class() for obj in model.objects.all(): data = ResourceData.get_or_create(obj, resource) - current = data.get_used() + data.update() if not resource.disable_trigger: - if data.used < data.allocated and current > data.allocated: + if data.used < data.allocated: op = Operation.create(backend, obj, Operation.EXCEED) operations.append(op) - elif data.used > data.allocated and current < data.allocated: + elif data.used < data.allocated: op = Operation.create(backend, obj, Operation.RECOVERY) operation.append(op) - data.used = current or 0 - data.last_update = timezone.now() - data.save() +# data = ResourceData.get_or_create(obj, resource) +# current = data.get_used() +# if not resource.disable_trigger: +# if data.used < data.allocated and current > data.allocated: +# op = Operation.create(backend, obj, Operation.EXCEED) +# operations.append(op) +# elif data.used > data.allocated and current < data.allocated: +# op = Operation.create(backend, obj, Operation.RECOVERY) +# operation.append(op) +# data.update(current=current) Operation.execute(operations) diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 46bd67bc..3346ea51 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -65,6 +65,8 @@ class ServiceHandler(plugins.Plugin): date = bp if self.payment_style == self.PREPAY: date += relativedelta.relativedelta(months=1) + else: + date = timezone.now().date() if self.billing_point == self.ON_REGISTER: day = order.registered_on.day elif self.billing_point == self.FIXED_DATE: @@ -84,7 +86,7 @@ class ServiceHandler(plugins.Plugin): raise NotImplementedError(msg) year = bp.year if self.payment_style == self.POSTPAY: - year = bo.year - relativedelta.relativedelta(years=1) + year = bp.year - relativedelta.relativedelta(years=1) if bp.month >= month: year = bp.year + 1 bp = datetime.datetime(year=year, month=month, day=day, @@ -116,10 +118,19 @@ class ServiceHandler(plugins.Plugin): return decimal.Decimal(size) def get_pricing_slots(self, ini, end): + day = 1 + month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH + if self.billing_point == self.ON_REGISTER: + day = ini.day + month = ini.month period = self.get_pricing_period() if period == self.MONTHLY: + ini = datetime.datetime(year=ini.year, month=ini.month, day=day, + tzinfo=timezone.get_current_timezone()).date() rdelta = relativedelta.relativedelta(months=1) elif period == self.ANUAL: + ini = datetime.datetime(year=ini.year, month=month, day=day, + tzinfo=timezone.get_current_timezone()).date() rdelta = relativedelta.relativedelta(years=1) elif period == self.NEVER: yield ini, end @@ -128,10 +139,9 @@ class ServiceHandler(plugins.Plugin): raise NotImplementedError while True: next = ini + rdelta - if next >= end: - yield ini, end - break yield ini, next + if next >= end: + break ini = next def generate_discount(self, line, dtype, price): @@ -213,12 +223,12 @@ class ServiceHandler(plugins.Plugin): for order in porders: bu = getattr(order, 'new_billed_until', order.billed_until) if bu: - if order.register >= ini and order.register < end: + if order.registered_on > ini and order.registered_on <= end: counter += 1 - if order.register != bu and bu >= ini and bu < end: + if order.registered_on != bu and bu > ini and bu <= end: counter += 1 if order.billed_until and order.billed_until != bu: - if order.register != order.billed_until and order.billed_until >= ini and order.billed_until < end: + if order.registered_on != order.billed_until and order.billed_until > ini and order.billed_until <= end: counter += 1 return counter @@ -230,7 +240,7 @@ class ServiceHandler(plugins.Plugin): size = self.get_price_size(ini, end) metric = len(orders) interval = helpers.Interval(ini=ini, end=end) - for position, order in enumerate(orders): + for position, order in enumerate(orders, start=1): csize = 0 compensations = getattr(order, '_compensations', []) # Compensations < new_billed_until @@ -269,14 +279,14 @@ class ServiceHandler(plugins.Plugin): def bill_registered_or_renew_events(self, account, porders, rates, commit=True): # Before registration lines = [] - perido = self.get_pricing_period() + period = self.get_pricing_period() if period == self.MONTHLY: rdelta = relativedelta.relativedelta(months=1) elif period == self.ANUAL: rdelta = relativedelta.relativedelta(years=1) elif period == self.NEVER: raise NotImplementedError("Rates with no pricing period?") - for position, order in enumerate(porders): + for position, order in enumerate(porders, start=1): if hasattr(order, 'new_billed_until'): pend = order.billed_until or order.registered_on pini = pend - rdelta @@ -298,6 +308,7 @@ class ServiceHandler(plugins.Plugin): if commit: order.billed_until = order.new_billed_until order.save() + return lines def bill_with_orders(self, orders, account, **options): # For the "boundary conditions" just think that: @@ -340,7 +351,7 @@ class ServiceHandler(plugins.Plugin): porders = related_orders.pricing_orders(ini, end) porders = list(set(orders).union(set(porders))) porders.sort(cmp=helpers.cmp_billed_until_or_registered_on) - if self.billing_period != self.NEVER and self.get_pricing_period != self.NEVER: + if self.billing_period != self.NEVER and self.get_pricing_period == self.NEVER: liens = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit) else: # TODO compensation in this case? @@ -371,6 +382,7 @@ class ServiceHandler(plugins.Plugin): # TODO filter out orders with cancelled_on < billed_until ? lines = [] commit = options.get('commit', True) + bp = None for order in orders: bp = self.get_billing_point(order, bp=bp, **options) ini = order.billed_until or order.registered_on @@ -381,25 +393,17 @@ class ServiceHandler(plugins.Plugin): prev = None lines_info = [] for ini, end in self.get_pricing_slots(ini, bp): - size = self.get_price_size(ini, end) metric = order.get_metric(ini, end) price = self.get_price(order, metric) - current = AttributeDict(price=price, size=size, ini=ini, end=end) - if prev and prev.metric == current.metric and prev.end == current.end: - prev.end = current.end - prev.size += current.size - prev.price += current.price - else: - lines_info.append(current) - prev = current - for line in lines_info: - lines.append(self.generate_line(order, price, size, ini, end)) + lines.append(self.generate_line(order, price, metric, ini, end)) if commit: order.billed_until = order.new_billed_until order.save() return lines def generate_bill_lines(self, orders, account, **options): + if options.get('proforma', False): + options['commit'] = False if not self.metric: lines = self.bill_with_orders(orders, account, **options) else: diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 52138268..43b72391 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -1,3 +1,4 @@ +import decimal import sys from django.db import models @@ -281,14 +282,14 @@ class Service(models.Model): if counter >= metric: counter = metric accumulated += (counter - ant_counter) * rate['price'] - return float(accumulated) + return decimal.Decimal(accumulated) ant_counter = counter accumulated += rate['price'] * rate['quantity'] else: for rate in rates: counter += rate['quantity'] if counter >= position: - return float(rate['price']) + return decimal.Decimal(rate['price']) def get_rates(self, account, cache=True): # rates are cached per account diff --git a/orchestra/apps/services/tests/test_handler.py b/orchestra/apps/services/tests/test_handler.py index 91009d37..eb22246b 100644 --- a/orchestra/apps/services/tests/test_handler.py +++ b/orchestra/apps/services/tests/test_handler.py @@ -50,7 +50,7 @@ class HandlerTests(BaseTestCase): billing_point=Service.FIXED_DATE, is_fee=False, metric='', - pricing_period=Service.BILLING_PERIOD, + pricing_period=Service.NEVER, rate_algorithm=Service.STEP_PRICE, on_cancel=Service.DISCOUNT, payment_style=Service.PREPAY, diff --git a/orchestra/apps/users/models.py b/orchestra/apps/users/models.py index 71aa42c6..ea301ae8 100644 --- a/orchestra/apps/users/models.py +++ b/orchestra/apps/users/models.py @@ -38,8 +38,7 @@ class User(auth.AbstractBaseUser): @property def is_main(self): - # TODO chicken and egg - return not self.account.user_id or self.account.user == self + return self.account.user == self def get_full_name(self): full_name = '%s %s' % (self.first_name, self.last_name)