Fixes on billing order with metric

This commit is contained in:
Marc 2014-09-22 15:59:53 +00:00
parent c992d5004c
commit 8f1d05873c
14 changed files with 310 additions and 79 deletions

View file

@ -14,7 +14,6 @@ TODO
* add `BackendLog` retry action * add `BackendLog` retry action
* move invoice contact to invoices app? * move invoice contact to invoices app?
* wrapper around reverse('admin:....') `link()` and `link_factory()`
* PHPbBckendMiixin with get_php_ini * PHPbBckendMiixin with get_php_ini
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]` * Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
* rename account.user to primary_user * 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. 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 * profile select_related vs prefetch_related
* use HTTP OPTIONS instead of configuration endpoint, or rename to settings?
* Log changes from rest api (serialized objects) * Log changes from rest api (serialized objects)
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict * passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
* pip install pyinotify * 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 * create log file at /var/log/orchestra.log and rotate
* order.register_at
@property
def register_on(self):
return order.register_at.date()

View file

@ -62,6 +62,9 @@ class Bill(models.Model):
objects = BillManager() objects = BillManager()
class Meta:
get_latest_by = 'created_on'
def __unicode__(self): def __unicode__(self):
return self.number return self.number

View file

@ -10,23 +10,27 @@ class BillsBackend(object):
bill = None bill = None
bills = [] bills = []
create_new = options.get('new_open', False) create_new = options.get('new_open', False)
is_proforma = options.get('is_proforma', False) proforma = options.get('proforma', False)
for line in lines: for line in lines:
service = line.order.service service = line.order.service
# Create bill if needed # Create bill if needed
if bill is None or service.is_fee: if bill is None or service.is_fee:
if is_proforma: if proforma:
if create_new: if create_new:
bill = ProForma.objects.create(account=account) bill = ProForma.objects.create(account=account)
else: 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: elif service.is_fee:
bill = Fee.objects.create(account=account) bill = Fee.objects.create(account=account)
else: else:
if create_new: if create_new:
bill = Invoice.objects.create(account=account) bill = Invoice.objects.create(account=account)
else: 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) bills.append(bill)
# Create bill line # Create bill line
billine = bill.lines.create( billine = bill.lines.create(

View file

@ -1,3 +1,4 @@
import decimal
import logging import logging
import sys import sys
@ -122,13 +123,15 @@ class Order(models.Model):
def update(self): def update(self):
instance = self.content_object instance = self.content_object
handler = self.service.handler handler = self.service.handler
metric = ''
if handler.metric: if handler.metric:
metric = handler.get_metric(instance) metric = handler.get_metric(instance)
if metric is not None: if metric is not None:
MetricStorage.store(self, metric) MetricStorage.store(self, metric)
metric = ', metric:{}'.format(metric)
description = "{}: {}".format(handler.description, str(instance)) description = "{}: {}".format(handler.description, str(instance))
logger.info("UPDATED order id: {id} description:{description}".format( logger.info("UPDATED order id:{id}, description:{description}{metric}".format(
id=self.id, description=description)) id=self.id, description=description, metric=metric))
if self.description != description: if self.description != description:
self.description = description self.description = description
self.save() self.save()
@ -143,10 +146,10 @@ class Order(models.Model):
class MetricStorage(models.Model): class MetricStorage(models.Model):
order = models.ForeignKey(Order, verbose_name=_("order")) order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics')
value = models.BigIntegerField(_("value")) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
created_on = models.DateField(_("created on"), auto_now_add=True) created_on = models.DateField(_("created"), auto_now_add=True)
updated_on = models.DateField(_("updated on"), auto_now=True) updated_on = models.DateTimeField(_("updated"))
class Meta: class Meta:
get_latest_by = 'created_on' get_latest_by = 'created_on'
@ -156,23 +159,24 @@ class MetricStorage(models.Model):
@classmethod @classmethod
def store(cls, order, value): def store(cls, order, value):
now = timezone.now()
try: try:
metric = cls.objects.filter(order=order).latest() metric = cls.objects.filter(order=order).latest()
except cls.DoesNotExist: except cls.DoesNotExist:
cls.objects.create(order=order, value=value) cls.objects.create(order=order, value=value, updated_on=now)
else: else:
if metric.value != value: if metric.value != value:
cls.objects.create(order=order, value=value) cls.objects.create(order=order, value=value, updated_on=now)
else: else:
metric.updated_on = now
metric.save() metric.save()
@classmethod @classmethod
def get(cls, order, ini, end): def get(cls, order, ini, end):
try: try:
return cls.objects.filter(order=order, updated_on__lt=end, return order.metrics.filter(updated_on__lt=end, updated_on__gte=ini).latest('updated_on').value
updated_on__gte=ini).latest('updated_on').value
except cls.DoesNotExist: except cls.DoesNotExist:
return 0 return decimal.Decimal(0)
_excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecorder.Migration) _excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecorder.Migration)

View file

@ -4,22 +4,17 @@ import sys
from dateutil import relativedelta from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import F
from django.utils import timezone from django.utils import timezone
from orchestra.apps.accounts.models import Account 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.services import settings as services_settings
from orchestra.apps.users.models import User from orchestra.apps.users.models import User
from orchestra.utils.tests import BaseTestCase, random_ascii from orchestra.utils.tests import BaseTestCase, random_ascii
class BillingTests(BaseTestCase): class BaseBillingTest(BaseTestCase):
DEPENDENCIES = (
'orchestra.apps.services',
'orchestra.apps.users',
'orchestra.apps.users.roles.posix',
)
def create_account(self): def create_account(self):
account = Account.objects.create() account = Account.objects.create()
user = User.objects.create_user(username='rata_palida', account=account) user = User.objects.create_user(username='rata_palida', account=account)
@ -27,8 +22,10 @@ class BillingTests(BaseTestCase):
account.save() account.save()
return account return account
class FTPBillingTest(BaseBillingTest):
def create_ftp_service(self): def create_ftp_service(self):
service = Service.objects.create( return Service.objects.create(
description="FTP Account", description="FTP Account",
content_type=ContentType.objects.get_for_model(User), content_type=ContentType.objects.get_for_model(User),
match='not user.is_main and user.has_posix()', match='not user.is_main and user.has_posix()',
@ -36,19 +33,18 @@ class BillingTests(BaseTestCase):
billing_point=Service.FIXED_DATE, billing_point=Service.FIXED_DATE,
is_fee=False, is_fee=False,
metric='', metric='',
pricing_period=Service.BILLING_PERIOD, pricing_period=Service.NEVER,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.STEP_PRICE,
on_cancel=Service.DISCOUNT, on_cancel=Service.DISCOUNT,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,
tax=0, tax=0,
nominal_price=10, nominal_price=10,
) )
return service
def create_ftp(self, account=None): def create_ftp(self, account=None):
username = '%s_ftp' % random_ascii(10)
if not account: if not account:
account = self.create_account() account = self.create_account()
username = '%s_ftp' % random_ascii(10)
user = User.objects.create_user(username=username, account=account) user = User.objects.create_user(username=username, account=account)
POSIX = user._meta.get_field_by_name('posix')[0].model POSIX = user._meta.get_field_by_name('posix')[0].model
POSIX.objects.create(user=user) POSIX.objects.create(user=user)
@ -98,8 +94,12 @@ class BillingTests(BaseTestCase):
user = self.create_ftp(account=account) user = self.create_ftp(account=account)
first_bp = timezone.now().date() + relativedelta.relativedelta(years=2) first_bp = timezone.now().date() + relativedelta.relativedelta(years=2)
bills = service.orders.bill(billing_point=first_bp, fixed_point=True) bills = service.orders.bill(billing_point=first_bp, fixed_point=True)
self.assertEqual(1, service.orders.active().count())
user.delete() user.delete()
self.assertEqual(0, service.orders.active().count())
user = self.create_ftp(account=account) 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) bp = timezone.now().date() + relativedelta.relativedelta(years=1)
bills = service.orders.bill(billing_point=bp, fixed_point=True, new_open=True) bills = service.orders.bill(billing_point=bp, fixed_point=True, new_open=True)
discount = bills[0].lines.order_by('id')[0].sublines.get() discount = bills[0].lines.order_by('id')[0].sublines.get()
@ -109,3 +109,204 @@ class BillingTests(BaseTestCase):
order = service.orders.order_by('-id').first() order = service.orders.order_by('-id').first()
self.assertEqual(first_bp, order.billed_until) self.assertEqual(first_bp, order.billed_until)
self.assertEqual(decimal.Decimal(0), bills[0].get_total()) 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())

View file

@ -172,6 +172,9 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
display_transactions.short_description = _("Transactions") display_transactions.short_description = _("Transactions")
display_transactions.allow_tags = True display_transactions.allow_tags = True
def has_add_permission(self, *args, **kwargs):
return False
def get_change_view_actions(self, obj=None): def get_change_view_actions(self, obj=None):
actions = super(TransactionProcessAdmin, self).get_change_view_actions() actions = super(TransactionProcessAdmin, self).get_change_view_actions()
exclude = [] exclude = []

View file

@ -52,7 +52,7 @@ class ServiceMonitor(ServiceBackend):
return line.split() return line.split()
def store(self, log): def store(self, log):
""" stores montirod values from stdout """ """ stores monitored values from stdout """
from .models import MonitorData from .models import MonitorData
name = self.get_name() name = self.get_name()
app_label, model_name = self.model.split('.') app_label, model_name = self.model.split('.')

View file

@ -11,7 +11,7 @@ from .backends import ServiceMonitor
def compute_resource_usage(data): def compute_resource_usage(data):
""" Computes MonitorData.used based on related monitors """ """ Computes MonitorData.used based on related monitors """
MonitorData = type(data) from .models import MonitorData
resource = data.resource resource = data.resource
today = timezone.now() today = timezone.now()
result = 0 result = 0
@ -29,9 +29,7 @@ def compute_resource_usage(data):
objects = monitor_model.objects.filter(**{fields: data.object_id}) objects = monitor_model.objects.filter(**{fields: data.object_id})
pks = objects.values_list('id', flat=True) pks = objects.values_list('id', flat=True)
ct = ContentType.objects.get_for_model(monitor_model) ct = ContentType.objects.get_for_model(monitor_model)
dataset = MonitorData.objects.filter(monitor=monitor, dataset = MonitorData.objects.filter(monitor=monitor, content_type=ct, object_id__in=pks)
content_type=ct, object_id__in=pks)
# Process dataset according to resource.period # Process dataset according to resource.period
if resource.period == resource.MONTHLY_AVG: if resource.period == resource.MONTHLY_AVG:
try: try:
@ -39,11 +37,9 @@ def compute_resource_usage(data):
except MonitorData.DoesNotExist: except MonitorData.DoesNotExist:
continue continue
has_result = True has_result = True
epoch = datetime(year=today.year, month=today.month, day=1, epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc)
tzinfo=timezone.utc)
total = (epoch-last.date).total_seconds() total = (epoch-last.date).total_seconds()
dataset = dataset.filter(date__year=today.year, dataset = dataset.filter(date__year=today.year, date__month=today.month)
date__month=today.month)
for data in dataset: for data in dataset:
slot = (previous-data.date).total_seconds() slot = (previous-data.date).total_seconds()
result += data.value * slot/total result += data.value * slot/total
@ -62,7 +58,5 @@ def compute_resource_usage(data):
continue continue
has_result = True has_result = True
else: else:
msg = "%s support not implemented" % data.period raise NotImplementedError("%s support not implemented" % data.period)
raise NotImplementedError(msg)
return result/resource.scale if has_result else None return result/resource.scale if has_result else None

View file

@ -2,11 +2,11 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djcelery.models import PeriodicTask, CrontabSchedule from djcelery.models import PeriodicTask, CrontabSchedule
from orchestra.models import queryset, fields from orchestra.models import queryset, fields
from orchestra.utils.functional import cached
from . import helpers from . import helpers
from .backends import ServiceMonitor from .backends import ServiceMonitor
@ -43,6 +43,7 @@ class Resource(models.Model):
default=LAST, default=LAST,
help_text=_("Operation used for aggregating this resource monitored" help_text=_("Operation used for aggregating this resource monitored"
"data.")) "data."))
# TODO rename to on_deman
ondemand = models.BooleanField(_("on demand"), default=False, ondemand = models.BooleanField(_("on demand"), default=False,
help_text=_("If enabled the resource will not be pre-allocated, " help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand")) "but allocated under the application demand"))
@ -79,6 +80,7 @@ class Resource(models.Model):
return "{}-{}".format(str(self.content_type), self.name) return "{}-{}".format(str(self.content_type), self.name)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# created = not self.pk
super(Resource, self).save(*args, **kwargs) super(Resource, self).save(*args, **kwargs)
# Create Celery periodic task # Create Celery periodic task
name = 'monitor.%s' % str(self) name = 'monitor.%s' % str(self)
@ -98,6 +100,8 @@ class Resource(models.Model):
elif task.crontab != self.crontab: elif task.crontab != self.crontab:
task.crontab = self.crontab task.crontab = self.crontab
task.save() task.save()
# if created:
# create_resource_relation()
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
super(Resource, self).delete(*args, **kwargs) super(Resource, self).delete(*args, **kwargs)
@ -137,6 +141,13 @@ class ResourceData(models.Model):
def get_used(self): def get_used(self):
return helpers.compute_resource_usage(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): class MonitorData(models.Model):
""" Stores monitored data """ """ Stores monitored data """
@ -169,7 +180,6 @@ def create_resource_relation():
resource = Resource.objects.get(content_type__model=model, resource = Resource.objects.get(content_type__model=model,
name=attr, is_active=True) name=attr, is_active=True)
data = ResourceData(content_object=self.obj, resource=resource) data = ResourceData(content_object=self.obj, resource=resource)
setattr(self, attr, data)
return data return data
def __get__(self, obj, cls): def __get__(self, obj, cls):

View file

@ -27,15 +27,22 @@ def monitor(resource_id):
model = resource.content_type.model_class() model = resource.content_type.model_class()
for obj in model.objects.all(): for obj in model.objects.all():
data = ResourceData.get_or_create(obj, resource) data = ResourceData.get_or_create(obj, resource)
current = data.get_used() data.update()
if not resource.disable_trigger: 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) op = Operation.create(backend, obj, Operation.EXCEED)
operations.append(op) operations.append(op)
elif data.used > data.allocated and current < data.allocated: elif data.used < data.allocated:
op = Operation.create(backend, obj, Operation.RECOVERY) op = Operation.create(backend, obj, Operation.RECOVERY)
operation.append(op) operation.append(op)
data.used = current or 0 # data = ResourceData.get_or_create(obj, resource)
data.last_update = timezone.now() # current = data.get_used()
data.save() # 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) Operation.execute(operations)

View file

@ -65,6 +65,8 @@ class ServiceHandler(plugins.Plugin):
date = bp date = bp
if self.payment_style == self.PREPAY: if self.payment_style == self.PREPAY:
date += relativedelta.relativedelta(months=1) date += relativedelta.relativedelta(months=1)
else:
date = timezone.now().date()
if self.billing_point == self.ON_REGISTER: if self.billing_point == self.ON_REGISTER:
day = order.registered_on.day day = order.registered_on.day
elif self.billing_point == self.FIXED_DATE: elif self.billing_point == self.FIXED_DATE:
@ -84,7 +86,7 @@ class ServiceHandler(plugins.Plugin):
raise NotImplementedError(msg) raise NotImplementedError(msg)
year = bp.year year = bp.year
if self.payment_style == self.POSTPAY: if self.payment_style == self.POSTPAY:
year = bo.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
bp = datetime.datetime(year=year, month=month, day=day, bp = datetime.datetime(year=year, month=month, day=day,
@ -116,10 +118,19 @@ class ServiceHandler(plugins.Plugin):
return decimal.Decimal(size) return decimal.Decimal(size)
def get_pricing_slots(self, ini, end): 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() period = self.get_pricing_period()
if period == self.MONTHLY: 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) rdelta = relativedelta.relativedelta(months=1)
elif period == self.ANUAL: 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) rdelta = relativedelta.relativedelta(years=1)
elif period == self.NEVER: elif period == self.NEVER:
yield ini, end yield ini, end
@ -128,10 +139,9 @@ class ServiceHandler(plugins.Plugin):
raise NotImplementedError raise NotImplementedError
while True: while True:
next = ini + rdelta next = ini + rdelta
if next >= end:
yield ini, end
break
yield ini, next yield ini, next
if next >= end:
break
ini = next ini = next
def generate_discount(self, line, dtype, price): def generate_discount(self, line, dtype, price):
@ -213,12 +223,12 @@ class ServiceHandler(plugins.Plugin):
for order in porders: for order in porders:
bu = getattr(order, 'new_billed_until', order.billed_until) bu = getattr(order, 'new_billed_until', order.billed_until)
if bu: if bu:
if order.register >= ini and order.register < end: if order.registered_on > ini and order.registered_on <= end:
counter += 1 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 counter += 1
if order.billed_until and order.billed_until != bu: 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 counter += 1
return counter return counter
@ -230,7 +240,7 @@ class ServiceHandler(plugins.Plugin):
size = self.get_price_size(ini, end) size = self.get_price_size(ini, end)
metric = len(orders) metric = len(orders)
interval = helpers.Interval(ini=ini, end=end) interval = helpers.Interval(ini=ini, end=end)
for position, order in enumerate(orders): for position, order in enumerate(orders, start=1):
csize = 0 csize = 0
compensations = getattr(order, '_compensations', []) compensations = getattr(order, '_compensations', [])
# Compensations < new_billed_until # Compensations < new_billed_until
@ -269,14 +279,14 @@ class ServiceHandler(plugins.Plugin):
def bill_registered_or_renew_events(self, account, porders, rates, commit=True): def bill_registered_or_renew_events(self, account, porders, rates, commit=True):
# Before registration # Before registration
lines = [] lines = []
perido = self.get_pricing_period() period = self.get_pricing_period()
if period == self.MONTHLY: if period == self.MONTHLY:
rdelta = relativedelta.relativedelta(months=1) rdelta = relativedelta.relativedelta(months=1)
elif period == self.ANUAL: elif period == self.ANUAL:
rdelta = relativedelta.relativedelta(years=1) rdelta = relativedelta.relativedelta(years=1)
elif period == self.NEVER: elif period == self.NEVER:
raise NotImplementedError("Rates with no pricing period?") 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'): if hasattr(order, 'new_billed_until'):
pend = order.billed_until or order.registered_on pend = order.billed_until or order.registered_on
pini = pend - rdelta pini = pend - rdelta
@ -298,6 +308,7 @@ class ServiceHandler(plugins.Plugin):
if commit: if commit:
order.billed_until = order.new_billed_until order.billed_until = order.new_billed_until
order.save() order.save()
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:
@ -340,7 +351,7 @@ class ServiceHandler(plugins.Plugin):
porders = related_orders.pricing_orders(ini, end) porders = related_orders.pricing_orders(ini, end)
porders = list(set(orders).union(set(porders))) porders = list(set(orders).union(set(porders)))
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on) 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) liens = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit)
else: else:
# TODO compensation in this case? # TODO compensation in this case?
@ -371,6 +382,7 @@ class ServiceHandler(plugins.Plugin):
# TODO filter out orders with cancelled_on < billed_until ? # TODO filter out orders with cancelled_on < billed_until ?
lines = [] lines = []
commit = options.get('commit', True) commit = options.get('commit', True)
bp = None
for order in orders: for order in orders:
bp = self.get_billing_point(order, bp=bp, **options) bp = self.get_billing_point(order, bp=bp, **options)
ini = order.billed_until or order.registered_on ini = order.billed_until or order.registered_on
@ -381,25 +393,17 @@ class ServiceHandler(plugins.Plugin):
prev = None prev = None
lines_info = [] lines_info = []
for ini, end in self.get_pricing_slots(ini, bp): for ini, end in self.get_pricing_slots(ini, bp):
size = self.get_price_size(ini, end)
metric = order.get_metric(ini, end) metric = order.get_metric(ini, end)
price = self.get_price(order, metric) price = self.get_price(order, metric)
current = AttributeDict(price=price, size=size, ini=ini, end=end) lines.append(self.generate_line(order, price, metric, ini, 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))
if commit: if commit:
order.billed_until = order.new_billed_until order.billed_until = order.new_billed_until
order.save() order.save()
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):
options['commit'] = False
if not self.metric: if not self.metric:
lines = self.bill_with_orders(orders, account, **options) lines = self.bill_with_orders(orders, account, **options)
else: else:

View file

@ -1,3 +1,4 @@
import decimal
import sys import sys
from django.db import models from django.db import models
@ -281,14 +282,14 @@ class Service(models.Model):
if counter >= metric: if counter >= metric:
counter = metric counter = metric
accumulated += (counter - ant_counter) * rate['price'] accumulated += (counter - ant_counter) * rate['price']
return float(accumulated) return decimal.Decimal(accumulated)
ant_counter = counter ant_counter = counter
accumulated += rate['price'] * rate['quantity'] accumulated += rate['price'] * rate['quantity']
else: else:
for rate in rates: for rate in rates:
counter += rate['quantity'] counter += rate['quantity']
if counter >= position: if counter >= position:
return float(rate['price']) return decimal.Decimal(rate['price'])
def get_rates(self, account, cache=True): def get_rates(self, account, cache=True):
# rates are cached per account # rates are cached per account

View file

@ -50,7 +50,7 @@ class HandlerTests(BaseTestCase):
billing_point=Service.FIXED_DATE, billing_point=Service.FIXED_DATE,
is_fee=False, is_fee=False,
metric='', metric='',
pricing_period=Service.BILLING_PERIOD, pricing_period=Service.NEVER,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.STEP_PRICE,
on_cancel=Service.DISCOUNT, on_cancel=Service.DISCOUNT,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,

View file

@ -38,8 +38,7 @@ class User(auth.AbstractBaseUser):
@property @property
def is_main(self): def is_main(self):
# TODO chicken and egg return self.account.user == self
return not self.account.user_id or self.account.user == self
def get_full_name(self): def get_full_name(self):
full_name = '%s %s' % (self.first_name, self.last_name) full_name = '%s %s' % (self.first_name, self.last_name)