diff --git a/TODO.md b/TODO.md
index bf93ff5c..b7303c9f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -444,8 +444,19 @@ def comma(value):
return value
-# FIX CLOSE SEND DOWNLOAD
# payment/bill report allow to change template using a setting variable
-# Payment transaction stats
+# Payment transaction stats, graps over time
# order stats: service, cost, top profit, etc
+# TODO remove bill.total
+
+
+reporter.stories_filed = F('stories_filed') + 1
+reporter.save()
+In order to access the new value that has been saved in this way, the object will need to be reloaded:
+https://docs.djangoproject.com/en/dev/ref/models/conditional-expressions/
+Greatest
+Colaesce('total', 'computed_total')
+Case
+
+# case on payment transaction state ? case when trans.amount >
diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin
index dd9f1e6c..79ce5453 100755
--- a/orchestra/bin/orchestra-admin
+++ b/orchestra/bin/orchestra-admin
@@ -163,7 +163,7 @@ function install_requirements () {
# Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support)
wkhtmltox=$(mktemp)
- wget http://downloads.sourceforge.net/wkhtmltopdf/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox}
+ wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox}
dpkg -i ${wkhtmltox}
# Make sure locales are in place before installing postgres
diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py
index 945b696a..729279a5 100644
--- a/orchestra/contrib/bills/actions.py
+++ b/orchestra/contrib/bills/actions.py
@@ -10,7 +10,7 @@ from django.core.urlresolvers import reverse
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import render, redirect
-from django.utils import translation
+from django.utils import translation, timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _
@@ -134,7 +134,9 @@ def download_bills(modeladmin, request, queryset):
return response
bill = queryset.get()
pdf = bill.as_pdf()
- return HttpResponse(pdf, content_type='application/pdf')
+ response = HttpResponse(pdf, content_type='application/pdf')
+ response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % bill.number
+ return response
download_bills.verbose_name = _("Download")
download_bills.url_name = 'download'
@@ -290,7 +292,7 @@ amend_bills.verbose_name = _("Amend")
amend_bills.url_name = 'amend'
-def report(modeladmin, request, queryset):
+def bill_report(modeladmin, request, queryset):
subtotals = {}
total = 0
for bill in queryset:
@@ -301,11 +303,54 @@ def report(modeladmin, request, queryset):
subtotals[tax] = subtotal
else:
subtotals[tax][1] += subtotal[1]
- total += bill.get_total()
+ total += bill.compute_total()
context = {
'subtotals': subtotals,
'total': total,
'bills': queryset,
'currency': settings.BILLS_CURRENCY,
}
- return render(request, 'admin/bills/report.html', context)
+ return render(request, 'admin/bills/bill/report.html', context)
+
+
+def service_report(modeladmin, request, queryset):
+ services = {}
+ totals = [0, 0, 0, 0, 0]
+ now = timezone.now().date()
+ if queryset.model == Bill:
+ queryset = BillLine.objects.filter(bill_id__in=queryset.values_list('id', flat=True))
+ # Filter amends
+ queryset = queryset.filter(bill__amend_of__isnull=True)
+ for line in queryset.select_related('order__service').prefetch_related('sublines'):
+ order, service = None, None
+ if line.order_id:
+ order = line.order
+ service = order.service
+ name = service.description
+ active, cancelled = (1, 0) if not order.cancelled_on or order.cancelled_on > now else (0, 1)
+ nominal_price = order.service.nominal_price
+ else:
+ name = '*%s' % line.description
+ active = 1
+ cancelled = 0
+ nominal_price = 0
+ try:
+ info = services[name]
+ except KeyError:
+ info = [active, cancelled, nominal_price, line.quantity or 1, line.compute_total()]
+ services[name] = info
+ else:
+ info[0] += active
+ info[1] += cancelled
+ info[3] += line.quantity or 1
+ info[4] += line.compute_total()
+ totals[0] += active
+ totals[1] += cancelled
+ totals[2] += nominal_price
+ totals[3] += line.quantity or 1
+ totals[4] += line.compute_total()
+ context = {
+ 'services': sorted(services.items(), key=lambda n: -n[1][4]),
+ 'totals': totals,
+ }
+ return render(request, 'admin/bills/billline/report.html', context)
diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py
index 7cc0096c..347ad6ba 100644
--- a/orchestra/contrib/bills/admin.py
+++ b/orchestra/contrib/bills/admin.py
@@ -88,7 +88,7 @@ class ClosedBillLineInline(BillLineInline):
display_description.allow_tags = True
def display_subtotal(self, line):
- subtotals = [' ' + str(line.subtotal)]
+ subtotals = [' ' + str(line.subtotal)]
for subline in line.sublines.all():
subtotals.append(str(subline.total))
return '
'.join(subtotals)
@@ -112,9 +112,11 @@ class BillLineAdmin(admin.ModelAdmin):
'description', 'bill_link', 'display_is_open', 'account_link', 'rate', 'quantity', 'tax',
'subtotal', 'display_sublinetotal', 'display_total'
)
- actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
- list_filter = ('tax', ('bill', admin.RelatedOnlyFieldListFilter), 'bill__is_open')
- list_select_related = ('bill',)
+ actions = (
+ actions.undo_billing, actions.move_lines, actions.copy_lines, actions.service_report
+ )
+ list_filter = ('tax', 'bill__is_open', 'order__service')
+ list_select_related = ('bill', 'bill__account')
search_fields = ('description', 'bill__number')
account_link = admin_link('bill__account')
@@ -139,9 +141,7 @@ class BillLineAdmin(admin.ModelAdmin):
qs = super(BillLineAdmin, self).get_queryset(request)
qs = qs.annotate(
subline_total=Sum('sublines__total'),
- computed_total=Sum(
- (F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)
- ),
+ computed_total=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100),
)
return qs
@@ -203,7 +203,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',),
}),
)
- list_prefetch_related = ('transactions',)
+ list_prefetch_related = ('transactions', 'lines__sublines')
search_fields = ('number', 'account__username', 'comments')
change_view_actions = [
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
@@ -211,7 +211,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
]
actions = [
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
- actions.amend_bills, actions.report, actions.close_send_download_bills,
+ actions.amend_bills, actions.bill_report, actions.service_report,
+ actions.close_send_download_bills,
]
change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link', 'amend_links')
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
@@ -236,10 +237,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines")
def display_total(self, bill):
- return "%s &%s;" % (round(bill.computed_total or 0, 2), settings.BILLS_CURRENCY.lower())
+ return "%s &%s;" % (bill.compute_total(), settings.BILLS_CURRENCY.lower())
display_total.allow_tags = True
display_total.short_description = _("total")
- display_total.admin_order_field = 'computed_total'
+ display_total.admin_order_field = 'approx_total'
def type_link(self, bill):
bill_type = bill.type.lower()
@@ -309,8 +310,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_inline_instances(self, request, obj=None):
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
if obj and not obj.is_open:
- return [inline for inline in inlines if not isinstance(inline, BillLineInline)]
- return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)]
+ return [inline for inline in inlines if type(inline) != BillLineInline]
+ return [inline for inline in inlines if type(inline) != ClosedBillLineInline]
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
@@ -327,9 +328,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(
models.Count('lines'),
- computed_total=Sum(
- (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
- ),
+ # FIXME https://code.djangoproject.com/ticket/10060
+ approx_total=Coalesce(Sum(
+ (F('lines__subtotal') + Coalesce('lines__sublines__total', 0)) * (1+F('lines__tax')/100),
+ ), 0),
)
qs = qs.prefetch_related(
Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends')
diff --git a/orchestra/contrib/bills/forms.py b/orchestra/contrib/bills/forms.py
index d6236fa1..9d2295cf 100644
--- a/orchestra/contrib/bills/forms.py
+++ b/orchestra/contrib/bills/forms.py
@@ -22,7 +22,7 @@ class SelectSourceForm(forms.ModelForm):
super(SelectSourceForm, self).__init__(*args, **kwargs)
bill = kwargs.get('instance')
if bill:
- total = bill.get_total()
+ total = bill.compute_total()
sources = bill.account.paymentsources.filter(is_active=True)
recharge = bool(total < 0)
choices = [(None, '-----------')]
diff --git a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo
index a9cc33e6..8f1b04a9 100644
Binary files a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo and b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo differ
diff --git a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po
index 35811b42..1a4f89a1 100644
--- a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po
+++ b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po
@@ -183,7 +183,7 @@ msgstr "Factura"
#: filters.py:21 models.py:88
msgid "Amendment invoice"
-msgstr "Factura rectificative"
+msgstr "Factura rectificativa"
#: filters.py:22 models.py:89
msgid "Fee"
diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py
index 78c15b20..648cc77c 100644
--- a/orchestra/contrib/bills/models.py
+++ b/orchestra/contrib/bills/models.py
@@ -165,7 +165,7 @@ class Bill(models.Model):
else:
raise TypeError("Unknown state")
ongoing = bool(secured != 0 or created or processed or executed)
- total = self.get_total()
+ total = self.compute_total()
if total >= 0:
if secured >= total:
return self.PAID
@@ -202,15 +202,6 @@ class Bill(models.Model):
'amend_of': _("Type %s requires an amend of link.") % self.get_type_display()
})
- def get_total(self):
- if not self.is_open:
- return self.total
- try:
- return round(self.computed_total or 0, 2)
- except AttributeError:
- self.computed_total = self.compute_total()
- return self.computed_total
-
def get_payment_state_display(self):
value = self.payment_state
return force_text(dict(self.PAYMENT_STATES).get(value, value))
@@ -332,33 +323,44 @@ class Bill(models.Model):
@cached
def compute_subtotals(self):
subtotals = {}
- lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
+ lines = self.lines.annotate(totals=F('subtotal') + Sum(Coalesce('sublines__total', 0)))
for tax, total in lines.values_list('tax', 'totals'):
- subtotal, taxes = subtotals.get(tax) or (0, 0)
- subtotal += total
- subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
- return subtotals
+ try:
+ subtotals[tax] += total
+ except KeyError:
+ subtotals[tax] = total
+ result = {}
+ for tax, subtotal in subtotals.items():
+ result[tax] = (subtotal, round(tax/100*subtotal, 2))
+ return result
@cached
def compute_base(self):
bases = self.lines.annotate(
- bases=Sum(F('subtotal') + Coalesce(F('sublines__total'), 0))
+ bases=F('subtotal') + Sum(Coalesce('sublines__total', 0))
)
return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
@cached
def compute_tax(self):
taxes = self.lines.annotate(
- taxes=Sum((F('subtotal') + Coalesce(F('sublines__total'), 0)) * (F('tax')/100))
+ taxes=(F('subtotal') + Coalesce(Sum('sublines__total'), 0)) * (F('tax')/100)
)
return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
@cached
def compute_total(self):
- totals = self.lines.annotate(
- totals=Sum((F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
- )
- return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 2)
+ if 'lines' in getattr(self, '_prefetched_objects_cache', ()):
+ total = 0
+ for line in self.lines.all():
+ line_total = line.compute_total()
+ total += line_total * (1+line.tax/100)
+ return round(total, 2)
+ else:
+ totals = self.lines.annotate(
+ totals=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100)
+ )
+ return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 2)
class Invoice(Bill):
@@ -410,11 +412,6 @@ class BillLine(models.Model):
def __str__(self):
return "#%i" % self.pk
- def compute_total(self):
- """ Computes subline discounts """
- if self.pk:
- return self.subtotal + sum([sub.total for sub in self.sublines.all()])
-
def get_verbose_quantity(self):
return self.verbose_quantity or self.quantity
@@ -434,6 +431,17 @@ class BillLine(models.Model):
return ini
return "{ini} / {end}".format(ini=ini, end=end)
+ @cached
+ def compute_total(self):
+ total = self.subtotal or 0
+ if hasattr(self, 'subline_total'):
+ total += self.subline_total or 0
+ elif 'sublines' in getattr(self, '_prefetched_objects_cache', ()):
+ total += sum(subline.total for subline in self.sublines.all())
+ else:
+ total += self.sublines.aggregate(sub_total=Sum('total'))['sub_total'] or 0
+ return round(total, 2)
+
# def save(self, *args, **kwargs):
# super(BillLine, self).save(*args, **kwargs)
# if self.bill.is_open:
diff --git a/orchestra/contrib/bills/templates/admin/bills/report.html b/orchestra/contrib/bills/templates/admin/bills/bill/report.html
similarity index 100%
rename from orchestra/contrib/bills/templates/admin/bills/report.html
rename to orchestra/contrib/bills/templates/admin/bills/bill/report.html
diff --git a/orchestra/contrib/bills/templates/admin/bills/billline/report.html b/orchestra/contrib/bills/templates/admin/bills/billline/report.html
new file mode 100644
index 00000000..4227a433
--- /dev/null
+++ b/orchestra/contrib/bills/templates/admin/bills/billline/report.html
@@ -0,0 +1,72 @@
+{% load i18n utils %}
+
+
+
{% trans "Services" %} | +{% trans "Active" %} | +{% trans "Cancelled" %} | +{% trans "Nominal price" %} | +{% trans "Quantity" %} | +{% trans "Profit" %} | +
---|---|---|---|---|---|
{{ service }} | +{{ info.0 }} | +{{ info.1 }} | +{{ info.2 }} | +{{ info.3 }} | +{{ info.4 }} | +
{% trans "TOTAL" %} | +{{ totals.0 }} | +{{ totals.1 }} | +{{ totals.2 }} | +{{ totals.3 }} | +{{ totals.4 }} | +