Billing
This commit is contained in:
parent
f6045869ac
commit
fba8dac8f5
|
@ -137,7 +137,8 @@ class AccountAdminMixin(object):
|
|||
def account_link(self, instance):
|
||||
account = instance.account if instance.pk else self.account
|
||||
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.allow_tags = True
|
||||
account_link.admin_order_field = 'account__user__username'
|
||||
|
|
|
@ -10,6 +10,7 @@ from . import settings
|
|||
|
||||
|
||||
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,
|
||||
verbose_name=_("user"), related_name='accounts', null=True)
|
||||
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
|
||||
|
@ -24,9 +25,9 @@ class Account(models.Model):
|
|||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def name(self):
|
||||
return self.user.username
|
||||
return self.user.username if self.user_id else str(self.pk)
|
||||
|
||||
@classmethod
|
||||
def get_main(cls):
|
||||
|
|
|
@ -220,6 +220,9 @@ class BillLine(models.Model):
|
|||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
|
||||
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"),
|
||||
related_name='amendment_lines', null=True, blank=True)
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ class MySQLPermissionBackend(ServiceController):
|
|||
|
||||
|
||||
class MysqlDisk(ServiceMonitor):
|
||||
model = 'database.Database'
|
||||
model = 'databases.Database'
|
||||
verbose_name = _("MySQL disk")
|
||||
|
||||
def exceeded(self, db):
|
||||
|
|
|
@ -53,6 +53,8 @@ class BillSelectedOrders(object):
|
|||
|
||||
def select_related(self, request):
|
||||
related = self.queryset.get_related().select_related('account__user', 'service')
|
||||
if not related:
|
||||
return self.confirmation(request)
|
||||
self.options['related_queryset'] = related
|
||||
form = BillSelectRelatedForm(initial=self.options)
|
||||
if int(request.POST.get('step')) >= 2:
|
||||
|
|
|
@ -39,7 +39,12 @@ def selected_related_choices(queryset):
|
|||
|
||||
|
||||
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,
|
||||
required=False)
|
||||
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.python import AttributeDict
|
||||
|
||||
from . import settings
|
||||
from .helpers import get_register_or_cancel_events, get_register_or_renew_events
|
||||
from . import settings, helpers
|
||||
|
||||
|
||||
class ServiceHandler(plugins.Plugin):
|
||||
|
@ -138,9 +137,9 @@ class ServiceHandler(plugins.Plugin):
|
|||
).filter(registered_on__lt=end).order_by('registered_on')
|
||||
price = 0
|
||||
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:
|
||||
events = get_register_or_cancel_events(porders, order, ini, end)
|
||||
events = helpers.get_register_or_cancel_events(porders, order, ini, end)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
for metric, position, ratio in events:
|
||||
|
@ -171,6 +170,68 @@ class ServiceHandler(plugins.Plugin):
|
|||
'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):
|
||||
# For the "boundary conditions" just think that:
|
||||
# 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:
|
||||
counter += 1
|
||||
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")),
|
||||
),
|
||||
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,
|
||||
help_text=_("Defines the lookup behaviour when using orders for "
|
||||
"the pricing rate computation of this service."),
|
||||
|
@ -320,8 +323,39 @@ class OrderQuerySet(models.QuerySet):
|
|||
else:
|
||||
bills += [(account, bill_lines)]
|
||||
return bills
|
||||
|
||||
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):
|
||||
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,
|
||||
billed_until__gte=F('cancelled_on')).distinct()
|
||||
original_ids = self.values_list('id', flat=True)
|
||||
|
@ -439,7 +473,8 @@ class MetricStorage(models.Model):
|
|||
except cls.DoesNotExist:
|
||||
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")
|
||||
def cancel_orders(sender, **kwargs):
|
||||
if sender in services:
|
||||
|
|
|
@ -6,10 +6,11 @@ from django.utils import timezone
|
|||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
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 ...models import Service
|
||||
from ...helpers import cmp_billed_until_or_registered_on
|
||||
from ...models import Service, Order
|
||||
|
||||
|
||||
class OrderTests(BaseTestCase):
|
||||
|
@ -51,10 +52,7 @@ class OrderTests(BaseTestCase):
|
|||
quantity=1,
|
||||
price=9,
|
||||
)
|
||||
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)
|
||||
self.account = self.create_account()
|
||||
return service
|
||||
|
||||
# def test_ftp_account_1_year_fiexed(self):
|
||||
|
@ -62,24 +60,177 @@ class OrderTests(BaseTestCase):
|
|||
# bp = timezone.now().date() + relativedelta.relativedelta(years=1)
|
||||
# bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||
# 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()
|
||||
handler = service.handler
|
||||
porders = []
|
||||
now = timezone.now().date()
|
||||
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
|
||||
ct = ContentType.objects.get_for_model(User)
|
||||
|
||||
ftp = self.create_ftp()
|
||||
order = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||
porders.append(order)
|
||||
end = handler.get_billing_point(order).date()
|
||||
chunks = handler.get_chunks(porders, now, end)
|
||||
self.assertEqual(1, len(chunks))
|
||||
self.assertIn([now, end, []], chunks)
|
||||
|
||||
ftp = self.create_ftp()
|
||||
order1 = Order.objects.get(content_type=ct, object_id=ftp.pk)
|
||||
order1.billed_until = now+datetime.timedelta(days=2)
|
||||
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):
|
||||
# service = self.create_service()
|
||||
# bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
||||
|
|
|
@ -65,6 +65,7 @@ class SEPADirectDebit(PaymentMethod):
|
|||
from ..models import TransactionProcess
|
||||
process = TransactionProcess.objects.create()
|
||||
context = cls.get_context(transactions)
|
||||
# http://businessbanking.bankofireland.com/fs/doc/wysiwyg/b22440-mss130725-pain001-xml-file-structure-dec13.pdf
|
||||
sepa = lxml.builder.ElementMaker(
|
||||
nsmap = {
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
|
@ -75,12 +76,12 @@ class SEPADirectDebit(PaymentMethod):
|
|||
E.CstmrCdtTrfInitn(
|
||||
cls.get_header(context),
|
||||
E.PmtInf( # Payment Info
|
||||
E.PmtInfId(str(process.id)), # Payment Id
|
||||
E.PmtInfId(str(process.id)), # Payment Id
|
||||
E.PmtMtd("TRF"), # Payment Method
|
||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||
E.CtrlSum(context['total']), # Control Sum
|
||||
E.ReqdExctnDt ( # Requested Execution Date
|
||||
context['now'].strftime("%Y-%m-%d")
|
||||
E.ReqdExctnDt( # Requested Execution Date
|
||||
(context['now']+datetime.timedelta(days=10)).strftime("%Y-%m-%d")
|
||||
),
|
||||
E.Dbtr( # Debtor
|
||||
E.Nm(context['name'])
|
||||
|
@ -108,6 +109,7 @@ class SEPADirectDebit(PaymentMethod):
|
|||
from ..models import TransactionProcess
|
||||
process = TransactionProcess.objects.create()
|
||||
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(
|
||||
nsmap = {
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
|
@ -118,7 +120,7 @@ class SEPADirectDebit(PaymentMethod):
|
|||
E.CstmrDrctDbtInitn(
|
||||
cls.get_header(context, process),
|
||||
E.PmtInf( # Payment Info
|
||||
E.PmtInfId(str(process.id)), # Payment Id
|
||||
E.PmtInfId(str(process.id)), # Payment Id
|
||||
E.PmtMtd("DD"), # Payment Method
|
||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||
E.CtrlSum(context['total']), # Control Sum
|
||||
|
|
Loading…
Reference in a new issue