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 %} + + + + Transaction Report + + + + + + + + + + + + + + +{% for service, info in services %} + + + + + + + + +{% endfor %} + + + + + + + + +
{% 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 }}
+
+* Custom lines +
+ + diff --git a/orchestra/contrib/bills/templates/bills/microspective.html b/orchestra/contrib/bills/templates/bills/microspective.html index ee6c4b46..8662695f 100644 --- a/orchestra/contrib/bills/templates/bills/microspective.html +++ b/orchestra/contrib/bills/templates/bills/microspective.html @@ -51,7 +51,7 @@
{% trans "TOTAL" %}
- {{ bill.get_total }} &{{ currency.lower }}; + {{ bill.compute_total }} &{{ currency.lower }};
{% blocktrans with bill_type=bill.get_type_display.upper %}{{ bill_type }} DATE{% endblocktrans %}
@@ -116,7 +116,7 @@
{% endfor %} {% trans "total" %} - {{ bill.get_total }} &{{ currency.lower }}; + {{ bill.compute_total }} &{{ currency.lower }};
{% endblock %} diff --git a/orchestra/contrib/orders/templates/admin/orders/order/report.html b/orchestra/contrib/orders/templates/admin/orders/order/report.html index e2886d68..87ef5f07 100644 --- a/orchestra/contrib/orders/templates/admin/orders/order/report.html +++ b/orchestra/contrib/orders/templates/admin/orders/order/report.html @@ -40,7 +40,6 @@ {% trans "Cancelled" %} {% trans "Nominal price" %} {% trans "Number" %} - {% trans "Profit" %} {% for service, info in services %} @@ -49,7 +48,6 @@ {{ info.1 }} {{ info.2 }} {{ info.3 }} - {{ info.2|mul:info.3 }} {% endfor %} @@ -57,10 +55,8 @@ {{ totals.0 }} {{ totals.1 }} {{ totals.2 }} - {{ totals.3 }} -# TODO calculate profit better: order.get_price() for everyperiod / metric, etc