Improvements on order billing
This commit is contained in:
parent
fba8dac8f5
commit
6533331461
|
@ -170,18 +170,12 @@ class ServiceHandler(plugins.Plugin):
|
|||
'discounts': discounts,
|
||||
})
|
||||
|
||||
def _generate_bill_lines(self, orders, **options):
|
||||
def _generate_bill_lines(self, orders, account, **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)
|
||||
|
@ -197,18 +191,45 @@ class ServiceHandler(plugins.Plugin):
|
|||
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
|
||||
related_orders = Order.objects.filter(service=self.service, account=account)
|
||||
if self.on_cancel in (self.COMPENSATE, self.REFOUND):
|
||||
# Get orders pending for compensation
|
||||
givers = related_orders.filter_givers(ini, end)
|
||||
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
self.compensate(givers, orders)
|
||||
|
||||
# Get pricing orders
|
||||
porders = related_orders.filter_pricing_orders(ini, end)
|
||||
porders = set(orders).union(set(porders))
|
||||
for ini, end, orders in self.get_chunks(porders, ini, end):
|
||||
if self.pricing_period == self.ANUAL:
|
||||
pass
|
||||
elif self.pricing_period == self.MONTHLY:
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError
|
||||
metric = len(orders)
|
||||
for position, order in enumerate(orders):
|
||||
# TODO position +1?
|
||||
price = self.get_price(order, metric, position=position)
|
||||
price *= size
|
||||
|
||||
def compensate(self, givers, receivers):
|
||||
compensations = []
|
||||
receivers = []
|
||||
for order in porders:
|
||||
for order in givers:
|
||||
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)
|
||||
for order in receivers:
|
||||
if not order.billed_until or order.billed_until < order.new_billed_until:
|
||||
# receiver
|
||||
ini = order.billed_until or order.registered_on
|
||||
end = order.cancelled_on or datetime.date.max
|
||||
order_interval = helpers.Interval(ini, order.new_billed_until) # TODO beyond interval?
|
||||
compensations, used_compensations = helpers.compensate(order_interval, compensations)
|
||||
order._compensations = used_compensations
|
||||
for comp in used_compensations:
|
||||
comp.order.billed_until = min(comp.order.billed_until, comp.end)
|
||||
|
||||
def get_chunks(self, porders, ini, end, ix=0):
|
||||
if ix >= len(porders):
|
||||
|
@ -267,44 +288,3 @@ class ServiceHandler(plugins.Plugin):
|
|||
if commit:
|
||||
order.save()
|
||||
return lines
|
||||
|
||||
def compensate(self, orders):
|
||||
# TODO this compensation is a bit hard to write it propertly
|
||||
# don't forget to think about weighted and num order prices.
|
||||
# Greedy algorithm for maximizing discount (non-deterministic)
|
||||
# Reduce and break orders in donors and receivers
|
||||
donors = []
|
||||
receivers = []
|
||||
for order in orders:
|
||||
if order.cancelled_on and order.billed_until > order.cancelled_on:
|
||||
donors.append(order)
|
||||
elif not order.cancelled_on or order.cancelled_on > order.billed_until:
|
||||
receivers.append(order)
|
||||
|
||||
# Assign weights to every donor-receiver combination
|
||||
weights = []
|
||||
for donor in donors:
|
||||
for receiver in receivers:
|
||||
if receiver.cancelled_on:
|
||||
if not receiver.cancelled_on or receiver.cancelled_on < donor.billed_until:
|
||||
end = receiver.cancelled_on
|
||||
else:
|
||||
end = donor.billed_until
|
||||
else:
|
||||
end = donor.billed_until
|
||||
ini = donor.billed_until or donor.registered_on
|
||||
if donor.cancelled_on > ini:
|
||||
ini = donor.cancelled_on
|
||||
weight = (end-ini).days
|
||||
weights.append((weight, ini, end, donor, receiver))
|
||||
|
||||
# Choose weightest pairs
|
||||
choosen = []
|
||||
weights.sort(key=lambda n: n[0])
|
||||
for weight, ini, end, donor, receiver in weigths:
|
||||
if donor not in choosen and receiver not in choosen:
|
||||
choosen += [donor, receiver]
|
||||
donor.billed_until = end
|
||||
donor.save()
|
||||
price = self.get_price()#TODO
|
||||
receiver.__discount_per_compensation =None
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import inspect
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from orchestra.apps.accounts.models import Account
|
||||
|
||||
|
||||
|
@ -116,32 +118,44 @@ class Interval(object):
|
|||
def __sub__(self, other):
|
||||
remaining = []
|
||||
if self.ini < other.ini:
|
||||
remaining.append(Interval(self.ini, min(self.end, other.ini)))
|
||||
remaining.append(Interval(self.ini, min(self.end, other.ini), self.order))
|
||||
if self.end > other.end:
|
||||
remaining.append(Interval(max(self.ini,other.end), self.end))
|
||||
remaining.append(Interval(max(self.ini,other.end), self.end, self.order))
|
||||
return remaining
|
||||
|
||||
def __repr__(self):
|
||||
return "Start: %s End: %s" % (self.ini, self.end)
|
||||
now = timezone.now()
|
||||
return "Start: %s End: %s" % ((self.ini-now).days, (self.end-now).days)
|
||||
|
||||
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))
|
||||
result = Interval(max(self.ini, other.ini), min(self.end, other.end), self.order)
|
||||
if len(result)>0:
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
|
||||
def intersect_set(self, others, remaining_self=None, remaining_other=None):
|
||||
intersections = []
|
||||
for interval in others:
|
||||
intersection = self.intersect(interval, remaining_self, remaining_other)
|
||||
if intersection:
|
||||
intersections.append(intersection)
|
||||
return intersections
|
||||
|
||||
|
||||
|
||||
def get_intersections(order, compensations):
|
||||
def get_intersections(order_intervals, compensations):
|
||||
intersections = []
|
||||
for compensation in compensations:
|
||||
intersection = compensation.intersect(order)
|
||||
if intersection:
|
||||
intersections.append((len(intersection), intersection))
|
||||
intersection = compensation.intersect_set(order_intervals)
|
||||
length = 0
|
||||
for intersection_interval in intersection:
|
||||
length += len(intersection_interval)
|
||||
intersections.append((length, compensation))
|
||||
intersections.sort()
|
||||
return intersections
|
||||
|
||||
# Intervals should not overlap
|
||||
|
@ -153,24 +167,32 @@ def intersect(compensation, order_intervals):
|
|||
compensated.append(compensation.intersect(interval, unused_compensation, not_compensated))
|
||||
return (compensated, not_compensated, unused_compensation)
|
||||
|
||||
def apply_compensation(order, compensation):
|
||||
remaining_order = []
|
||||
remaining_compensation = []
|
||||
applied_compensation = compensation.intersect_set(order, remaining_compensation, remaining_order)
|
||||
return applied_compensation, remaining_order, remaining_compensation
|
||||
|
||||
# TODO can be optimized
|
||||
def update_intersections(not_compensated, compensations):
|
||||
intersections = []
|
||||
for (_,compensation) in compensations:
|
||||
intersections += get_intersections(compensation, not_compensated)
|
||||
return intersections
|
||||
compensation_intervals = []
|
||||
for __, compensation in compensations:
|
||||
compensation_intervals.append(compensation)
|
||||
return get_intersections(not_compensated, compensation_intervals)
|
||||
|
||||
|
||||
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
|
||||
remaining_interval = [order]
|
||||
ordered_intersections = get_intersections(remaining_interval, compensations)
|
||||
applied_compensations = []
|
||||
remaining_compensations = []
|
||||
while ordered_intersections and ordered_intersections[len(ordered_intersections)-1][0]>0:
|
||||
# Apply the first compensation:
|
||||
__, compensation = ordered_intersections.pop()
|
||||
(applied_compensation, remaining_interval, remaining_compensation) = apply_compensation(remaining_interval, compensation)
|
||||
remaining_compensations += remaining_compensation
|
||||
applied_compensations += applied_compensation
|
||||
ordered_intersections = update_intersections(remaining_interval, ordered_intersections)
|
||||
for __, compensation in ordered_intersections:
|
||||
remaining_compensations.append(compensation)
|
||||
return remaining_compensations, applied_compensations
|
||||
|
|
|
@ -323,46 +323,16 @@ 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, 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)
|
||||
return self.model.objects.exclude(id__in=original_ids).filter(
|
||||
service__in=qs.values_list('service_id', flat=True),
|
||||
account__in=qs.values_list('account_id', flat=True)
|
||||
)
|
||||
def filter_givers(self, ini, end):
|
||||
return self.filter(
|
||||
cancelled_on__isnull=False, billed_until__isnull=False,
|
||||
cancelled_on__lte=F('billed_until'), billed_until__gt=ini,
|
||||
registered_on__lt=end)
|
||||
|
||||
def filter_pricing_orders(self, ini, end):
|
||||
return self.filter(billed_until__isnull=False, billed_until__gt=ini,
|
||||
registered_on__lt=end)
|
||||
|
||||
def by_object(self, obj, **kwargs):
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
|
@ -386,7 +356,7 @@ class Order(models.Model):
|
|||
object_id = models.PositiveIntegerField(null=True)
|
||||
service = models.ForeignKey(Service, verbose_name=_("service"),
|
||||
related_name='orders')
|
||||
registered_on = models.DateField(_("registered on"), auto_now_add=True)
|
||||
registered_on = models.DateField(_("registered on"), auto_now_add=True) # TODO datetime field?
|
||||
cancelled_on = models.DateField(_("cancelled on"), null=True, blank=True)
|
||||
billed_on = models.DateField(_("billed on"), null=True, blank=True)
|
||||
billed_until = models.DateField(_("billed until"), null=True, blank=True)
|
||||
|
|
|
@ -8,8 +8,7 @@ from orchestra.apps.accounts.models import Account
|
|||
from orchestra.apps.users.models import User
|
||||
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||
|
||||
from ... import settings
|
||||
from ...helpers import cmp_billed_until_or_registered_on
|
||||
from ... import settings, helpers
|
||||
from ...models import Service, Order
|
||||
|
||||
|
||||
|
@ -175,44 +174,73 @@ class OrderTests(BaseTestCase):
|
|||
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))
|
||||
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
|
||||
|
||||
def test_compensation(self):
|
||||
now = timezone.now()
|
||||
order = Order(
|
||||
description='0',
|
||||
registered_on=now,
|
||||
billed_until=now+datetime.timedelta(days=200),
|
||||
billed_until=now+datetime.timedelta(days=220),
|
||||
cancelled_on=now+datetime.timedelta(days=100))
|
||||
order1 = Order(
|
||||
description='1',
|
||||
registered_on=now+datetime.timedelta(days=5),
|
||||
cancelled_on=now+datetime.timedelta(days=190),
|
||||
billed_until=now+datetime.timedelta(days=200))
|
||||
order2 = Order(
|
||||
description='2',
|
||||
registered_on=now+datetime.timedelta(days=6),
|
||||
cancelled_on=now+datetime.timedelta(days=200),
|
||||
billed_until=now+datetime.timedelta(days=200))
|
||||
order3 = Order(
|
||||
description='3',
|
||||
registered_on=now+datetime.timedelta(days=6),
|
||||
billed_until=now+datetime.timedelta(days=200))
|
||||
|
||||
tests = []
|
||||
order4 = Order(
|
||||
registered_on=now+datetime.timedelta(days=6))
|
||||
description='4',
|
||||
registered_on=now+datetime.timedelta(days=6),
|
||||
billed_until=now+datetime.timedelta(days=102))
|
||||
order4.new_billed_until = now+datetime.timedelta(days=200)
|
||||
tests.append([
|
||||
[now+datetime.timedelta(days=102), now+datetime.timedelta(days=220), order],
|
||||
])
|
||||
order5 = Order(
|
||||
registered_on=now+datetime.timedelta(days=7))
|
||||
description='5',
|
||||
registered_on=now+datetime.timedelta(days=7),
|
||||
billed_until=now+datetime.timedelta(days=102))
|
||||
order5.new_billed_until = now+datetime.timedelta(days=195)
|
||||
tests.append([
|
||||
[now+datetime.timedelta(days=190), now+datetime.timedelta(days=200), order1]
|
||||
])
|
||||
order6 = Order(
|
||||
description='6',
|
||||
registered_on=now+datetime.timedelta(days=8))
|
||||
order6.new_billed_until = now+datetime.timedelta(days=200)
|
||||
tests.append([
|
||||
[now+datetime.timedelta(days=100), now+datetime.timedelta(days=102), order],
|
||||
])
|
||||
porders = [order3, order, order1, order2, order4, order5, order6]
|
||||
porders = sorted(porders, cmp=cmp_billed_until_or_registered_on)
|
||||
porders = sorted(porders, cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
service = self.create_service()
|
||||
compensations = []
|
||||
from ... import helpers
|
||||
receivers = []
|
||||
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)
|
||||
|
||||
elif hasattr(order, 'new_billed_until') and (not order.billed_until or order.billed_until < order.new_billed_until):
|
||||
receivers.append(order)
|
||||
for order, test in zip(receivers, tests):
|
||||
ini = order.billed_until or order.registered_on
|
||||
end = order.cancelled_on or now+datetime.timedelta(days=20000)
|
||||
order_interval = helpers.Interval(ini, end)
|
||||
(compensations, used_compensations) = helpers.compensate(order_interval, compensations)
|
||||
for compensation, test_line in zip(used_compensations, test):
|
||||
self.assertEqual(test_line[0], compensation.ini)
|
||||
self.assertEqual(test_line[1], compensation.end)
|
||||
self.assertEqual(test_line[2], compensation.order)
|
||||
|
||||
# def test_ftp_account_1_year_fiexed(self):
|
||||
# service = self.create_service()
|
||||
|
|
Loading…
Reference in a new issue