Fixes on bills

This commit is contained in:
Marc Aymerich 2015-07-13 11:31:32 +00:00
parent 03f03328b8
commit e1eda7a7d5
12 changed files with 192 additions and 58 deletions

15
TODO.md
View file

@ -444,8 +444,19 @@ def comma(value):
return value return value
# FIX CLOSE SEND DOWNLOAD
# payment/bill report allow to change template using a setting variable # 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 # 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 >

View file

@ -163,7 +163,7 @@ function install_requirements () {
# Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support) # Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support)
wkhtmltox=$(mktemp) 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} dpkg -i ${wkhtmltox}
# Make sure locales are in place before installing postgres # Make sure locales are in place before installing postgres

View file

@ -10,7 +10,7 @@ from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render, redirect 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.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
@ -134,7 +134,9 @@ def download_bills(modeladmin, request, queryset):
return response return response
bill = queryset.get() bill = queryset.get()
pdf = bill.as_pdf() 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.verbose_name = _("Download")
download_bills.url_name = 'download' download_bills.url_name = 'download'
@ -290,7 +292,7 @@ amend_bills.verbose_name = _("Amend")
amend_bills.url_name = 'amend' amend_bills.url_name = 'amend'
def report(modeladmin, request, queryset): def bill_report(modeladmin, request, queryset):
subtotals = {} subtotals = {}
total = 0 total = 0
for bill in queryset: for bill in queryset:
@ -301,11 +303,54 @@ def report(modeladmin, request, queryset):
subtotals[tax] = subtotal subtotals[tax] = subtotal
else: else:
subtotals[tax][1] += subtotal[1] subtotals[tax][1] += subtotal[1]
total += bill.get_total() total += bill.compute_total()
context = { context = {
'subtotals': subtotals, 'subtotals': subtotals,
'total': total, 'total': total,
'bills': queryset, 'bills': queryset,
'currency': settings.BILLS_CURRENCY, '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)

View file

@ -88,7 +88,7 @@ class ClosedBillLineInline(BillLineInline):
display_description.allow_tags = True display_description.allow_tags = True
def display_subtotal(self, line): def display_subtotal(self, line):
subtotals = ['  ' + str(line.subtotal)] subtotals = [' ' + str(line.subtotal)]
for subline in line.sublines.all(): for subline in line.sublines.all():
subtotals.append(str(subline.total)) subtotals.append(str(subline.total))
return '<br>'.join(subtotals) return '<br>'.join(subtotals)
@ -112,9 +112,11 @@ class BillLineAdmin(admin.ModelAdmin):
'description', 'bill_link', 'display_is_open', 'account_link', 'rate', 'quantity', 'tax', 'description', 'bill_link', 'display_is_open', 'account_link', 'rate', 'quantity', 'tax',
'subtotal', 'display_sublinetotal', 'display_total' 'subtotal', 'display_sublinetotal', 'display_total'
) )
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,) actions = (
list_filter = ('tax', ('bill', admin.RelatedOnlyFieldListFilter), 'bill__is_open') actions.undo_billing, actions.move_lines, actions.copy_lines, actions.service_report
list_select_related = ('bill',) )
list_filter = ('tax', 'bill__is_open', 'order__service')
list_select_related = ('bill', 'bill__account')
search_fields = ('description', 'bill__number') search_fields = ('description', 'bill__number')
account_link = admin_link('bill__account') account_link = admin_link('bill__account')
@ -139,9 +141,7 @@ class BillLineAdmin(admin.ModelAdmin):
qs = super(BillLineAdmin, self).get_queryset(request) qs = super(BillLineAdmin, self).get_queryset(request)
qs = qs.annotate( qs = qs.annotate(
subline_total=Sum('sublines__total'), subline_total=Sum('sublines__total'),
computed_total=Sum( computed_total=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100),
(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)
),
) )
return qs return qs
@ -203,7 +203,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',), 'fields': ('html',),
}), }),
) )
list_prefetch_related = ('transactions',) list_prefetch_related = ('transactions', 'lines__sublines')
search_fields = ('number', 'account__username', 'comments') search_fields = ('number', 'account__username', 'comments')
change_view_actions = [ change_view_actions = [
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills, actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
@ -211,7 +211,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
] ]
actions = [ actions = [
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills, 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') change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link', 'amend_links')
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state') readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
@ -236,10 +237,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines") num_lines.short_description = _("lines")
def display_total(self, bill): 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.allow_tags = True
display_total.short_description = _("total") display_total.short_description = _("total")
display_total.admin_order_field = 'computed_total' display_total.admin_order_field = 'approx_total'
def type_link(self, bill): def type_link(self, bill):
bill_type = bill.type.lower() bill_type = bill.type.lower()
@ -309,8 +310,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
inlines = super(BillAdmin, self).get_inline_instances(request, obj) inlines = super(BillAdmin, self).get_inline_instances(request, obj)
if obj and not obj.is_open: 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 type(inline) != BillLineInline]
return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)] return [inline for inline in inlines if type(inline) != ClosedBillLineInline]
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """
@ -327,9 +328,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(BillAdmin, self).get_queryset(request) qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate( qs = qs.annotate(
models.Count('lines'), models.Count('lines'),
computed_total=Sum( # FIXME https://code.djangoproject.com/ticket/10060
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100) approx_total=Coalesce(Sum(
), (F('lines__subtotal') + Coalesce('lines__sublines__total', 0)) * (1+F('lines__tax')/100),
), 0),
) )
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends') Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends')

View file

@ -22,7 +22,7 @@ class SelectSourceForm(forms.ModelForm):
super(SelectSourceForm, self).__init__(*args, **kwargs) super(SelectSourceForm, self).__init__(*args, **kwargs)
bill = kwargs.get('instance') bill = kwargs.get('instance')
if bill: if bill:
total = bill.get_total() total = bill.compute_total()
sources = bill.account.paymentsources.filter(is_active=True) sources = bill.account.paymentsources.filter(is_active=True)
recharge = bool(total < 0) recharge = bool(total < 0)
choices = [(None, '-----------')] choices = [(None, '-----------')]

View file

@ -183,7 +183,7 @@ msgstr "Factura"
#: filters.py:21 models.py:88 #: filters.py:21 models.py:88
msgid "Amendment invoice" msgid "Amendment invoice"
msgstr "Factura rectificative" msgstr "Factura rectificativa"
#: filters.py:22 models.py:89 #: filters.py:22 models.py:89
msgid "Fee" msgid "Fee"

View file

@ -165,7 +165,7 @@ class Bill(models.Model):
else: else:
raise TypeError("Unknown state") raise TypeError("Unknown state")
ongoing = bool(secured != 0 or created or processed or executed) ongoing = bool(secured != 0 or created or processed or executed)
total = self.get_total() total = self.compute_total()
if total >= 0: if total >= 0:
if secured >= total: if secured >= total:
return self.PAID return self.PAID
@ -202,15 +202,6 @@ class Bill(models.Model):
'amend_of': _("Type %s requires an amend of link.") % self.get_type_display() '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): def get_payment_state_display(self):
value = self.payment_state value = self.payment_state
return force_text(dict(self.PAYMENT_STATES).get(value, value)) return force_text(dict(self.PAYMENT_STATES).get(value, value))
@ -332,31 +323,42 @@ class Bill(models.Model):
@cached @cached
def compute_subtotals(self): def compute_subtotals(self):
subtotals = {} 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'): for tax, total in lines.values_list('tax', 'totals'):
subtotal, taxes = subtotals.get(tax) or (0, 0) try:
subtotal += total subtotals[tax] += total
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2)) except KeyError:
return subtotals subtotals[tax] = total
result = {}
for tax, subtotal in subtotals.items():
result[tax] = (subtotal, round(tax/100*subtotal, 2))
return result
@cached @cached
def compute_base(self): def compute_base(self):
bases = self.lines.annotate( 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) return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
@cached @cached
def compute_tax(self): def compute_tax(self):
taxes = self.lines.annotate( 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) return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
@cached @cached
def compute_total(self): def compute_total(self):
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 = self.lines.annotate(
totals=Sum((F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)) totals=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100)
) )
return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 2) return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 2)
@ -410,11 +412,6 @@ class BillLine(models.Model):
def __str__(self): def __str__(self):
return "#%i" % self.pk 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): def get_verbose_quantity(self):
return self.verbose_quantity or self.quantity return self.verbose_quantity or self.quantity
@ -434,6 +431,17 @@ class BillLine(models.Model):
return ini return ini
return "{ini} / {end}".format(ini=ini, end=end) 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): # def save(self, *args, **kwargs):
# super(BillLine, self).save(*args, **kwargs) # super(BillLine, self).save(*args, **kwargs)
# if self.bill.is_open: # if self.bill.is_open:

View file

@ -0,0 +1,72 @@
{% load i18n utils %}
<html>
<head>
<title>Transaction Report</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
@page {
size: 11.69in 8.27in;
}
table {
max-width: 10in;
font-family: sans;
font-size: 10px;
}
table tr:nth-child(even) {
background-color: #eee;
}
table tr:nth-child(odd) {
background-color: #fff;
}
table th {
color: white;
background-color: grey;
}
.item.column-created, .item.column-updated {
text-align: center;
}
.item.column-amount {
text-align: right;
}
.footnote {
font-family: sans;
font-size: 10px;
}
</style>
</head>
<body>
<table id="summary">
<tr class="header">
<th class="title column-name">{% trans "Services" %}</th>
<th class="title column-active">{% trans "Active" %}</th>
<th class="title column-cancelled">{% trans "Cancelled" %}</th>
<th class="title column-nominal-price">{% trans "Nominal price" %}</th>
<th class="title column-number">{% trans "Quantity" %}</th>
<th class="title column-number">{% trans "Profit" %}</th>
</tr>
{% for service, info in services %}
<tr>
<td class="item column-name">{{ service }}</td>
<td class="item column-amount">{{ info.0 }}</td>
<td class="item column-amount">{{ info.1 }}</td>
<td class="item column-amount">{{ info.2 }}</td>
<td class="item column-amount">{{ info.3 }}</td>
<td class="item column-amount">{{ info.4 }}</td>
</tr>
{% endfor %}
<tr>
<td class="item column-name"><b>{% trans "TOTAL" %}</b></td>
<td class="item column-amount"><b>{{ totals.0 }}</b></td>
<td class="item column-amount"><b>{{ totals.1 }}<b></td>
<td class="item column-amount"><b>{{ totals.2 }}<b></td>
<td class="item column-amount"><b>{{ totals.3 }}<b></td>
<td class="item column-amount"><b>{{ totals.4 }}<b></td>
</tr>
</table>
<div class="footnote">
* Custom lines
</div>
</body>
</html>

View file

@ -51,7 +51,7 @@
</div> </div>
<div id="total"> <div id="total">
<span class="title">{% trans "TOTAL" %}</span><br> <span class="title">{% trans "TOTAL" %}</span><br>
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span> <psan class="value">{{ bill.compute_total }} &{{ currency.lower }};</span>
</div> </div>
<div id="bill-date"> <div id="bill-date">
<span class="title">{% blocktrans with bill_type=bill.get_type_display.upper %}{{ bill_type }} DATE{% endblocktrans %}</span><br> <span class="title">{% blocktrans with bill_type=bill.get_type_display.upper %}{{ bill_type }} DATE{% endblocktrans %}</span><br>
@ -116,7 +116,7 @@
<br> <br>
{% endfor %} {% endfor %}
<span class="total column-title">{% trans "total" %}</span> <span class="total column-title">{% trans "total" %}</span>
<span class="total column-value">{{ bill.get_total }} &{{ currency.lower }};</span> <span class="total column-value">{{ bill.compute_total }} &{{ currency.lower }};</span>
<br> <br>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -40,7 +40,6 @@
<th class="title column-cancelled">{% trans "Cancelled" %}</th> <th class="title column-cancelled">{% trans "Cancelled" %}</th>
<th class="title column-nominal-price">{% trans "Nominal price" %}</th> <th class="title column-nominal-price">{% trans "Nominal price" %}</th>
<th class="title column-number">{% trans "Number" %}</th> <th class="title column-number">{% trans "Number" %}</th>
<th class="title column-number">{% trans "Profit" %}</th>
</tr> </tr>
{% for service, info in services %} {% for service, info in services %}
<tr> <tr>
@ -49,7 +48,6 @@
<td class="item column-amount">{{ info.1 }}</td> <td class="item column-amount">{{ info.1 }}</td>
<td class="item column-amount">{{ info.2 }}</td> <td class="item column-amount">{{ info.2 }}</td>
<td class="item column-amount">{{ info.3 }}</td> <td class="item column-amount">{{ info.3 }}</td>
<td class="item column-amount">{{ info.2|mul:info.3 }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr> <tr>
@ -57,10 +55,8 @@
<td class="item column-amount"><b>{{ totals.0 }}</b></td> <td class="item column-amount"><b>{{ totals.0 }}</b></td>
<td class="item column-amount">{{ totals.1 }}</td> <td class="item column-amount">{{ totals.1 }}</td>
<td class="item column-amount">{{ totals.2 }}</td> <td class="item column-amount">{{ totals.2 }}</td>
<td class="item column-amount">{{ totals.3 }}</td>
</tr> </tr>
</table> </table>
# TODO calculate profit better: order.get_price() for everyperiod / metric, etc
</body> </body>
</html> </html>