diff --git a/orchestra/apps/bills/actions.py b/orchestra/apps/bills/actions.py
index 414ba6c8..5161f2c3 100644
--- a/orchestra/apps/bills/actions.py
+++ b/orchestra/apps/bills/actions.py
@@ -6,7 +6,7 @@ from orchestra.utils.system import run
def generate_bill(modeladmin, request, queryset):
bill = queryset.get()
bill.close()
-# return HttpResponse(bill.html)
+ return HttpResponse(bill.html)
pdf = run('xvfb-run -a -s "-screen 0 640x4800x16" '
'wkhtmltopdf --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
stdin=bill.html.encode('utf-8'), display=False)
diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py
index dcba6b74..643ed8af 100644
--- a/orchestra/apps/bills/admin.py
+++ b/orchestra/apps/bills/admin.py
@@ -1,12 +1,14 @@
from django import forms
from django.contrib import admin
from django.core.urlresolvers import reverse
+from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
+from . import settings
from .actions import generate_bill
from .filters import BillTypeListFilter
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
@@ -15,9 +17,16 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
class BillLineInline(admin.TabularInline):
model = BillLine
- fields = (
- 'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
- )
+ fields = ('description', 'rate', 'amount', 'tax', 'total', 'subtotal')
+ readonly_fields = ('subtotal',)
+
+ def subtotal(self, line):
+ if line.total:
+ subtotal = 0
+ for subline in line.sublines.all():
+ subtotal += subline.total
+ return line.total - subtotal
+ return ''
def get_readonly_fields(self, request, obj=None):
if obj and obj.status != Bill.OPEN:
@@ -37,21 +46,20 @@ class BillLineInline(admin.TabularInline):
class BudgetLineInline(admin.TabularInline):
model = Budget
- fields = (
- 'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
- )
+ fields = ('description', 'rate', 'amount', 'tax', 'total')
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = (
- 'number', 'status', 'type_link', 'account_link', 'created_on_display'
+ 'number', 'status', 'type_link', 'account_link', 'created_on_display',
+ 'num_lines', 'display_total'
)
list_filter = (BillTypeListFilter, 'status',)
add_fields = ('account', 'type', 'status', 'due_on', 'comments')
fieldsets = (
(None, {
- 'fields': ('number', 'account_link', 'type', 'status', 'due_on',
- 'comments'),
+ 'fields': ('number', 'display_total', 'account_link', 'type',
+ 'status', 'due_on', 'comments'),
}),
(_("Raw"), {
'classes': ('collapse',),
@@ -60,11 +68,21 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
)
change_view_actions = [generate_bill]
change_readonly_fields = ('account_link', 'type', 'status')
- readonly_fields = ('number',)
+ readonly_fields = ('number', 'display_total')
inlines = [BillLineInline]
created_on_display = admin_date('created_on')
+ def num_lines(self, bill):
+ return bill.billlines__count
+ num_lines.admin_order_field = 'billlines__count'
+ num_lines.short_description = _("lines")
+
+ def display_total(self, bill):
+ return "%i &%s;" % (bill.get_total(), settings.BILLS_CURRENCY.lower())
+ display_total.allow_tags = True
+ display_total.short_description = _("total")
+
def type_link(self, bill):
bill_type = bill.type.lower()
url = reverse('admin:bills_%s_changelist' % bill_type)
@@ -93,7 +111,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
if db_field.name == 'html':
kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20})
return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs)
-
+
+ def queryset(self, request):
+ qs = super(BillAdmin, self).queryset(request)
+ qs = qs.annotate(models.Count('billlines'))
+ qs = qs.prefetch_related('billlines', 'billlines__sublines')
+ return qs
+
admin.site.register(Bill, BillAdmin)
admin.site.register(Invoice, BillAdmin)
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
index a3895d7c..5f550530 100644
--- a/orchestra/apps/bills/models.py
+++ b/orchestra/apps/bills/models.py
@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account
from orchestra.core import accounts
+from orchestra.utils.functional import cached
from . import settings
@@ -140,6 +141,25 @@ class Bill(models.Model):
if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
self.set_number()
super(Bill, self).save(*args, **kwargs)
+
+ @cached
+ def get_subtotals(self):
+ subtotals = {}
+ for line in self.lines.all():
+ subtotal, taxes = subtotals.get(line.tax, (0, 0))
+ subtotal += line.total
+ for subline in line.sublines.all():
+ subtotal += subline.total
+ subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal)
+ return subtotals
+
+ @cached
+ def get_total(self):
+ total = 0
+ for tax, subtotal in self.get_subtotals().iteritems():
+ subtotal, taxes = subtotal
+ total += subtotal + taxes
+ return total
class Invoice(Bill):
@@ -176,11 +196,11 @@ class BaseBillLine(models.Model):
bill = models.ForeignKey(Bill, verbose_name=_("bill"),
related_name='%(class)ss')
description = models.CharField(_("description"), max_length=256)
- initial_date = models.DateTimeField()
- final_date = models.DateTimeField()
- price = models.DecimalField(max_digits=12, decimal_places=2)
+ rate = models.DecimalField(_("rate"), blank=True, null=True,
+ max_digits=12, decimal_places=2)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
- tax = models.DecimalField(_("tax"), max_digits=12, decimal_places=2)
+ total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
+ tax = models.PositiveIntegerField(_("tax"))
class Meta:
abstract = True
@@ -188,7 +208,7 @@ class BaseBillLine(models.Model):
def __unicode__(self):
return "#%i" % self.number
- @property
+ @cached_property
def number(self):
lines = type(self).objects.filter(bill=self.bill_id)
return lines.filter(id__lte=self.id).order_by('id').count()
@@ -207,7 +227,7 @@ class BillLine(BaseBillLine):
related_name='amendment_lines', null=True, blank=True)
-class SubBillLine(models.Model):
+class BillSubline(models.Model):
""" Subline used for describing an item discount """
bill_line = models.ForeignKey(BillLine, verbose_name=_("bill line"),
related_name='sublines')
diff --git a/orchestra/apps/bills/templates/bills/microspective.html b/orchestra/apps/bills/templates/bills/microspective.html
index f851d8aa..981cd10f 100644
--- a/orchestra/apps/bills/templates/bills/microspective.html
+++ b/orchestra/apps/bills/templates/bills/microspective.html
@@ -50,7 +50,7 @@
TOTAL
-
{{ bill.total }} &{{ currency.lower }};
+ {{ bill.get_total }} &{{ currency.lower }};
{{ bill.get_type_display.upper }} DATE
@@ -79,20 +79,22 @@
{{ line.description }}
{{ line.amount|default:" " }}
{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}
- {{ line.price }} &{{ currency.lower }};
+ {{ line.total }} &{{ currency.lower }};
{% endfor %}
- subtotal
- {{ bill.subtotal }} &{{ currency.lower }};
-
- tax
- {{ bill.taxes }} &{{ currency.lower }};
-
+ {% for tax, subtotal in bill.get_subtotals.iteritems %}
+ subtotal {{ tax }}% VAT
+ {{ subtotal | first }} &{{ currency.lower }};
+
+ taxes {{ tax }}% VAT
+ {{ subtotal | last }} &{{ currency.lower }};
+
+ {% endfor %}
total
- {{ bill.total }} &{{ currency.lower }};
+ {{ bill.get_total }} &{{ currency.lower }};
{% endblock %}
diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py
index 903622eb..96eba692 100644
--- a/orchestra/apps/contacts/admin.py
+++ b/orchestra/apps/contacts/admin.py
@@ -97,7 +97,7 @@ class ContactInline(InvoiceContactInline):
def has_invoice(account):
try:
- account.invoicecontact.get()
+ account.invoicecontact
except InvoiceContact.DoesNotExist:
return False
return True
diff --git a/orchestra/apps/orders/actions.py b/orchestra/apps/orders/actions.py
index b4310d56..876f3b8a 100644
--- a/orchestra/apps/orders/actions.py
+++ b/orchestra/apps/orders/actions.py
@@ -70,7 +70,7 @@ class BillSelectedOrders(object):
msg = _("Selected orders do not have pending billing")
self.modeladmin.message_user(request, msg, messages.WARNING)
else:
- ids = ','.join([bill.id for bill in bills])
+ ids = ','.join([str(bill.id) for bill in bills])
url = reverse('admin:bills_bill_changelist')
context = {
'url': url + '?id=%s' % ids,
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index 46e2487e..8923da9d 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -78,9 +78,9 @@ class ServiceAdmin(admin.ModelAdmin):
class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
list_display = (
'id', 'service', 'account_link', 'content_object_link',
- 'display_registered_on', 'display_cancelled_on'
+ 'display_registered_on', 'display_billed_until', 'display_cancelled_on'
)
- list_display_link = ('id', 'service')
+ list_display_links = ('id', 'service')
list_filter = (ActiveOrderListFilter, 'service',)
actions = (BillSelectedOrders(),)
date_hierarchy = 'registered_on'
@@ -90,6 +90,7 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on')
+ display_billed_until = admin_date('billed_until')
display_cancelled_on = admin_date('cancelled_on')
def get_queryset(self, request):
diff --git a/orchestra/apps/orders/backends.py b/orchestra/apps/orders/backends.py
new file mode 100644
index 00000000..93bd7c9d
--- /dev/null
+++ b/orchestra/apps/orders/backends.py
@@ -0,0 +1,42 @@
+import datetime
+
+from orchestra.apps.bills.models import Invoice, Fee, BillLine, BillSubline
+
+
+class BillsBackend(object):
+ def create_bills(self, account, lines):
+ invoice = None
+ fees = []
+ for order, nominal_price, size, ini, end, discounts in lines:
+ service = order.service
+ if service.is_fee:
+ fee = Fee.objects.get_or_create(account=account, status=Fee.OPEN)
+ line = fee.lines.create(rate=service.nominal_price, amount=size,
+ total=nominal_price, tax=0)
+ self.create_sublines(line, discounts)
+ fees.append(fee)
+ else:
+ if invoice is None:
+ invoice, __ = Invoice.objects.get_or_create(account=account,
+ status=Invoice.OPEN)
+ description = order.description
+ if service.billing_period != service.NEVER:
+ description += " {ini} to {end}".format(
+ ini=ini.strftime("%b, %Y"),
+ end=(end-datetime.timedelta(seconds=1)).strftime("%b, %Y"))
+ line = invoice.lines.create(
+ description=description,
+ rate=service.nominal_price,
+ amount=size,
+ total=nominal_price,
+ tax=service.tax,
+ )
+ self.create_sublines(line, discounts)
+ return [invoice] + fees
+
+ def create_sublines(self, line, discounts):
+ for name, value in discounts:
+ line.sublines.create(
+ description=_("Discount per %s") % name,
+ total=value,
+ )
diff --git a/orchestra/apps/orders/handlers.py b/orchestra/apps/orders/handlers.py
index 096c4604..2c6fe2c2 100644
--- a/orchestra/apps/orders/handlers.py
+++ b/orchestra/apps/orders/handlers.py
@@ -1,6 +1,7 @@
import calendar
+import datetime
-from dateutil.relativedelta import relativedelta
+from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils import timezone
@@ -8,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
+from . import settings
from .helpers import get_register_or_cancel_events, get_register_or_renew_events
@@ -52,55 +54,59 @@ class ServiceHandler(plugins.Plugin):
return eval(self.metric, safe_locals)
def get_billing_point(self, order, bp=None, **options):
- not_cachable = self.billing_point is self.FIXED_DATE and options.get('fixed_point')
+ not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point')
if not_cachable or bp is None:
bp = options.get('billing_point', timezone.now().date())
if not options.get('fixed_point'):
- if self.billing_period is self.MONTHLY:
+ msg = ("Support for '%s' period and '%s' point is not implemented"
+ % (self.get_billing_period_display(), self.get_billing_point_display()))
+ if self.billing_period == self.MONTHLY:
date = bp
- if self.payment_style is self.PREPAY:
- date += relativedelta(months=1)
- if self.billing_point is self.ON_REGISTER:
+ if self.payment_style == self.PREPAY:
+ date += relativedelta.relativedelta(months=1)
+ if self.billing_point == self.ON_REGISTER:
day = order.registered_on.day
- elif self.billing_point is self.FIXED_DATE:
+ elif self.billing_point == self.FIXED_DATE:
day = 1
+ else:
+ raise NotImplementedError(msg)
bp = datetime.datetime(year=date.year, month=date.month,
day=day, tzinfo=timezone.get_current_timezone())
- elif self.billing_period is self.ANUAL:
- if self.billing_point is self.ON_REGISTER:
+ elif self.billing_period == self.ANUAL:
+ if self.billing_point == self.ON_REGISTER:
month = order.registered_on.month
day = order.registered_on.day
- elif self.billing_point is self.FIXED_DATE:
+ elif self.billing_point == self.FIXED_DATE:
month = settings.ORDERS_SERVICE_ANUAL_BILLING_MONTH
day = 1
+ else:
+ raise NotImplementedError(msg)
year = bp.year
- if self.payment_style is self.POSTPAY:
- year = bo.year - relativedelta(years=1)
+ if self.payment_style == self.POSTPAY:
+ year = bo.year - relativedelta.relativedelta(years=1)
if bp.month >= month:
year = bp.year + 1
bp = datetime.datetime(year=year, month=month, day=day,
tzinfo=timezone.get_current_timezone())
- elif self.billing_period is self.NEVER:
+ elif self.billing_period == self.NEVER:
bp = order.registered_on
else:
- raise NotImplementedError(
- "Support for '%s' billing period and '%s' billing point is not implemented"
- % (self.display_billing_period(), self.display_billing_point())
- )
- if self.on_cancel is not self.NOTHING and order.cancelled_on < bp:
+ raise NotImplementedError(msg)
+ if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
return order.cancelled_on
return bp
def get_pricing_size(self, ini, end):
rdelta = relativedelta.relativedelta(end, ini)
- if self.get_pricing_period() is self.MONTHLY:
+ if self.get_pricing_period() == self.MONTHLY:
size = rdelta.months
- days = calendar.monthrange(bp.year, bp.month)[1]
- size += float(bp.day)/days
- elif self.get_pricint_period() is self.ANUAL:
+ days = calendar.monthrange(end.year, end.month)[1]
+ size += float(rdelta.days)/days
+ elif self.get_pricing_period() == self.ANUAL:
size = rdelta.years
- size += float(rdelta.days)/365
- elif self.get_pricing_period() is self.NEVER:
+ days = 366 if calendar.isleap(end.year) else 365
+ size += float((end-ini).days)/days
+ elif self.get_pricing_period() == self.NEVER:
size = 1
else:
raise NotImplementedError
@@ -108,11 +114,11 @@ class ServiceHandler(plugins.Plugin):
def get_pricing_slots(self, ini, end):
period = self.get_pricing_period()
- if period is self.MONTHLY:
- rdelta = relativedelta(months=1)
- elif period is self.ANUAL:
- rdelta = relativedelta(years=1)
- elif period is self.NEVER:
+ if period == self.MONTHLY:
+ rdelta = relativedelta.relativedelta(months=1)
+ elif period == self.ANUAL:
+ rdelta = relativedelta.relativedelta(years=1)
+ elif period == self.NEVER:
yield ini, end
raise StopIteration
else:
@@ -125,51 +131,107 @@ class ServiceHandler(plugins.Plugin):
yield ini, next
ini = next
- def create_line(self, order, price, size):
+ def get_price_with_orders(self, order, size, ini, end):
+ porders = self.orders.filter(account=order.account).filter(
+ Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini)
+ ).filter(registered_on__lt=end)
+ price = 0
+ if self.orders_effect == self.REGISTER_OR_RENEW:
+ events = get_register_or_renew_events(porders, ini, end)
+ elif self.orders_effect == self.CONCURRENT:
+ events = get_register_or_cancel_events(porders, ini, end)
+ else:
+ raise NotImplementedError
+ for metric, ratio in events:
+ price += self.get_rate(order, metric) * size * ratio
+ return price
+
+ def get_price_with_metric(self, order, size, ini, end):
+ metric = order.get_metric(ini, end)
+ price = self.get_rate(order, metric) * size
+ return price
+
+ def create_line(self, order, price, size, ini, end):
nominal_price = self.nominal_price * size
+ discounts = []
if nominal_price > price:
- discount = nominal_price-price
+ discounts.append(('volume', nominal_price-price))
+ # TODO Uncomment when prices are done
+# elif nominal_price < price:
+# raise ValueError("Something is wrong!")
+ return (order, nominal_price, size, ini, end, discounts)
def create_bill_lines(self, orders, **options):
- # Perform compensations on cancelled services
- # TODO WTF to do with day 1 of each month.
- if self.on_cancel in (Order.COMPENSATE, Order.REFOUND):
+ # 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)
+ # compensate(orders, **options)
# TODO create discount per compensation
bp = None
lines = []
for order in orders:
bp = self.get_billing_point(order, bp=bp, **options)
ini = order.billed_until or order.registered_on
- if bp < ini:
+ if bp <= ini:
continue
if not self.metric:
# Number of orders metric; bill line per order
- porders = service.orders.filter(account=order.account).filter(
- Q(is_active=True) | Q(cancelled_on__gt=order.billed_until)
- ).filter(registered_on__lt=bp)
- price = 0
size = self.get_pricing_size(ini, bp)
- if self.orders_effect is self.REGISTER_OR_RENEW:
- events = get_register_or_renew_events(porders, ini, bp)
- elif self.orders_effect is self.CONCURRENT:
- events = get_register_or_cancel_events(porders, ini, bp)
- else:
- raise NotImplementedError
- for metric, ratio in events:
- price += self.get_rate(metric, account) * size * ratio
- lines += self.create_line(order, price, size)
+ price = self.get_price_with_orders(order, size, ini, bp)
+ lines.append(self.create_line(order, price, size, ini, bp))
else:
# weighted metric; bill line per pricing period
for ini, end in self.get_pricing_slots(ini, bp):
- metric = order.get_metric(ini, end)
size = self.get_pricing_size(ini, end)
- price = self.get_rate(metric, account) * size
- lines += self.create_line(order, price, size)
+ price = self.get_price_with_metric(order, size, ini, end)
+ lines.append(self.create_line(order, price, size, ini, end))
+ order.billed_until = bp
+ order.save() # TODO if commit
return lines
def compensate(self, orders):
- # num orders and weights
- # Discounts
- pass
+ # 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
diff --git a/orchestra/apps/orders/helpers.py b/orchestra/apps/orders/helpers.py
index 6fa6a287..91714b47 100644
--- a/orchestra/apps/orders/helpers.py
+++ b/orchestra/apps/orders/helpers.py
@@ -37,7 +37,7 @@ def get_related_objects(origin, max_depth=2):
queue.append(new_models)
def get_register_or_cancel_events(porders, ini, end):
- assert ini > end, "ini > end"
+ assert ini <= end, "ini > end"
CANCEL = 'cancel'
REGISTER = 'register'
changes = {}
@@ -50,21 +50,22 @@ def get_register_or_cancel_events(porders, ini, end):
if cancel > ini and cancel < end:
changes.setdefault(cancel, [])
changes[cancel].append(CANCEL)
- if order.registered_on < ini:
+ if order.registered_on <= ini:
counter += 1
elif order.registered_on < end:
changes.setdefault(order.registered_on, [])
changes[order.registered_on].append(REGISTER)
pointer = ini
total = float((end-ini).days)
- for date in changes.keys().sort():
+ for date in sorted(changes.keys()):
+ yield counter, (date-pointer).days/total
for change in changes[date]:
if change is CANCEL:
counter -= 1
else:
counter += 1
- yield counter, (date-pointer).days/total
pointer = date
+ yield counter, (end-pointer).days/total
def get_register_or_renew_events(handler, porders, ini, end):
@@ -72,7 +73,7 @@ def get_register_or_renew_events(handler, porders, ini, end):
for sini, send in handler.get_pricing_slots(ini, end):
counter = 0
for order in porders:
- if order.registered_on > sini and order.registered_on < send:
+ if order.registered_on >= sini and order.registered_on < send:
counter += 1
elif order.billed_until > send or order.cancelled_on > send:
counter += 1
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index 584b7bbe..89de5229 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -84,7 +84,7 @@ class Service(models.Model):
metric = models.CharField(_("metric"), max_length=256, blank=True,
help_text=_("Metric used to compute the pricing rate. "
"Number of orders is used when left blank."))
- tax = models.IntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES,
+ tax = models.PositiveIntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES,
default=settings.ORDERS_SERVICE_DEFAUL_TAX)
pricing_period = models.CharField(_("pricing period"), max_length=16,
help_text=_("Period used for calculating the metric used on the "
@@ -163,6 +163,11 @@ class Service(models.Model):
),
default=NEVER)
+ @property
+ def nominal_price(self):
+ # FIXME delete and make it a model field
+ return 10
+
def __unicode__(self):
return self.description
@@ -215,24 +220,29 @@ class Service(models.Model):
msg = "{0} {1}: {2}".format(attr, name, message)
raise ValidationError(msg)
- def get_nominal_price(self, order):
- """ returns the price of an item """
-
+ def get_pricing_period(self):
+ if self.pricing_period == self.BILLING_PERIOD:
+ return self.billing_period
+ return self.pricing_period
- def get_price(self, order, amount='TODO'):
- pass
+ def get_rate(self, order, metric):
+ # TODO implement
+ return 12
class OrderQuerySet(models.QuerySet):
group_by = queryset.group_by
def bill(self, **options):
- for account, services in self.group_by('account_id', 'service_id'):
+ bills = []
+ bill_backend = Order.get_bill_backend()
+ for account, services in self.group_by('account', 'service'):
bill_lines = []
for service, orders in services:
- lines = helpers.create_bill_lines(service, orders, **options)
+ lines = service.handler.create_bill_lines(orders, **options)
bill_lines.extend(lines)
- helpers.create_bills(account, bill_lines)
+ bills += bill_backend.create_bills(account, bill_lines)
+ return bills
def get_related(self):
pass
@@ -259,10 +269,10 @@ class Order(models.Model):
object_id = models.PositiveIntegerField(null=True)
service = models.ForeignKey(Service, verbose_name=_("service"),
related_name='orders')
- registered_on = models.DateTimeField(_("registered on"), auto_now_add=True)
- cancelled_on = models.DateTimeField(_("cancelled on"), null=True, blank=True)
- billed_on = models.DateTimeField(_("billed on"), null=True, blank=True)
- billed_until = models.DateTimeField(_("billed until"), null=True, blank=True)
+ registered_on = models.DateField(_("registered on"), auto_now_add=True)
+ 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)
ignore = models.BooleanField(_("ignore"), default=False)
description = models.TextField(_("description"), blank=True)
@@ -302,16 +312,26 @@ class Order(models.Model):
elif orders:
orders.get().cancel()
+ @classmethod
+ def get_bill_backend(cls):
+ # TODO
+ from .backends import BillsBackend
+ return BillsBackend()
+
def cancel(self):
self.cancelled_on = timezone.now()
self.save()
+
+ def get_metric(self, ini, end):
+ # TODO implement
+ return 10
class MetricStorage(models.Model):
order = models.ForeignKey(Order, verbose_name=_("order"))
value = models.BigIntegerField(_("value"))
- created_on = models.DateTimeField(_("created on"), auto_now_add=True)
- updated_on = models.DateTimeField(_("updated on"), auto_now=True)
+ created_on = models.DateField(_("created on"), auto_now_add=True)
+ updated_on = models.DateField(_("updated on"), auto_now=True)
class Meta:
get_latest_by = 'created_on'
diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin
index 8f487d7e..d39ad91c 100755
--- a/orchestra/bin/orchestra-admin
+++ b/orchestra/bin/orchestra-admin
@@ -131,7 +131,7 @@ function install_requirements () {
wkhtmltopdf \
xvfb"
- PIP="django==1.6.1 \
+ PIP="django==1.7 \
django-celery-email==1.0.4 \
django-fluent-dashboard==0.3.5 \
https://bitbucket.org/izi/django-admin-tools/get/a0abfffd76a0.zip \
diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py
index cbfd944f..dc821394 100644
--- a/orchestra/utils/humanize.py
+++ b/orchestra/utils/humanize.py
@@ -5,19 +5,27 @@ from django.utils.translation import ungettext, ugettext as _
def pluralize_year(n):
- return ungettext(_('{num:.1f} year ago'), _('{num:.1f} years ago'), n)
+ return ungettext(
+ _('{ahead}{num:.1f} year{ago}'),
+ _('{ahead}{num:.1f} years{ago}'), n)
def pluralize_month(n):
- return ungettext(_('{num:.1f} month ago'), _('{num:.1f} months ago'), n)
+ return ungettext(
+ _('{ahead}{num:.1f} month{ago}'),
+ _('{ahead}{num:.1f} months{ago}'), n)
def pluralize_week(n):
- return ungettext(_('{num:.1f} week ago'), _('{num:.1f} weeks ago'), n)
+ return ungettext(
+ _('{ahead}{num:.1f} week{ago}'),
+ _('{ahead}{num:.1f} weeks {ago}'), n)
def pluralize_day(n):
- return ungettext(_('{num:.1f} day ago'), _('{num:.1f} days ago'), n)
+ return ungettext(
+ _('{ahead}{num:.1f} day{ago}'),
+ _('{ahead}{num:.1f} days{ago}'), n)
OLDER_CHUNKS = (
@@ -48,29 +56,34 @@ def naturaldate(date, include_seconds=False):
minutes = delta.seconds / 60
seconds = delta.seconds
+ ago = ' ago'
+ ahead = ''
if days < 0:
- return _('just now')
+ ago = ''
+ ahead = 'in '
+ days = abs(days)
if days == 0:
if hours == 0:
if minutes > 0:
minutes += float(seconds)/60
return ungettext(
- _('{minutes:.1f} minute ago'),
- _('{minutes:.1f} minutes ago'), minutes
- ).format(minutes=minutes)
+ _('{ahead}{minutes:.1f} minute{ago}'),
+ _('{ahead}{minutes:.1f} minutes{ago}'), minutes
+ ).format(minutes=minutes, ago=ago, ahead=ahead)
else:
if include_seconds and seconds:
return ungettext(
- _('{seconds} second ago'),
- _('{seconds} seconds ago'), seconds
- ).format(seconds=seconds)
+ _('{ahead}{seconds} second{ago}'),
+ _('{ahead}{seconds} seconds{ago}'), seconds
+ ).format(seconds=seconds, ago=ago, ahead=ahead)
return _('just now')
else:
hours += float(minutes)/60
return ungettext(
- _('{hours:.1f} hour ago'), _('{hours:.1f} hours ago'), hours
- ).format(hours=hours)
+ _('{ahead}{hours:.1f} hour{ago}'),
+ _('{ahead}{hours:.1f} hours{ago}'), hours
+ ).format(hours=hours, ago=ago, ahead=ahead)
if delta_midnight.days == 0:
return _('yesterday at {time}').format(time=date.strftime('%H:%M'))
@@ -80,8 +93,9 @@ def naturaldate(date, include_seconds=False):
if days < 7.0:
count = days + float(hours)/24
fmt = pluralize_day(count)
- return fmt.format(num=count)
+ return fmt.format(num=count, ago=ago, ahead=ahead)
if days >= chunk:
count = (delta_midnight.days + 1) / chunk
+ count = abs(count)
fmt = pluralizefun(count)
- return fmt.format(num=count)
+ return fmt.format(num=count, ago=ago, ahead=ahead)