Finished billing prototype

This commit is contained in:
Marc 2014-09-03 13:56:02 +00:00
parent 9e8a76bc1b
commit f4c8ca06ca
13 changed files with 308 additions and 122 deletions

View File

@ -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)

View File

@ -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)
@ -94,6 +112,12 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
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)

View File

@ -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
@ -141,6 +142,25 @@ class Bill(models.Model):
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):
class Meta:
@ -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')

View File

@ -50,7 +50,7 @@
</div>
<div id="total">
<span class="title">TOTAL</span><br>
<psan class="value">{{ bill.total }} &{{ currency.lower }};</span>
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
</div>
<div id="bill-date">
<span class="title">{{ bill.get_type_display.upper }} DATE</span><br>
@ -79,20 +79,22 @@
<span class="value column-description">{{ line.description }}</span>
<span class="value column-quantity">{{ line.amount|default:"&nbsp;" }}</span>
<span class="value column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
<span class="value column-subtotal">{{ line.price }} &{{ currency.lower }};</span>
<span class="value column-subtotal">{{ line.total }} &{{ currency.lower }};</span>
<br>
{% endfor %}
</div>
<div id="totals">
<br>&nbsp;<br>
<span class="subtotal column-title">subtotal</span>
<span class="subtotal column-value">{{ bill.subtotal }} &{{ currency.lower }};</span>
<br>
<span class="tax column-title">tax</span>
<span class="tax column-value">{{ bill.taxes }} &{{ currency.lower }};</span>
<br>
{% for tax, subtotal in bill.get_subtotals.iteritems %}
<span class="subtotal column-title">subtotal {{ tax }}% VAT</span>
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
<br>
<span class="tax column-title">taxes {{ tax }}% VAT</span>
<span class="tax column-value">{{ subtotal | last }} &{{ currency.lower }};</span>
<br>
{% endfor %}
<span class="total column-title">total</span>
<span class="total column-value">{{ bill.total }} &{{ currency.lower }};</span>
<span class="total column-value">{{ bill.get_total }} &{{ currency.lower }};</span>
<br>
</div>
{% endblock %}

View File

@ -97,7 +97,7 @@ class ContactInline(InvoiceContactInline):
def has_invoice(account):
try:
account.invoicecontact.get()
account.invoicecontact
except InvoiceContact.DoesNotExist:
return False
return True

View File

@ -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,

View File

@ -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):

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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 \

View File

@ -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)