Fixes on billing

This commit is contained in:
Marc Aymerich 2015-07-10 13:00:51 +00:00
parent 47098ae398
commit 03f03328b8
13 changed files with 228 additions and 32 deletions

10
TODO.md
View File

@ -435,10 +435,6 @@ serailzer self.instance on create.
# process monitor data to represent state, or maybe create new resource datas when period expires? # process monitor data to represent state, or maybe create new resource datas when period expires?
# Automatically mark as paid transactions with 0 or prevent its creation?
@register.filter @register.filter
def comma(value): def comma(value):
value = str(value) value = str(value)
@ -447,3 +443,9 @@ def comma(value):
return ','.join((left, right)) return ','.join((left, right))
return value return value
# FIX CLOSE SEND DOWNLOAD
# payment/bill report allow to change template using a setting variable
# Payment transaction stats
# order stats: service, cost, top profit, etc

View File

@ -1,6 +1,7 @@
import io import io
import zipfile import zipfile
from datetime import date from datetime import date
from decimal import Decimal
from django.contrib import messages from django.contrib import messages
from django.contrib.admin import helpers from django.contrib.admin import helpers
@ -35,7 +36,7 @@ view_bill.url_name = 'view'
@transaction.atomic @transaction.atomic
def close_bills(modeladmin, request, queryset): def close_bills(modeladmin, request, queryset, action='close_bills'):
queryset = queryset.filter(is_open=True) queryset = queryset.filter(is_open=True)
if not queryset: if not queryset:
messages.warning(request, _("Selected bills should be in open state")) messages.warning(request, _("Selected bills should be in open state"))
@ -80,7 +81,7 @@ def close_bills(modeladmin, request, queryset):
'content_message': _("Once a bill is closed it can not be further modified.</p>" 'content_message': _("Once a bill is closed it can not be further modified.</p>"
"<p>Please select a payment source for the selected bills"), "<p>Please select a payment source for the selected bills"),
'action_name': 'Close bills', 'action_name': 'Close bills',
'action_value': 'close_bills', 'action_value': action,
'display_objects': [], 'display_objects': [],
'queryset': queryset, 'queryset': queryset,
'opts': opts, 'opts': opts,
@ -94,8 +95,11 @@ close_bills.verbose_name = _("Close")
close_bills.url_name = 'close' close_bills.url_name = 'close'
@action_with_confirmation() def send_bills_action(modeladmin, request, queryset):
def send_bills(modeladmin, request, queryset): """
raw function without confirmation
enables reuse on close_send_download_bills because of generic_confirmation.action_view
"""
for bill in queryset: for bill in queryset:
if not validate_contact(request, bill): if not validate_contact(request, bill):
return False return False
@ -108,6 +112,11 @@ def send_bills(modeladmin, request, queryset):
_("One bill has been sent."), _("One bill has been sent."),
_("%i bills have been sent.") % num, _("%i bills have been sent.") % num,
num)) num))
@action_with_confirmation()
def send_bills(modeladmin, request, queryset):
return send_bills_action(modeladmin, request, queryset)
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send") send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
send_bills.url_name = 'send' send_bills.url_name = 'send'
@ -131,9 +140,9 @@ download_bills.url_name = 'download'
def close_send_download_bills(modeladmin, request, queryset): def close_send_download_bills(modeladmin, request, queryset):
response = close_bills(modeladmin, request, queryset) response = close_bills(modeladmin, request, queryset, action='close_send_download_bills')
if request.POST.get('post') == 'generic_confirmation': if request.POST.get('post') == 'generic_confirmation':
response = send_bills(modeladmin, request, queryset) response = send_bills_action(modeladmin, request, queryset)
if response is False: if response is False:
return return
return download_bills(modeladmin, request, queryset) return download_bills(modeladmin, request, queryset)
@ -282,7 +291,20 @@ amend_bills.url_name = 'amend'
def report(modeladmin, request, queryset): def report(modeladmin, request, queryset):
subtotals = {}
total = 0
for bill in queryset:
for tax, subtotal in bill.compute_subtotals().items():
try:
subtotals[tax][0] += subtotal[0]
except KeyError:
subtotals[tax] = subtotal
else:
subtotals[tax][1] += subtotal[1]
total += bill.get_total()
context = { context = {
'subtotals': subtotals,
'total': total,
'bills': queryset, 'bills': queryset,
'currency': settings.BILLS_CURRENCY, 'currency': settings.BILLS_CURRENCY,
} }

