diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py index 218f0e6f..5e93479e 100644 --- a/orchestra/apps/orders/billing.py +++ b/orchestra/apps/orders/billing.py @@ -35,7 +35,7 @@ class BillsBackend(object): # Create bill line billine = bill.lines.create( rate=service.nominal_price, - quantity=line.size, + quantity=line.metric*line.size, subtotal=line.subtotal, tax=service.tax, description=self.get_line_description(line), @@ -54,11 +54,14 @@ class BillsBackend(object): service = line.order.service if service.is_fee: return self.format_period(line.ini, line.end) - else: - description = line.order.description - if service.billing_period != service.NEVER: - description += " %s" % self.format_period(line.ini, line.end) - return description + description = line.order.description + if service.billing_period != service.NEVER: + description += " %s" % self.format_period(line.ini, line.end) + if service.metric and service.billing_period != service.NEVER and service.pricing_period == service.NEVER: + metric = format(line.metric, '.2f').rstrip('0').rstrip('.') + size = format(line.size, '.2f').rstrip('0').rstrip('.') + description += " (%s*%s)" % (metric, size) + return description def create_sublines(self, line, discounts): for discount in discounts: diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 15f49b81..73f2ad04 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -141,14 +141,32 @@ class Order(models.Model): self.save() logger.info("CANCELLED order id: {id}".format(id=self.id)) - def get_metric(self, ini, end): - return MetricStorage.get(self, ini, end) + def get_metric(self, ini, end, changes=False): + if changes: + result = [] + prev = None + for metric in self.metrics.filter(created_on__lt=end).order_by('created_on'): + created = metric.created_on.date() + if created > ini: + cini = prev.created_on.date() + if not result: + cini = ini + result.append((cini, created, prev.value)) + prev = metric + if created < end: + result.append((created, end, metric.value)) + return result + try: + metrics = self.metrics.filter(updated_on__lt=end, updated_on__gte=ini) + return metrics.latest('updated_on').value + except MetricStorage.DoesNotExist: + return decimal.Decimal(0) class MetricStorage(models.Model): 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) + created_on = models.DateTimeField(_("created"), auto_now_add=True) updated_on = models.DateTimeField(_("updated")) class Meta: @@ -170,25 +188,19 @@ class MetricStorage(models.Model): else: metric.updated_on = now metric.save() - - @classmethod - def get(cls, order, ini, end): - try: - return order.metrics.filter(updated_on__lt=end, updated_on__gte=ini).latest('updated_on').value - except cls.DoesNotExist: - return decimal.Decimal(0) _excluded_models = (MetricStorage, LogEntry, Order, ContentType, MigrationRecorder.Migration) + @receiver(post_delete, dispatch_uid="orders.cancel_orders") def cancel_orders(sender, **kwargs): if sender not in _excluded_models: instance = kwargs['instance'] - if sender in services: + if hasattr(instance, 'account'): for order in Order.objects.by_object(instance).active(): order.cancel() - elif not hasattr(instance, 'account'): + else: related = helpers.get_related_objects(instance) if related and related != instance: Order.update_orders(related) @@ -198,12 +210,13 @@ def cancel_orders(sender, **kwargs): def update_orders(sender, **kwargs): if sender not in _excluded_models: instance = kwargs['instance'] - if sender in services: + if hasattr(instance, 'account'): Order.update_orders(instance) - elif not hasattr(instance, 'account'): + else: related = helpers.get_related_objects(instance) if related and related != instance: Order.update_orders(related) + accounts.register(Order) diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py index fc6bf5bf..26c17cfe 100644 --- a/orchestra/apps/orders/tests/functional_tests/tests.py +++ b/orchestra/apps/orders/tests/functional_tests/tests.py @@ -311,7 +311,9 @@ class TrafficBillingTest(BaseBillingTest): 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) + metric = order.metrics.latest() + metric.updated_on -= delta + metric.save() bills = service.orders.bill(proforma=True) self.assertEqual(900, bills[0].get_total()) @@ -320,6 +322,11 @@ class TrafficBillingTest(BaseBillingTest): resource = self.create_traffic_resource() account1 = self.create_account() account2 = self.create_account() + # TODO + + def test_traffic_prepay(self): + pass + # TODO class MailboxBillingTest(BaseBillingTest): @@ -361,8 +368,7 @@ class MailboxBillingTest(BaseBillingTest): 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=1, price=10) return service def create_disk_resource(self): @@ -398,18 +404,56 @@ class MailboxBillingTest(BaseBillingTest): self.allocate_disk(mailbox, 10) bill = service.orders.bill()[0] self.assertEqual(0, bill.get_total()) - bill = disk_service.orders.bill()[0] - for line in bill.lines.all(): - for discount in line.sublines.all(): - print discount.__dict__ - self.assertEqual(80, bill.get_total()) + bp = timezone.now().date() + relativedelta.relativedelta(years=1) + bill = disk_service.orders.bill(billing_point=bp, fixed_point=True)[0] + self.assertEqual(90, bill.get_total()) mailbox = self.create_mailbox(account=account) mailbox = self.create_mailbox(account=account) mailbox = self.create_mailbox(account=account) mailbox = self.create_mailbox(account=account) mailbox = self.create_mailbox(account=account) - bill = service.orders.bill()[0] - print disk_service.orders.bill()[0].get_total() + mailbox = self.create_mailbox(account=account) + bill = service.orders.bill(billing_point=bp, fixed_point=True)[0] + self.assertEqual(120, bill.get_total()) + + def test_mailbox_size_with_changes(self): + service = self.create_mailbox_disk_service() + self.create_disk_resource() + account = self.create_account() + mailbox = self.create_mailbox(account=account) + now = timezone.now() + bp = now.date() + relativedelta.relativedelta(years=1) + + self.allocate_disk(mailbox, 10) + bill = service.orders.bill(billing_point=bp, fixed_point=True, proforma=True, new_open=True)[0] + self.assertEqual(9*10, bill.get_total()) + + self.allocate_disk(mailbox, 20) + created_on = now+relativedelta.relativedelta(months=6) + order = service.orders.get() + metric = order.metrics.latest('id') + metric.created_on = created_on + metric.save() + bill = service.orders.bill(billing_point=bp, fixed_point=True, proforma=True, new_open=True)[0] + self.assertEqual(9*10*0.5 + 19*10*0.5, bill.get_total()) + + self.allocate_disk(mailbox, 30) + created_on = now+relativedelta.relativedelta(months=9) + order = service.orders.get() + metric = order.metrics.latest('id') + metric.created_on = created_on + metric.save() + bill = service.orders.bill(billing_point=bp, fixed_point=True, proforma=True, new_open=True)[0] + self.assertEqual(9*10*0.5 + 19*10*0.25 + 29*10*0.25, bill.get_total()) + + self.allocate_disk(mailbox, 10) + created_on = now+relativedelta.relativedelta(years=1) + order = service.orders.get() + metric = order.metrics.latest('id') + metric.created_on = created_on + metric.save() + bill = service.orders.bill(billing_point=bp, fixed_point=True, proforma=True, new_open=True)[0] + self.assertEqual(9*10*0.5 + 19*10*0.25 + 29*10*0.25, bill.get_total()) class JobBillingTest(BaseBillingTest): @@ -418,10 +462,10 @@ class JobBillingTest(BaseBillingTest): description="Random job", content_type=ContentType.objects.get_for_model(Miscellaneous), match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'job'", - billing_period=Service.MONTHLY, - billing_point=Service.FIXED_DATE, + billing_period=Service.NEVER, + billing_point=Service.ON_REGISTER, is_fee=False, - metric='mailbox.resources.disk.allocated', + metric='miscellaneous.amount', pricing_period=Service.BILLING_PERIOD, rate_algorithm=Service.STEP_PRICE, on_cancel=Service.NOTHING, @@ -434,15 +478,19 @@ class JobBillingTest(BaseBillingTest): service.rates.create(plan=plan, quantity=11, price=10) return service - def create_job(self, account=None): + def create_job(self, amount, account=None): if not account: account = self.create_account() job_name = '%s.es' % random_ascii(10) - job_service, __ = MiscService.objects.get_or_create(name='job', description='Random job') - return Miscellaneous.objects.create(service=job_service, description=job_name, account=account) + job_service, __ = MiscService.objects.get_or_create(name='job', description='Random job', has_amount=True) + return Miscellaneous.objects.create(service=job_service, description=job_name, account=account, amount=amount) def test_job(self): - pass + service = self.create_job_service() + account = self.create_account() + job = self.create_job(10, account=account) + print service.orders.all() + print service.orders.bill()[0].get_total() class PlanBillingTest(BaseBillingTest): diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 47a1a3d0..48e525c1 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -150,14 +150,29 @@ class ServiceHandler(plugins.Plugin): 'total': price, })) - def generate_line(self, order, price, size, ini, end, discounts=[]): - subtotal = self.nominal_price * size + def generate_line(self, order, price, *dates, **kwargs): + if len(dates) == 2: + ini, end = dates + elif len(dates) == 1: + ini, end = dates[0], dates[0] + else: + raise AttributeError + metric = kwargs.pop('metric', 1) + discounts = kwargs.pop('discounts', ()) + computed = kwargs.pop('computed', False) + if kwargs: + raise AttributeError + size = self.get_price_size(ini, end) + if not computed: + price = price * size + subtotal = self.nominal_price * size * metric line = AttributeDict(**{ 'order': order, 'subtotal': subtotal, - 'size': size, 'ini': ini, 'end': end, + 'size': size, + 'metric': metric, 'discounts': [], }) discounted = 0 @@ -249,7 +264,7 @@ class ServiceHandler(plugins.Plugin): csize += self.get_price_size(intersect.ini, intersect.end) price = self.get_price(account, metric, position=position, rates=rates) price = price * size - cprice = price * (size-csize) + cprice = price * csize if order in priced: priced[order][0] += price priced[order][1] += cprice @@ -269,7 +284,7 @@ class ServiceHandler(plugins.Plugin): size = self.get_price_size(order.new_billed_until, new_end) price += price*size order.new_billed_until = new_end - line = self.generate_line(order, price, size, ini, end, discounts=discounts) + line = self.generate_line(order, price, ini, new_end or end, discounts=discounts, computed=True) lines.append(line) if commit: order.billed_until = order.new_billed_until @@ -302,8 +317,7 @@ class ServiceHandler(plugins.Plugin): order.new_billed_until = new_end end = new_end size = self.get_price_size(ini, end) - price = price * size - line = self.generate_line(order, price, size, ini, end, discounts=discounts) + line = self.generate_line(order, price, ini, end, discounts=discounts) lines.append(line) if commit: order.billed_until = order.new_billed_until @@ -322,6 +336,7 @@ class ServiceHandler(plugins.Plugin): bp = None ini = datetime.date.max end = datetime.date.min + # TODO compensation with one time billing? for order in orders: cini = order.registered_on if order.billed_until: @@ -370,8 +385,7 @@ class ServiceHandler(plugins.Plugin): if new_end: order.new_billed_until = new_end end = new_end - size = self.get_price_size(ini, end) - line = self.generate_line(order, price*size, size, ini, end, discounts=discounts) + line = self.generate_line(order, price, ini, end, discounts=discounts) lines.append(line) if commit: order.billed_until = order.new_billed_until @@ -379,40 +393,36 @@ class ServiceHandler(plugins.Plugin): return lines def bill_with_metric(self, orders, account, **options): - # TODO filter out orders with cancelled_on < billed_until ? lines = [] commit = options.get('commit', True) bp = None for order in orders: + if order.billed_until and order.cancelled_on >= order.billed_until: + continue bp = self.get_billing_point(order, bp=bp, **options) ini = order.billed_until or order.registered_on if bp <= ini: + # TODO except one time service continue order.new_billed_until = bp - # weighted metric; bill line per pricing period - prev = None - lines_info = [] if self.billing_period != self.NEVER: if self.get_pricing_period() == self.NEVER: # Changes - for ini, end, metric in order.get_metric(ini, end, changes=True) - size = self.get_price_size(ini, end) + for ini, end, metric in order.get_metric(ini, bp, changes=True): price = self.get_price(order, metric) - price = price * size - # TODO metric and size in invoice (period and quantity) - lines.append(self.generate_line(order, price, metric, ini, end)) + lines.append(self.generate_line(order, price, ini, end, metric=metric)) else: # pricing_slots for ini, end in self.get_pricing_slots(ini, bp): metric = order.get_metric(ini, end) price = self.get_price(order, metric) - lines.append(self.generate_line(order, price, metric, ini, end)) + lines.append(self.generate_line(order, price, ini, end, metric=metric)) else: if self.get_pricing_period() == self.NEVER: # get metric metric = order.get_metric(ini, end) price = self.get_price(order, metric) - lines.append(self.generate_line(order, price, metric, ini, end)) + lines.append(self.generate_line(order, price, ini, bp, metric=metric)) else: raise NotImplementedError if commit: