Fixes on billing
This commit is contained in:
parent
47098ae398
commit
03f03328b8
10
TODO.md
10
TODO.md
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,)
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue