Enough of fucking billing tests0
This commit is contained in:
parent
c8651cf4ed
commit
af323ebe25
|
@ -100,9 +100,13 @@ class Order(models.Model):
|
|||
return str(self.service)
|
||||
|
||||
@classmethod
|
||||
def update_orders(cls, instance):
|
||||
Service = get_model(*settings.ORDERS_SERVICE_MODEL.split('.'))
|
||||
for service in Service.get_services(instance):
|
||||
def update_orders(cls, instance, service=None):
|
||||
if service is None:
|
||||
Service = get_model(*settings.ORDERS_SERVICE_MODEL.split('.'))
|
||||
services = Service.get_services(instance)
|
||||
else:
|
||||
services = [service]
|
||||
for service in services:
|
||||
orders = Order.objects.by_object(instance, service=service).active()
|
||||
if service.handler.matches(instance):
|
||||
if not orders:
|
||||
|
|
|
@ -101,6 +101,7 @@ class Resource(models.Model):
|
|||
task.crontab = self.crontab
|
||||
task.save()
|
||||
if created:
|
||||
# This only work on tests because of multiprocessing used on real deployments
|
||||
create_resource_relation()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def update_orders(modeladmin, request, queryset):
|
||||
for service in queryset:
|
||||
service.update_orders()
|
||||
modeladmin.log_change(request, transaction, 'Update orders')
|
||||
msg = _("Orders for %s selected services have been updated.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
update_orders.url_name = 'update-orders'
|
||||
update_orders.verbose_name = _("Update orders")
|
|
@ -4,10 +4,12 @@ from django.core.urlresolvers import reverse
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ChangeViewActionsMixin
|
||||
from orchestra.admin.filters import UsedContentTypeFilter
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
from orchestra.core import services
|
||||
|
||||
from .actions import update_orders
|
||||
from .models import Plan, ContractedPlan, Rate, Service
|
||||
|
||||
|
||||
|
@ -27,7 +29,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
|
|||
list_filter = ('plan__name',)
|
||||
|
||||
|
||||
class ServiceAdmin(admin.ModelAdmin):
|
||||
class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
'description', 'content_type', 'handler_type', 'num_orders', 'is_active'
|
||||
)
|
||||
|
@ -49,6 +51,8 @@ class ServiceAdmin(admin.ModelAdmin):
|
|||
}),
|
||||
)
|
||||
inlines = [RateInline]
|
||||
actions = [update_orders]
|
||||
change_view_actions = actions
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Improve performance of account field and filter by account """
|
||||
|
|
|
@ -18,6 +18,8 @@ class ServiceHandler(plugins.Plugin):
|
|||
"""
|
||||
Separates all the logic of billing handling from the model allowing to better
|
||||
customize its behaviout
|
||||
|
||||
Relax and enjoy the journey.
|
||||
"""
|
||||
|
||||
model = None
|
||||
|
@ -213,7 +215,6 @@ class ServiceHandler(plugins.Plugin):
|
|||
compensations, used_compensations = helpers.compensate(interval, compensations)
|
||||
order._compensations = used_compensations
|
||||
for comp in used_compensations:
|
||||
# TODO get min right
|
||||
comp.order.new_billed_until = min(comp.order.billed_until, comp.ini,
|
||||
getattr(comp.order, 'new_billed_until', datetime.date.max))
|
||||
if options.get('commit', True):
|
||||
|
@ -337,7 +338,6 @@ class ServiceHandler(plugins.Plugin):
|
|||
# In most cases:
|
||||
# ini >= registered_date, end < registered_date
|
||||
# boundary lookup and exclude cancelled and billed
|
||||
# TODO service.payment_style == self.POSTPAY no discounts no shit on_cancel
|
||||
orders_ = []
|
||||
bp = None
|
||||
ini = datetime.date.max
|
||||
|
@ -359,7 +359,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
|
||||
# Compensation
|
||||
related_orders = account.orders.filter(service=self.service)
|
||||
if self.on_cancel == self.COMPENSATE:
|
||||
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
||||
# Get orders pending for compensation
|
||||
givers = list(related_orders.givers(ini, end))
|
||||
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
|
@ -381,7 +381,6 @@ class ServiceHandler(plugins.Plugin):
|
|||
# Periodic billing with no pricing period
|
||||
lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
|
||||
else:
|
||||
# TODO compensation in this case?
|
||||
# Periodic and one-time billing with pricing period
|
||||
lines = self.bill_registered_or_renew_events(account, porders, rates)
|
||||
else:
|
||||
|
|
|
@ -3,6 +3,7 @@ import sys
|
|||
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.loading import get_model
|
||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.contenttypes import generic
|
||||
|
@ -325,6 +326,12 @@ class Service(models.Model):
|
|||
def rate_method(self):
|
||||
return self.RATE_METHODS[self.rate_algorithm]
|
||||
|
||||
def update_orders(self):
|
||||
order_model = get_model(settings.SERVICES_ORDER_MODEL)
|
||||
related_model = self.content_type.model_class()
|
||||
for instance in related_model.objects.all():
|
||||
order_model.update_orders(instance, service=self)
|
||||
|
||||
|
||||
accounts.register(ContractedPlan)
|
||||
services.register(ContractedPlan, menu=False)
|
||||
|
|
|
@ -42,11 +42,32 @@ def _compute(rates, metric):
|
|||
return value, steps
|
||||
|
||||
|
||||
def _prepend_missing(rates):
|
||||
"""
|
||||
Support for incomplete rates
|
||||
When first rate (quantity=5, price=10) defaults to nominal_price
|
||||
"""
|
||||
if rates:
|
||||
first = rates[0]
|
||||
if first.quantity == 0:
|
||||
first.quantity = 1
|
||||
elif first.quantity > 1:
|
||||
if not isinstance(rates, list):
|
||||
rates = list(rates)
|
||||
service = first.service
|
||||
rate_class = type(first)
|
||||
rates.insert(0,
|
||||
rate_class(service=service, plan=first.plan, quantity=1, price=service.nominal_price)
|
||||
)
|
||||
return rates
|
||||
|
||||
|
||||
def step_price(rates, metric):
|
||||
# Step price
|
||||
group = []
|
||||
minimal = (sys.maxint, [])
|
||||
for plan, rates in rates.group_by('plan').iteritems():
|
||||
rates = _prepend_missing(rates)
|
||||
value, steps = _compute(rates, metric)
|
||||
if plan.is_combinable:
|
||||
group.append(steps)
|
||||
|
@ -102,7 +123,8 @@ def match_price(rates, metric):
|
|||
candidates = []
|
||||
selected = False
|
||||
prev = None
|
||||
for rate in rates.distinct():
|
||||
rates = _prepend_missing(rates.distinct())
|
||||
for rate in rates:
|
||||
if prev:
|
||||
if prev.plan != rate.plan:
|
||||
if not selected and prev.quantity <= metric:
|
||||
|
|
|
@ -12,3 +12,6 @@ SERVICES_SERVICE_DEFAUL_TAX = getattr(settings, 'ORDERS_SERVICE_DFAULT_TAX', 0)
|
|||
|
||||
|
||||
SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL_BILLING_MONTH', 4)
|
||||
|
||||
|
||||
SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', 'orders.Order')
|
||||
|
|
|
@ -36,8 +36,9 @@ class DomainBillingTest(BaseBillingTest):
|
|||
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)
|
||||
domain_service, __ = MiscService.objects.get_or_create(name='domain .es',
|
||||
description='Domain .ES')
|
||||
return account.miscellaneous.create(service=domain_service, description=domain_name)
|
||||
|
||||
def test_domain(self):
|
||||
service = self.create_domain_service()
|
||||
|
|
|
@ -98,6 +98,3 @@ class FTPBillingTest(BaseBillingTest):
|
|||
order = account.orders.order_by('-id').first()
|
||||
self.assertEqual(first_bp, order.billed_until)
|
||||
self.assertEqual(decimal.Decimal(0), bills[0].get_total())
|
||||
|
||||
def test_ftp_account_with_rates(self):
|
||||
pass
|
||||
|
|
|
@ -34,8 +34,9 @@ class JobBillingTest(BaseBillingTest):
|
|||
if not account:
|
||||
account = self.create_account()
|
||||
description = 'Random Job %s' % random_ascii(10)
|
||||
service, __ = MiscService.objects.get_or_create(name='job', description=description, has_amount=True)
|
||||
return Miscellaneous.objects.create(service=service, description=description, account=account, amount=amount)
|
||||
service, __ = MiscService.objects.get_or_create(name='job', description=description,
|
||||
has_amount=True)
|
||||
return account.miscellaneous.create(service=service, description=description, amount=amount)
|
||||
|
||||
def test_job(self):
|
||||
service = self.create_job_service()
|
||||
|
@ -48,5 +49,3 @@ class JobBillingTest(BaseBillingTest):
|
|||
job = self.create_job(100, account=account)
|
||||
bill = account.orders.bill(new_open=True)[0]
|
||||
self.assertEqual(100*15, bill.get_total())
|
||||
|
||||
|
||||
|
|
|
@ -156,4 +156,3 @@ class MailboxBillingTest(BaseBillingTest):
|
|||
with freeze_time(now+relativedelta(months=6)):
|
||||
bills = service.orders.bill(new_open=True, **options)
|
||||
self.assertEqual([], bills)
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous
|
||||
|
||||
from ...models import Service, Plan
|
||||
from ...models import Service, Plan, ContractedPlan
|
||||
|
||||
from . import BaseBillingTest
|
||||
|
||||
|
@ -11,8 +9,8 @@ class PlanBillingTest(BaseBillingTest):
|
|||
def create_plan_service(self):
|
||||
service = Service.objects.create(
|
||||
description="Association membership fee",
|
||||
content_type=ContentType.objects.get_for_model(Miscellaneous),
|
||||
match="account.is_active and account.type == 'ASSOCIATION'",
|
||||
content_type=ContentType.objects.get_for_model(ContractedPlan),
|
||||
match="contractedplan.plan.name == 'association_fee'",
|
||||
billing_period=Service.ANUAL,
|
||||
billing_point=Service.FIXED_DATE,
|
||||
is_fee=True,
|
||||
|
@ -26,12 +24,28 @@ class PlanBillingTest(BaseBillingTest):
|
|||
)
|
||||
return service
|
||||
|
||||
def create_plan(self):
|
||||
def create_plan(self, account=None):
|
||||
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)
|
||||
plan, __ = Plan.objects.get_or_create(name='association_fee')
|
||||
return plan.contracts.create(account=account)
|
||||
|
||||
def test_update_orders(self):
|
||||
account = self.create_account()
|
||||
account1 = self.create_account()
|
||||
self.create_plan(account=account)
|
||||
self.create_plan(account=account1)
|
||||
service = self.create_plan_service()
|
||||
self.assertEqual(0, service.orders.count())
|
||||
service.update_orders()
|
||||
self.assertEqual(2, service.orders.count())
|
||||
|
||||
def test_plan(self):
|
||||
pass
|
||||
account = self.create_account()
|
||||
service = self.create_plan_service()
|
||||
self.create_plan(account=account)
|
||||
bill = account.orders.bill().pop()
|
||||
self.assertEqual(bill.FEE, bill.type)
|
||||
|
||||
|
||||
# TODO test price with multiple plans
|
||||
|
|
|
@ -54,7 +54,8 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
|||
def report_traffic(self, account, value):
|
||||
ct = ContentType.objects.get_for_model(Account)
|
||||
object_id = account.pk
|
||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value, date=timezone.now())
|
||||
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user,
|
||||
value=value, date=timezone.now())
|
||||
data = ResourceData.get_or_create(account, self.resource)
|
||||
data.update()
|
||||
|
||||
|
@ -85,11 +86,20 @@ class TrafficBillingTest(BaseTrafficBillingTest):
|
|||
resource = self.create_traffic_resource()
|
||||
account1 = self.create_account()
|
||||
account2 = self.create_account()
|
||||
# TODO
|
||||
self.report_traffic(account1, 10**10)
|
||||
self.report_traffic(account2, 10**10*5)
|
||||
with freeze_time(timezone.now()+relativedelta(months=1)):
|
||||
bill1 = account1.orders.bill().pop()
|
||||
bill2 = account2.orders.bill().pop()
|
||||
self.assertNotEqual(bill1.get_total(), bill2.get_total())
|
||||
|
||||
|
||||
class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
||||
METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(is_active=True, service__name='traffic prepay').last(), 'amount', 0), 0)"
|
||||
METRIC = ("max("
|
||||
"(account.resources.traffic.used or 0) - "
|
||||
"getattr(account.miscellaneous.filter(is_active=True, service__name='traffic prepay').last(), 'amount', 0)"
|
||||
", 0)"
|
||||
)
|
||||
|
||||
def create_prepay_service(self):
|
||||
service = Service.objects.create(
|
||||
|
@ -114,8 +124,9 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
|
|||
if not account:
|
||||
account = self.create_account()
|
||||
name = 'traffic prepay'
|
||||
service, __ = MiscService.objects.get_or_create(name='traffic prepay', description='Traffic prepay', has_amount=True)
|
||||
return Miscellaneous.objects.create(service=service, description=name, account=account, amount=amount)
|
||||
service, __ = MiscService.objects.get_or_create(name='traffic prepay',
|
||||
description='Traffic prepay', has_amount=True)
|
||||
return account.miscellaneous.create(service=service, description=name, amount=amount)
|
||||
|
||||
def test_traffic_prepay(self):
|
||||
service = self.create_traffic_service()
|
||||
|
|
|
@ -313,6 +313,47 @@ class HandlerTests(BaseTestCase):
|
|||
self.assertEqual(decimal.Decimal('9.00'), results[0].price)
|
||||
self.assertEqual(30, results[0].quantity)
|
||||
|
||||
def test_incomplete_rates(self):
|
||||
service = self.create_ftp_service()
|
||||
account = self.create_account()
|
||||
superplan = Plan.objects.create(
|
||||
name='SUPER', allow_multiple=False, is_combinable=True)
|
||||
service.rates.create(plan=superplan, quantity=4, price=9)
|
||||
service.rates.create(plan=superplan, quantity=10, price=1)
|
||||
account.plans.create(plan=superplan)
|
||||
results = service.get_rates(account, cache=False)
|
||||
results = service.rate_method(results, 30)
|
||||
rates = [
|
||||
{'price': decimal.Decimal('10.00'), 'quantity': 3},
|
||||
{'price': decimal.Decimal('9.00'), 'quantity': 6},
|
||||
{'price': decimal.Decimal('1.00'), 'quantity': 21}
|
||||
]
|
||||
for rate, result in zip(rates, results):
|
||||
self.assertEqual(rate['price'], result.price)
|
||||
self.assertEqual(rate['quantity'], result.quantity)
|
||||
|
||||
def test_zero_rates(self):
|
||||
service = self.create_ftp_service()
|
||||
account = self.create_account()
|
||||
superplan = Plan.objects.create(
|
||||
name='SUPER', allow_multiple=False, is_combinable=True)
|
||||
service.rates.create(plan=superplan, quantity=0, price=0)
|
||||
service.rates.create(plan=superplan, quantity=3, price=10)
|
||||
service.rates.create(plan=superplan, quantity=4, price=9)
|
||||
service.rates.create(plan=superplan, quantity=10, price=1)
|
||||
account.plans.create(plan=superplan)
|
||||
results = service.get_rates(account, cache=False)
|
||||
results = service.rate_method(results, 30)
|
||||
rates = [
|
||||
{'price': decimal.Decimal('0.00'), 'quantity': 2},
|
||||
{'price': decimal.Decimal('10.00'), 'quantity': 1},
|
||||
{'price': decimal.Decimal('9.00'), 'quantity': 6},
|
||||
{'price': decimal.Decimal('1.00'), 'quantity': 21}
|
||||
]
|
||||
for rate, result in zip(rates, results):
|
||||
self.assertEqual(rate['price'], result.price)
|
||||
self.assertEqual(rate['quantity'], result.quantity)
|
||||
|
||||
def test_rates_allow_multiple(self):
|
||||
service = self.create_ftp_service()
|
||||
account = self.create_account()
|
||||
|
@ -352,20 +393,3 @@ class HandlerTests(BaseTestCase):
|
|||
for rate, result in zip(rates, results):
|
||||
self.assertEqual(rate['price'], result.price)
|
||||
self.assertEqual(rate['quantity'], result.quantity)
|
||||
|
||||
def test_generate_bill_lines_with_compensation(self):
|
||||
service = self.create_ftp_service()
|
||||
account = self.create_account()
|
||||
now = timezone.now().date()
|
||||
order = Order(
|
||||
cancelled_on=now,
|
||||
billed_until=now+relativedelta.relativedelta(years=2)
|
||||
)
|
||||
order1 = Order()
|
||||
orders = [order, order1]
|
||||
lines = service.handler.generate_bill_lines(orders, account, commit=False)
|
||||
print lines
|
||||
print len(lines)
|
||||
# TODO
|
||||
|
||||
# TODO test incomplete rate 1 -> nominal_price 10 -> rate
|
||||
|
|
Loading…
Reference in New Issue