Billing
This commit is contained in:
parent
f6045869ac
commit
fba8dac8f5
|
@ -137,7 +137,8 @@ class AccountAdminMixin(object):
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
url = reverse('admin:accounts_account_change', args=(account.pk,))
|
url = reverse('admin:accounts_account_change', args=(account.pk,))
|
||||||
return '<a href="%s">%s</a>' % (url, account.name)
|
pk = account.pk
|
||||||
|
return '<a href="%s">%s</a>' % (url, str(account))
|
||||||
account_link.short_description = _("account")
|
account_link.short_description = _("account")
|
||||||
account_link.allow_tags = True
|
account_link.allow_tags = True
|
||||||
account_link.admin_order_field = 'account__user__username'
|
account_link.admin_order_field = 'account__user__username'
|
||||||
|
|
|
@ -10,6 +10,7 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class Account(models.Model):
|
class Account(models.Model):
|
||||||
|
# Users depends on Accounts (think about what should happen when you delete an account)
|
||||||
user = models.OneToOneField(djsettings.AUTH_USER_MODEL,
|
user = models.OneToOneField(djsettings.AUTH_USER_MODEL,
|
||||||
verbose_name=_("user"), related_name='accounts', null=True)
|
verbose_name=_("user"), related_name='accounts', null=True)
|
||||||
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
|
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
|
||||||
|
@ -24,9 +25,9 @@ class Account(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.user.username
|
return self.user.username if self.user_id else str(self.pk)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_main(cls):
|
def get_main(cls):
|
||||||
|
|
|
@ -220,6 +220,9 @@ class BillLine(models.Model):
|
||||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||||
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
|
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
|
||||||
tax = models.PositiveIntegerField(_("tax"))
|
tax = models.PositiveIntegerField(_("tax"))
|
||||||
|
# TODO
|
||||||
|
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
||||||
|
# help_text=_("Informative link back to the order"))
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True)
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ class MySQLPermissionBackend(ServiceController):
|
||||||
|
|
||||||
|
|
||||||
class MysqlDisk(ServiceMonitor):
|
class MysqlDisk(ServiceMonitor):
|
||||||
model = 'database.Database'
|
model = 'databases.Database'
|
||||||
verbose_name = _("MySQL disk")
|
verbose_name = _("MySQL disk")
|
||||||
|
|
||||||
def exceeded(self, db):
|
def exceeded(self, db):
|
||||||
|
|
|
@ -53,6 +53,8 @@ class BillSelectedOrders(object):
|
||||||
|
|
||||||
def select_related(self, request):
|
def select_related(self, request):
|
||||||
related = self.queryset.get_related().select_related('account__user', 'service')
|
related = self.queryset.get_related().select_related('account__user', 'service')
|
||||||
|
if not related:
|
||||||
|
return self.confirmation(request)
|
||||||
self.options['related_queryset'] = related
|
self.options['related_queryset'] = related
|
||||||
form = BillSelectRelatedForm(initial=self.options)
|
form = BillSelectRelatedForm(initial=self.options)
|
||||||
if int(request.POST.get('step')) >= 2:
|
if int(request.POST.get('step')) >= 2:
|
||||||
|
|
|
@ -39,7 +39,12 @@ def selected_related_choices(queryset):
|
||||||
|
|
||||||
|
|
||||||
class BillSelectRelatedForm(AdminFormMixin, forms.Form):
|
class BillSelectRelatedForm(AdminFormMixin, forms.Form):
|
||||||
selected_related = forms.ModelMultipleChoiceField(label=_("Related"),
|
# This doesn't work well with reordering after billing
|
||||||
|
# pricing_with_all = forms.BooleanField(label=_("Do pricing with all orders"),
|
||||||
|
# initial=False, required=False, help_text=_("The price may vary "
|
||||||
|
# "depending on the billed orders. This options designates whether "
|
||||||
|
# "all existing orders will be used for price computation or not."))
|
||||||
|
selected_related = forms.ModelMultipleChoiceField(label=_("Related orders"),
|
||||||
queryset=Order.objects.none(), widget=forms.CheckboxSelectMultiple,
|
queryset=Order.objects.none(), widget=forms.CheckboxSelectMultiple,
|
||||||
required=False)
|
required=False)
|
||||||
billing_point = forms.DateField(widget=forms.HiddenInput())
|
billing_point = forms.DateField(widget=forms.HiddenInput())
|
||||||
|
|
|
@ -10,8 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
from orchestra.utils.python import AttributeDict
|
from orchestra.utils.python import AttributeDict
|
||||||
|
|
||||||
from . import settings
|
from . import settings, helpers
|
||||||
from .helpers import get_register_or_cancel_events, get_register_or_renew_events
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceHandler(plugins.Plugin):
|
class ServiceHandler(plugins.Plugin):
|
||||||
|
@ -138,9 +137,9 @@ class ServiceHandler(plugins.Plugin):
|
||||||
).filter(registered_on__lt=end).order_by('registered_on')
|
).filter(registered_on__lt=end).order_by('registered_on')
|
||||||
price = 0
|
price = 0
|
||||||
if self.orders_effect == self.REGISTER_OR_RENEW:
|
if self.orders_effect == self.REGISTER_OR_RENEW:
|
||||||
events = get_register_or_renew_events(porders, order, ini, end)
|
events = helpers.get_register_or_renew_events(porders, order, ini, end)
|
||||||
elif self.orders_effect == self.CONCURRENT:
|
elif self.orders_effect == self.CONCURRENT:
|
||||||
events = get_register_or_cancel_events(porders, order, ini, end)
|
events = helpers.get_register_or_cancel_events(porders, order, ini, end)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
for metric, position, ratio in events:
|
for metric, position, ratio in events:
|
||||||
|
@ -171,6 +170,68 @@ class ServiceHandler(plugins.Plugin):
|
||||||
'discounts': discounts,
|
'discounts': discounts,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _generate_bill_lines(self, orders, **options):
|
||||||
|
# For the "boundary conditions" just think that:
|
||||||
|
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
||||||
|
# In most cases:
|
||||||
|
# ini >= registered_date, end < registered_date
|
||||||
|
|
||||||
|
# TODO Perform compensations on cancelled services
|
||||||
|
if self.on_cancel in (self.COMPENSATE, self.REFOUND):
|
||||||
|
pass
|
||||||
|
# TODO compensations with commit=False, fuck commit or just fuck the transaction?
|
||||||
|
# compensate(orders, **options)
|
||||||
|
# TODO create discount per compensation
|
||||||
|
bp = None
|
||||||
|
lines = []
|
||||||
|
commit = options.get('commit', True)
|
||||||
|
ini = datetime.date.max
|
||||||
|
end = datetime.date.ini
|
||||||
|
# boundary lookup
|
||||||
|
for order in orders:
|
||||||
|
cini = order.registered_on
|
||||||
|
if order.billed_until:
|
||||||
|
cini = order.billed_until
|
||||||
|
bp = self.get_billing_point(order, bp=bp, **options)
|
||||||
|
order.new_billed_until = bp
|
||||||
|
ini = min(ini, cini)
|
||||||
|
end = max(end, bp) # TODO if all bp are the same ...
|
||||||
|
|
||||||
|
porders = orders.pricing_orders(ini=ini, end=end)
|
||||||
|
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||||
|
# Compensation
|
||||||
|
compensations = []
|
||||||
|
receivers = []
|
||||||
|
for order in porders:
|
||||||
|
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
||||||
|
compensations.append[Interval(order.cancelled_on, order.billed_until, order)]
|
||||||
|
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||||
|
for order in orders:
|
||||||
|
order_interval = Interval(order.billed_until or order.registered_on, order.new_billed_until)
|
||||||
|
helpers.compensate(order_interval, compensations)
|
||||||
|
|
||||||
|
def get_chunks(self, porders, ini, end, ix=0):
|
||||||
|
if ix >= len(porders):
|
||||||
|
return [[ini, end, []]]
|
||||||
|
order = porders[ix]
|
||||||
|
ix += 1
|
||||||
|
bu = getattr(order, 'new_billed_until', order.billed_until)
|
||||||
|
if not bu or bu <= ini or order.registered_on >= end:
|
||||||
|
return self.get_chunks(porders, ini, end, ix=ix)
|
||||||
|
result = []
|
||||||
|
if order.registered_on < end and order.registered_on > ini:
|
||||||
|
ro = order.registered_on
|
||||||
|
result = self.get_chunks(porders, ini, ro, ix=ix)
|
||||||
|
ini = ro
|
||||||
|
if bu < end:
|
||||||
|
result += self.get_chunks(porders, bu, end, ix=ix)
|
||||||
|
end = bu
|
||||||
|
chunks = self.get_chunks(porders, ini, end, ix=ix)
|
||||||
|
for chunk in chunks:
|
||||||
|
chunk[2].insert(0, order)
|
||||||
|
result.append(chunk)
|
||||||
|
return result
|
||||||
|
|
||||||
def generate_bill_lines(self, orders, **options):
|
def generate_bill_lines(self, orders, **options):
|
||||||
# For the "boundary conditions" just think that:
|
# For the "boundary conditions" just think that:
|
||||||
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
|
||||||
|
|
|
@ -88,3 +88,89 @@ def get_register_or_renew_events(handler, porders, order, ini, end):
|
||||||
elif porder.billed_until > send or porder.cancelled_on > send:
|
elif porder.billed_until > send or porder.cancelled_on > send:
|
||||||
counter += 1
|
counter += 1
|
||||||
yield counter, position, (send-sini)/total
|
yield counter, position, (send-sini)/total
|
||||||
|
|
||||||
|
|
||||||
|
def cmp_billed_until_or_registered_on(a, b):
|
||||||
|
"""
|
||||||
|
1) billed_until greater first
|
||||||
|
2) registered_on smaller first
|
||||||
|
"""
|
||||||
|
if a.billed_until == b.billed_until:
|
||||||
|
return (a.registered_on-b.registered_on).days
|
||||||
|
elif a.billed_until and b.billed_until:
|
||||||
|
return (b.billed_until-a.billed_until).days
|
||||||
|
elif a.billed_until:
|
||||||
|
return (b.registered_on-a.billed_until).days
|
||||||
|
return (b.billed_until-a.registered_on).days
|
||||||
|
|
||||||
|
|
||||||
|
class Interval(object):
|
||||||
|
def __init__(self, ini, end, order=None):
|
||||||
|
self.ini = ini
|
||||||
|
self.end = end
|
||||||
|
self.order = order
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return max((self.end-self.ini).days, 0)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
remaining = []
|
||||||
|
if self.ini < other.ini:
|
||||||
|
remaining.append(Interval(self.ini, min(self.end, other.ini)))
|
||||||
|
if self.end > other.end:
|
||||||
|
remaining.append(Interval(max(self.ini,other.end), self.end))
|
||||||
|
return remaining
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Start: %s End: %s" % (self.ini, self.end)
|
||||||
|
|
||||||
|
def intersect(self, other, remaining_self=None, remaining_other=None):
|
||||||
|
if remaining_self is not None:
|
||||||
|
remaining_self += (self - other)
|
||||||
|
if remaining_other is not None:
|
||||||
|
remaining_other += (other - self)
|
||||||
|
result = Interval(max(self.ini, other.ini), min(self.end, other.end))
|
||||||
|
if len(result)>0:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_intersections(order, compensations):
|
||||||
|
intersections = []
|
||||||
|
for compensation in compensations:
|
||||||
|
intersection = compensation.intersect(order)
|
||||||
|
if intersection:
|
||||||
|
intersections.append((len(intersection), intersection))
|
||||||
|
return intersections
|
||||||
|
|
||||||
|
# Intervals should not overlap
|
||||||
|
def intersect(compensation, order_intervals):
|
||||||
|
compensated = []
|
||||||
|
not_compensated = []
|
||||||
|
unused_compensation = []
|
||||||
|
for interval in order_intervals:
|
||||||
|
compensated.append(compensation.intersect(interval, unused_compensation, not_compensated))
|
||||||
|
return (compensated, not_compensated, unused_compensation)
|
||||||
|
|
||||||
|
|
||||||
|
def update_intersections(not_compensated, compensations):
|
||||||
|
intersections = []
|
||||||
|
for (_,compensation) in compensations:
|
||||||
|
intersections += get_intersections(compensation, not_compensated)
|
||||||
|
return intersections
|
||||||
|
|
||||||
|
|
||||||
|
def compensate(order, compensations):
|
||||||
|
intersections = get_intersections(order, compensations)
|
||||||
|
not_compensated = [order]
|
||||||
|
result = []
|
||||||
|
while intersections:
|
||||||
|
# Apply the biggest intersection
|
||||||
|
intersections.sort(reverse=True)
|
||||||
|
(_,intersection) = intersections.pop()
|
||||||
|
(compensated, not_compensated, unused_compensation) = intersect(intersection, not_compensated)
|
||||||
|
# Reorder de intersections:
|
||||||
|
intersections = update_intersections(not_compensated, intersections)
|
||||||
|
result += compensated
|
||||||
|
return result
|
||||||
|
|
|
@ -152,6 +152,9 @@ class Service(models.Model):
|
||||||
(MATCH_PRICE, _("Match price")),
|
(MATCH_PRICE, _("Match price")),
|
||||||
),
|
),
|
||||||
default=BEST_PRICE)
|
default=BEST_PRICE)
|
||||||
|
# TODO remove since it can be infered from pricing period?
|
||||||
|
# VARIABLE -> REGISTER_OR_RENEW
|
||||||
|
# FIXED -> CONCURRENT
|
||||||
orders_effect = models.CharField(_("orders effect"), max_length=16,
|
orders_effect = models.CharField(_("orders effect"), max_length=16,
|
||||||
help_text=_("Defines the lookup behaviour when using orders for "
|
help_text=_("Defines the lookup behaviour when using orders for "
|
||||||
"the pricing rate computation of this service."),
|
"the pricing rate computation of this service."),
|
||||||
|
@ -321,7 +324,38 @@ class OrderQuerySet(models.QuerySet):
|
||||||
bills += [(account, bill_lines)]
|
bills += [(account, bill_lines)]
|
||||||
return bills
|
return bills
|
||||||
|
|
||||||
def get_related(self):
|
def pricing_effect(self, ini=None, end=None, **options):
|
||||||
|
# TODO register but not billed duscard
|
||||||
|
if not ini:
|
||||||
|
for cini, ro in self.values_list('billed_until', 'registered_on'):
|
||||||
|
if not cini:
|
||||||
|
cini = ro
|
||||||
|
if not ini:
|
||||||
|
ini = cini
|
||||||
|
|
||||||
|
ini = min(ini, cini)
|
||||||
|
if not end:
|
||||||
|
order = self.first()
|
||||||
|
if order:
|
||||||
|
service = order.service
|
||||||
|
service.billing_point == service.FIXED_DATE
|
||||||
|
end = service.handler.get_billing_point(order, **options)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
return self.exclude(
|
||||||
|
cancelled_on__isnull=False, billed_until__isnull=False,
|
||||||
|
cancelled_on__lte=F('billed_until'), billed_until__lte=ini,
|
||||||
|
registered_on__gte=end)
|
||||||
|
|
||||||
|
def get_related(self, ini=None, end=None):
|
||||||
|
if not ini:
|
||||||
|
ini = ''
|
||||||
|
if not end:
|
||||||
|
end = ''
|
||||||
|
return self.pricing_effect().filter(
|
||||||
|
Q(billed_until__isnull=False, billed_until__lt=end) |
|
||||||
|
Q(billed_until__isnull=True, registered_on__lt=end))
|
||||||
|
# TODO iterate over every order, calculate its billing point and find related
|
||||||
qs = self.exclude(cancelled_on__isnull=False,
|
qs = self.exclude(cancelled_on__isnull=False,
|
||||||
billed_until__gte=F('cancelled_on')).distinct()
|
billed_until__gte=F('cancelled_on')).distinct()
|
||||||
original_ids = self.values_list('id', flat=True)
|
original_ids = self.values_list('id', flat=True)
|
||||||
|
@ -439,7 +473,8 @@ class MetricStorage(models.Model):
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# TODO If this happens to be very costly then, consider an additional
|
||||||
|
# implementation when runnning within a request/Response cycle, more efficient :)
|
||||||
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
||||||
def cancel_orders(sender, **kwargs):
|
def cancel_orders(sender, **kwargs):
|
||||||
if sender in services:
|
if sender in services:
|
||||||
|
|
|
@ -6,10 +6,11 @@ from django.utils import timezone
|
||||||
|
|
||||||
from orchestra.apps.accounts.models import Account
|
from orchestra.apps.accounts.models import Account
|
||||||
from orchestra.apps.users.models import User
|
from orchestra.apps.users.models import User
|
||||||
from orchestra.utils.tests import BaseTestCase
|
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||||
|
|
||||||
from ... import settings
|
from ... import settings
|
||||||
from ...models import Service
|
from ...helpers import cmp_billed_until_or_registered_on
|
||||||
|
from ...models import Service, Order
|
||||||
|
|
||||||
|
|
||||||
class OrderTests(BaseTestCase):
|
class OrderTests(BaseTestCase):
|
||||||
|
@ -51,10 +52,7 @@ class OrderTests(BaseTestCase):
|
||||||
quantity=1,
|
quantity=1,
|
||||||
price=9,
|
price=9,
|
||||||
)
|
)
|
||||||
account = self.create_account()
|
self.account = self.create_account()
|
||||||
user = User.objects.create_user(username='rata_palida_ftp', account=account)
|
|
||||||
POSIX = user._meta.get_field_by_name('posix')[0].model
|
|
||||||
POSIX.objects.create(user=user)
|
|
||||||
return service
|
return service
|
||||||
|
|
||||||
# def test_ftp_account_1_year_fiexed(self):
|
# def test_ftp_account_1_year_fiexed(self):
|
||||||
|
@ -63,23 +61,176 @@ class OrderTests(BaseTestCase):
|
||||||
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||||
# self.assertEqual(20, bills[0].get_total())
|
# self.assertEqual(20, bills[0].get_total())
|
||||||
|
|
||||||
def test_ftp_account_1_year_fiexed(self):
|
def create_ftp(self):
|
||||||
|
username = '%s_ftp' % random_ascii(10)
|
||||||
|
user = User.objects.create_user(username=username, account=self.account)
|
||||||
|
POSIX = user._meta.get_field_by_name('posix')[0].model
|
||||||
|
POSIX.objects.create(user=user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def atest_get_chunks(self):
|
||||||
service = self.create_service()
|
service = self.create_service()
|
||||||
|
handler = service.handler
|
||||||
|
porders = []
|
||||||
now = timezone.now().date()
|
now = timezone.now().date()
|
||||||
month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
|
ct = ContentType.objects.get_for_model(User)
|
||||||
ini = datetime.datetime(year=now.year, month=month,
|
|
||||||
day=1, tzinfo=timezone.get_current_timezone())
|
ftp = self.create_ftp()
|
||||||
order = service.orders.all()[0]
|
order = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
order.registered_on = ini
|
porders.append(order)
|
||||||
order.save()
|
end = handler.get_billing_point(order).date()
|
||||||
bp = ini
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
|
self.assertEqual(1, len(chunks))
|
||||||
print bills[0][1][0].subtotal
|
self.assertIn([now, end, []], chunks)
|
||||||
print bills
|
|
||||||
bp = ini + relativedelta.relativedelta(months=12)
|
ftp = self.create_ftp()
|
||||||
bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
|
order1 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
print bills[0][1][0].subtotal
|
order1.billed_until = now+datetime.timedelta(days=2)
|
||||||
print bills
|
porders.append(order1)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(2, len(chunks))
|
||||||
|
self.assertIn([order1.registered_on, order1.billed_until, [order1]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, end, []], chunks)
|
||||||
|
|
||||||
|
ftp = self.create_ftp()
|
||||||
|
order2 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
|
order2.billed_until = now+datetime.timedelta(days=700)
|
||||||
|
porders.append(order2)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(2, len(chunks))
|
||||||
|
self.assertIn([order.registered_on, order1.billed_until, [order1, order2]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, end, [order2]], chunks)
|
||||||
|
|
||||||
|
ftp = self.create_ftp()
|
||||||
|
order3 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
|
order3.billed_until = now+datetime.timedelta(days=700)
|
||||||
|
porders.append(order3)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(2, len(chunks))
|
||||||
|
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, end, [order2, order3]], chunks)
|
||||||
|
|
||||||
|
ftp = self.create_ftp()
|
||||||
|
order4 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
|
order4.registered_on = now+datetime.timedelta(days=5)
|
||||||
|
order4.billed_until = now+datetime.timedelta(days=10)
|
||||||
|
porders.append(order4)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(4, len(chunks))
|
||||||
|
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
|
||||||
|
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
|
||||||
|
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
||||||
|
|
||||||
|
ftp = self.create_ftp()
|
||||||
|
order5 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
|
order5.registered_on = now+datetime.timedelta(days=700)
|
||||||
|
order5.billed_until = now+datetime.timedelta(days=780)
|
||||||
|
porders.append(order5)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(4, len(chunks))
|
||||||
|
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
|
||||||
|
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
|
||||||
|
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
||||||
|
|
||||||
|
ftp = self.create_ftp()
|
||||||
|
order6 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||||
|
order6.registered_on = now-datetime.timedelta(days=780)
|
||||||
|
order6.billed_until = now-datetime.timedelta(days=700)
|
||||||
|
porders.append(order6)
|
||||||
|
chunks = handler.get_chunks(porders, now, end)
|
||||||
|
self.assertEqual(4, len(chunks))
|
||||||
|
self.assertIn([order.registered_on, order1.billed_until, [order1, order2, order3]], chunks)
|
||||||
|
self.assertIn([order1.billed_until, order4.registered_on, [order2, order3]], chunks)
|
||||||
|
self.assertIn([order4.registered_on, order4.billed_until, [order2, order3, order4]], chunks)
|
||||||
|
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
||||||
|
|
||||||
|
def atest_sort_billed_until_or_registered_on(self):
|
||||||
|
service = self.create_service()
|
||||||
|
now = timezone.now()
|
||||||
|
order = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now,
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order1 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=5),
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order2 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=6),
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order3 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=6),
|
||||||
|
billed_until=now+datetime.timedelta(days=201))
|
||||||
|
order4 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=6))
|
||||||
|
order5 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=7))
|
||||||
|
order6 = Order(
|
||||||
|
service=service,
|
||||||
|
registered_on=now+datetime.timedelta(days=8))
|
||||||
|
orders = [order3, order, order1, order2, order4, order5, order6]
|
||||||
|
self.assertEqual(orders, sorted(orders, cmp=cmp_billed_until_or_registered_on))
|
||||||
|
|
||||||
|
def test_compensation(self):
|
||||||
|
now = timezone.now()
|
||||||
|
order = Order(
|
||||||
|
registered_on=now,
|
||||||
|
billed_until=now+datetime.timedelta(days=200),
|
||||||
|
cancelled_on=now+datetime.timedelta(days=100))
|
||||||
|
order1 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=5),
|
||||||
|
cancelled_on=now+datetime.timedelta(days=190),
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order2 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=6),
|
||||||
|
cancelled_on=now+datetime.timedelta(days=200),
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order3 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=6),
|
||||||
|
billed_until=now+datetime.timedelta(days=200))
|
||||||
|
order4 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=6))
|
||||||
|
order5 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=7))
|
||||||
|
order6 = Order(
|
||||||
|
registered_on=now+datetime.timedelta(days=8))
|
||||||
|
porders = [order3, order, order1, order2, order4, order5, order6]
|
||||||
|
porders = sorted(porders, cmp=cmp_billed_until_or_registered_on)
|
||||||
|
service = self.create_service()
|
||||||
|
compensations = []
|
||||||
|
from ... import helpers
|
||||||
|
for order in porders:
|
||||||
|
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
||||||
|
compensations.append(helpers.Interval(order.cancelled_on, order.billed_until, order=order))
|
||||||
|
for order in porders:
|
||||||
|
bp = service.handler.get_billing_point(order)
|
||||||
|
order_interval = helpers.Interval(order.billed_until or order.registered_on, bp)
|
||||||
|
print helpers.compensate(order_interval, compensations)
|
||||||
|
|
||||||
|
|
||||||
|
# def test_ftp_account_1_year_fiexed(self):
|
||||||
|
# service = self.create_service()
|
||||||
|
# now = timezone.now().date()etb
|
||||||
|
# month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
|
||||||
|
# ini = datetime.datetime(year=now.year, month=month,
|
||||||
|
# day=1, tzinfo=timezone.get_current_timezone())
|
||||||
|
# order = service.orders.all()[0]
|
||||||
|
# order.registered_on = ini
|
||||||
|
# order.save()
|
||||||
|
# bp = ini
|
||||||
|
# bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
|
||||||
|
# print bills[0][1][0].subtotal
|
||||||
|
# print bills
|
||||||
|
# bp = ini + relativedelta.relativedelta(months=12)
|
||||||
|
# bills = service.orders.bill(billing_point=bp, fixed_point=False, commit=False)
|
||||||
|
# print bills[0][1][0].subtotal
|
||||||
|
# print bills
|
||||||
# def test_ftp_account_2_year_fiexed(self):
|
# def test_ftp_account_2_year_fiexed(self):
|
||||||
# service = self.create_service()
|
# service = self.create_service()
|
||||||
# bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
# bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
||||||
|
|
|
@ -65,6 +65,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
from ..models import TransactionProcess
|
from ..models import TransactionProcess
|
||||||
process = TransactionProcess.objects.create()
|
process = TransactionProcess.objects.create()
|
||||||
context = cls.get_context(transactions)
|
context = cls.get_context(transactions)
|
||||||
|
# http://businessbanking.bankofireland.com/fs/doc/wysiwyg/b22440-mss130725-pain001-xml-file-structure-dec13.pdf
|
||||||
sepa = lxml.builder.ElementMaker(
|
sepa = lxml.builder.ElementMaker(
|
||||||
nsmap = {
|
nsmap = {
|
||||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||||
|
@ -75,12 +76,12 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
E.CstmrCdtTrfInitn(
|
E.CstmrCdtTrfInitn(
|
||||||
cls.get_header(context),
|
cls.get_header(context),
|
||||||
E.PmtInf( # Payment Info
|
E.PmtInf( # Payment Info
|
||||||
E.PmtInfId(str(process.id)), # Payment Id
|
E.PmtInfId(str(process.id)), # Payment Id
|
||||||
E.PmtMtd("TRF"), # Payment Method
|
E.PmtMtd("TRF"), # Payment Method
|
||||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||||
E.CtrlSum(context['total']), # Control Sum
|
E.CtrlSum(context['total']), # Control Sum
|
||||||
E.ReqdExctnDt ( # Requested Execution Date
|
E.ReqdExctnDt( # Requested Execution Date
|
||||||
context['now'].strftime("%Y-%m-%d")
|
(context['now']+datetime.timedelta(days=10)).strftime("%Y-%m-%d")
|
||||||
),
|
),
|
||||||
E.Dbtr( # Debtor
|
E.Dbtr( # Debtor
|
||||||
E.Nm(context['name'])
|
E.Nm(context['name'])
|
||||||
|
@ -108,6 +109,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
from ..models import TransactionProcess
|
from ..models import TransactionProcess
|
||||||
process = TransactionProcess.objects.create()
|
process = TransactionProcess.objects.create()
|
||||||
context = cls.get_context(transactions)
|
context = cls.get_context(transactions)
|
||||||
|
# http://businessbanking.bankofireland.com/fs/doc/wysiwyg/sepa-direct-debit-pain-008-001-02-xml-file-structure-july-2013.pdf
|
||||||
sepa = lxml.builder.ElementMaker(
|
sepa = lxml.builder.ElementMaker(
|
||||||
nsmap = {
|
nsmap = {
|
||||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||||
|
@ -118,7 +120,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
E.CstmrDrctDbtInitn(
|
E.CstmrDrctDbtInitn(
|
||||||
cls.get_header(context, process),
|
cls.get_header(context, process),
|
||||||
E.PmtInf( # Payment Info
|
E.PmtInf( # Payment Info
|
||||||
E.PmtInfId(str(process.id)), # Payment Id
|
E.PmtInfId(str(process.id)), # Payment Id
|
||||||
E.PmtMtd("DD"), # Payment Method
|
E.PmtMtd("DD"), # Payment Method
|
||||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||||
E.CtrlSum(context['total']), # Control Sum
|
E.CtrlSum(context['total']), # Control Sum
|
||||||
|
|
Loading…
Reference in New Issue