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?
# Automatically mark as paid transactions with 0 or prevent its creation?
@register.filter
def comma(value):
value = str(value)
@ -447,3 +443,9 @@ def comma(value):
return ','.join((left, right))
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 zipfile
from datetime import date
from decimal import Decimal
from django.contrib import messages
from django.contrib.admin import helpers
@ -35,7 +36,7 @@ view_bill.url_name = 'view'
@transaction.atomic
def close_bills(modeladmin, request, queryset):
def close_bills(modeladmin, request, queryset, action='close_bills'):
queryset = queryset.filter(is_open=True)
if not queryset:
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>"
"<p>Please select a payment source for the selected bills"),
'action_name': 'Close bills',
'action_value': 'close_bills',
'action_value': action,
'display_objects': [],
'queryset': queryset,
'opts': opts,
@ -94,8 +95,11 @@ close_bills.verbose_name = _("Close")
close_bills.url_name = 'close'
@action_with_confirmation()
def send_bills(modeladmin, request, queryset):
def send_bills_action(modeladmin, request, queryset):
"""
raw function without confirmation
enables reuse on close_send_download_bills because of generic_confirmation.action_view
"""
for bill in queryset:
if not validate_contact(request, bill):
return False
@ -108,6 +112,11 @@ def send_bills(modeladmin, request, queryset):
_("One bill has been sent."),
_("%i bills have been sent.") % 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.url_name = 'send'
@ -131,9 +140,9 @@ download_bills.url_name = 'download'
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':
response = send_bills(modeladmin, request, queryset)
response = send_bills_action(modeladmin, request, queryset)
if response is False:
return
return download_bills(modeladmin, request, queryset)
@ -282,7 +291,20 @@ amend_bills.url_name = 'amend'
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 = {
'subtotals': subtotals,
'total': total,
'bills': queryset,
'currency': settings.BILLS_CURRENCY,
}

View file

@ -184,7 +184,7 @@ class BillLineManagerAdmin(BillLineAdmin):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
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'
)
list_filter = (
@ -218,7 +218,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
inlines = [BillLineInline, ClosedBillLineInline]
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')
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.contacts.models import Contact
from orchestra.core import validators
from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf
from . import settings
@ -205,7 +206,7 @@ class Bill(models.Model):
if not self.is_open:
return self.total
try:
return round(self.computed_total, 2)
return round(self.computed_total or 0, 2)
except AttributeError:
self.computed_total = self.compute_total()
return self.computed_total
@ -328,6 +329,7 @@ class Bill(models.Model):
self.number = self.get_number()
super(Bill, self).save(*args, **kwargs)
@cached
def compute_subtotals(self):
subtotals = {}
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))
return subtotals
@cached
def compute_base(self):
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)
@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):
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)

View file

@ -12,6 +12,10 @@
font-family: sans;
font-size: 10px;
max-width: 10in;
margin: 4px;
}
.item.column-name {
text-align: right;
}
table tr:nth-child(even) {
background-color: #eee;
@ -23,6 +27,7 @@
color: white;
background-color: grey;
}
.item.column-base, .item.column-vat, .item.column-total, .item.column-number {
text-align: right;
}
@ -32,8 +37,30 @@
</style>
</head>
<body>
<table>
<tr id="transaction">
<table id="summary">
<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-vat-number">{% trans "VAT number" %}</th>
<th class="title column-billcontant">{% trans "Contact" %}</th>

View file

@ -172,14 +172,14 @@ class Domain(models.Model):
type=Record.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
if default_a:
records.append(AttrDict(
type=Record.A,
value=default_a
))
if not has_aaaa:
default_aaaa = settings.DOMAINS_DEFAULT_AAAA
if default_aaaa:
records.append(AttrDict(

View file

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

View file

@ -1,6 +1,7 @@
from django.contrib import admin, messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _
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,
num)
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.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 .models import Order, MetricStorage
@ -55,7 +55,7 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
default_changelist_filters = (
('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)
date_hierarchy = 'registered_on'
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:
transactions = queryset.values_list('transactions__id', flat=True).distinct()
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 = {
'transactions': transactions
'states': states,
'total': total,
'transactions': transactions,
}
return render(request, 'admin/payments/transaction/report.html', context)

View file

@ -9,9 +9,9 @@
size: 11.69in 8.27in;
}
table {
max-width: 10in;
font-family: sans;
font-size: 10px;
max-width: 10in;
}
table tr:nth-child(even) {
background-color: #eee;
@ -23,9 +23,34 @@
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 "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>
<tr id="transaction">
<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-amount">{{ transaction.amount }}</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-state">{{ transaction.modified_at|date }}</td>
<td class="item column-created">{{ transaction.created_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>
{% endfor %}
</table>

View file

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