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
* 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()

View file

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

View file

@ -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(

View file

@ -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)

View file

@ -4,22 +4,17 @@ 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)
@ -27,8 +22,10 @@ class BillingTests(BaseTestCase):
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())

View file

@ -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 = []

View file

@ -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('.')

View file

@ -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

View file

@ -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)
@ -137,6 +141,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):
""" Stores monitored data """
@ -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):

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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)