View File

@ -184,7 +184,7 @@ class BillLineManagerAdmin(BillLineAdmin):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'number', 'type_link', 'account_link', 'created_on_display', 'number', 'type_link', 'account_link', 'updated_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent' 'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
) )
list_filter = ( list_filter = (
@ -218,7 +218,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
inlines = [BillLineInline, ClosedBillLineInline] inlines = [BillLineInline, ClosedBillLineInline]
date_hierarchy = 'closed_on' date_hierarchy = 'closed_on'
created_on_display = admin_date('created_on', short_description=_("Created")) updated_on_display = admin_date('updated_on', short_description=_("Updated"))
amend_of_link = admin_link('amend_of') amend_of_link = admin_link('amend_of')
def amend_links(self, bill): def amend_links(self, bill):

View File

@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.accounts.models import Account from orchestra.contrib.accounts.models import Account
from orchestra.contrib.contacts.models import Contact from orchestra.contrib.contacts.models import Contact
from orchestra.core import validators from orchestra.core import validators
from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
from . import settings from . import settings
@ -205,7 +206,7 @@ class Bill(models.Model):
if not self.is_open: if not self.is_open:
return self.total return self.total
try: try:
return round(self.computed_total, 2) return round(self.computed_total or 0, 2)
except AttributeError: except AttributeError:
self.computed_total = self.compute_total() self.computed_total = self.compute_total()
return self.computed_total return self.computed_total
@ -328,6 +329,7 @@ class Bill(models.Model):
self.number = self.get_number() self.number = self.get_number()
super(Bill, self).save(*args, **kwargs) super(Bill, self).save(*args, **kwargs)
@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') + Coalesce(F('sublines__total'), 0)))
@ -337,15 +339,24 @@ class Bill(models.Model):
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2)) subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
return subtotals return subtotals
@cached
def compute_base(self): def compute_base(self):
bases = self.lines.annotate( bases = self.lines.annotate(
bases=F('subtotal') + Coalesce(F('sublines__total'), 0) bases=Sum(F('subtotal') + Coalesce(F('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
def compute_tax(self):
taxes = self.lines.annotate(
taxes=Sum((F('subtotal') + Coalesce(F('sublines__total'), 0)) * (F('tax')/100))
)
return round(taxes.aggregate(Sum('taxes'))['taxes__sum'] or 0, 2)
@cached
def compute_total(self): def compute_total(self):
totals = self.lines.annotate( totals = self.lines.annotate(
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100) totals=Sum((F('subtotal') + Coalesce(F('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)

View File

@ -6,12 +6,16 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css"> <style type="text/css">
@page { @page {
size: 11.69in 8.27in; size: 11.69in 8.27in;
} }
table { table {
font-family: sans; font-family: sans;
font-size: 10px; font-size: 10px;
max-width: 10in; max-width: 10in;
margin: 4px;
}
.item.column-name {
text-align: right;
} }
table tr:nth-child(even) { table tr:nth-child(even) {
background-color: #eee; background-color: #eee;
@ -23,6 +27,7 @@
color: white; color: white;
background-color: grey; background-color: grey;
} }
.item.column-base, .item.column-vat, .item.column-total, .item.column-number { .item.column-base, .item.column-vat, .item.column-total, .item.column-number {
text-align: right; text-align: right;
} }
@ -32,8 +37,30 @@
</style> </style>
</head> </head>
<body> <body>
<table> <table id="summary">
<tr id="transaction"> <tr class="header">
<th class="title column-name">{% trans "Summary" %}</th>
<th class="title column-total">{% trans "Total" %}</th>
</tr>
{% for tax, subtotal in subtotals.items %}
<tr>
<td class="item column-name">{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}</td>
<td class="item column-total">{{ subtotal|first}}</td>
</tr>
<tr>
<td class="item column-name">{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}</td>
<td class="item column-total">{{ subtotal|last}}</td>
</tr>
{% endfor %}
<tr>
<td class="item column-name"><b>{% trans "TOTAL" %}</b></td>
<td class="item column-total"><b>{{ total }}</b></td>
</tr>
</table>
<table id="main">
<tr class="header">
<th class="title column-number">{% trans "Number" %}</th> <th class="title column-number">{% trans "Number" %}</th>
<th class="title column-vat-number">{% trans "VAT number" %}</th> <th class="title column-vat-number">{% trans "VAT number" %}</th>
<th class="title column-billcontant">{% trans "Contact" %}</th> <th class="title column-billcontant">{% trans "Contact" %}</th>

View File

@ -172,14 +172,14 @@ class Domain(models.Model):
type=Record.MX, type=Record.MX,
value=mx value=mx
)) ))
if not has_a: # A and AAAA point to the same default host
if not has_a and not has_aaaa:
default_a = settings.DOMAINS_DEFAULT_A default_a = settings.DOMAINS_DEFAULT_A
if default_a: if default_a:
records.append(AttrDict( records.append(AttrDict(
type=Record.A, type=Record.A,
value=default_a value=default_a
)) ))
if not has_aaaa:
default_aaaa = settings.DOMAINS_DEFAULT_AAAA default_aaaa = settings.DOMAINS_DEFAULT_AAAA
if default_aaaa: if default_aaaa:
records.append(AttrDict( records.append(AttrDict(

View File

@ -15,10 +15,8 @@ from . import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
paramiko_connections = {}
def Paramiko(backend, log, server, cmds, async=False, paramiko_connections={}):
def Paramiko(backend, log, server, cmds, async=False):
""" """
Executes cmds to remote server using Pramaiko Executes cmds to remote server using Pramaiko
""" """

View File

@ -1,6 +1,7 @@
from django.contrib import admin, messages from django.contrib import admin, messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.utils import 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 _
from django.shortcuts import render from django.shortcuts import render
@ -144,3 +145,30 @@ def mark_as_not_ignored(modeladmin, request, queryset):
_("%i selected orders have been marked as not ignored.") % num, _("%i selected orders have been marked as not ignored.") % num,
num) num)
modeladmin.message_user(request, msg) modeladmin.message_user(request, msg)
def report(modeladmin, request, queryset):
services = {}
totals = [0, 0, None, 0]
now = timezone.now().date()
for order in queryset.select_related('service'):
name = order.service.description
active, cancelled = (1, 0) if not order.cancelled_on or order.cancelled_on > now else (0, 1)
try:
info = services[name]
except KeyError:
nominal_price = order.service.nominal_price
info = [active, cancelled, nominal_price, 1]
services[name] = info
else:
info[0] += active
info[1] += cancelled
info[3] += 1
totals[0] += active
totals[1] += cancelled
totals[3] += 1
context = {
'services': sorted(services.items(), key=lambda n: -n[1][0]),
'totals': totals,
}
return render(request, 'admin/orders/order/report.html', context)

View File

@ -11,7 +11,7 @@ from orchestra.admin.utils import admin_link, admin_date
from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.utils.humanize import naturaldate from orchestra.utils.humanize import naturaldate
from .actions import BillSelectedOrders, mark_as_ignored, mark_as_not_ignored from .actions import BillSelectedOrders, mark_as_ignored, mark_as_not_ignored, report
from .filters import IgnoreOrderListFilter, ActiveOrderListFilter, BilledOrderListFilter from .filters import IgnoreOrderListFilter, ActiveOrderListFilter, BilledOrderListFilter
from .models import Order, MetricStorage from .models import Order, MetricStorage
@ -55,7 +55,7 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
default_changelist_filters = ( default_changelist_filters = (
('ignore', '0'), ('ignore', '0'),
) )
actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored, report)
change_view_actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) change_view_actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored)
date_hierarchy = 'registered_on' date_hierarchy = 'registered_on'
inlines = (MetricStorageInline,) inlines = (MetricStorageInline,)

View File

@ -0,0 +1,66 @@
{% 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;
}
</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 "Number" %}</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.2|mul:info.3 }}</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">{{ totals.1 }}</td>
<td class="item column-amount">{{ totals.2 }}</td>
<td class="item column-amount">{{ totals.3 }}</td>
</tr>
</table>
# TODO calculate profit better: order.get_price() for everyperiod / metric, etc
</body>
</html>

View File

@ -190,7 +190,18 @@ def report(modeladmin, request, queryset):
else: else:
transactions = queryset.values_list('transactions__id', flat=True).distinct() transactions = queryset.values_list('transactions__id', flat=True).distinct()
transactions = Transaction.objects.filter(id__in=transactions) transactions = Transaction.objects.filter(id__in=transactions)
states = {}
total = 0
for transaction in transactions:
state = transaction.get_state_display()
try:
states[state] += transaction.amount
except KeyError:
states[state] = transaction.amount
total += transaction.amount
context = { context = {
'transactions': transactions 'states': states,
'total': total,
'transactions': transactions,
} }
return render(request, 'admin/payments/transaction/report.html', context) return render(request, 'admin/payments/transaction/report.html', context)

View File

@ -9,9 +9,9 @@
size: 11.69in 8.27in; size: 11.69in 8.27in;
} }
table { table {
max-width: 10in;
font-family: sans; font-family: sans;
font-size: 10px; font-size: 10px;
max-width: 10in;
} }
table tr:nth-child(even) { table tr:nth-child(even) {
background-color: #eee; background-color: #eee;
@ -23,9 +23,34 @@
color: white; color: white;
background-color: grey; background-color: grey;
} }
.item.column-created, .item.column-updated {
text-align: center;
}
.item.column-amount {
text-align: right;
}
</style> </style>
</head> </head>
<body> <body>
<table id="summary">
<tr class="header">
<th class="title column-name">{% trans "Summary" %}</th>
<th class="title column-amount">{% trans "Amount" %}</th>
</tr>
{% for state, amount in states.items %}
<tr>
<td class="item column-name">{{ state }}</td>
<td class="item column-amount">{{ amount }}</td>
</tr>
{% endfor %}
<tr>
<td class="item column-name"><b>{% trans "TOTAL" %}</b></td>
<td class="item column-amount"><b>{{ total }}</b></td>
</tr>
</table>
<table> <table>
<tr id="transaction"> <tr id="transaction">
<th class="title column-id">ID</th> <th class="title column-id">ID</th>
@ -47,8 +72,8 @@
<td class="item column-iban">{{ transaction.source.data.iban }}</td> <td class="item column-iban">{{ transaction.source.data.iban }}</td>
<td class="item column-amount">{{ transaction.amount }}</td> <td class="item column-amount">{{ transaction.amount }}</td>
<td class="item column-state">{{ transaction.get_state_display }}</td> <td class="item column-state">{{ transaction.get_state_display }}</td>
<td class="item column-state">{{ transaction.created_at|date }}</td> <td class="item column-created">{{ transaction.created_at|date }}</td>
<td class="item column-state">{{ transaction.modified_at|date }}</td> <td class="item column-updated">{% if transaction.created_at|date != transaction.modified_at|date %}{{ transaction.modified_at|date }}{% else %} --- {% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -100,3 +100,9 @@ def isactive(obj):
@register.filter @register.filter
def sub(value, arg): def sub(value, arg):
return value - arg return value - arg
@register.filter
def mul(value, arg):
return value * arg