diff --git a/TODO.md b/TODO.md index f4334e58..bf93ff5c 100644 --- a/TODO.md +++ b/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 diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py index d3f03862..945b696a 100644 --- a/orchestra/contrib/bills/actions.py +++ b/orchestra/contrib/bills/actions.py @@ -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.

" "

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, } diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 1cabf881..7cc0096c 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -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): diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index aabb3305..78c15b20 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -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) diff --git a/orchestra/contrib/bills/templates/admin/bills/report.html b/orchestra/contrib/bills/templates/admin/bills/report.html index 61d74426..4a26ec78 100644 --- a/orchestra/contrib/bills/templates/admin/bills/report.html +++ b/orchestra/contrib/bills/templates/admin/bills/report.html @@ -6,12 +6,16 @@ - - +
+ + + + +{% for tax, subtotal in subtotals.items %} + + + + + + + + +{% endfor %} + + + + +
{% trans "Summary" %}{% trans "Total" %}
{% trans "subtotal" %} {{ tax }}% {% trans "VAT" %}{{ subtotal|first}}
{% trans "taxes" %} {{ tax }}% {% trans "VAT" %}{{ subtotal|last}}
{% trans "TOTAL" %}{{ total }}
+ + + + diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index a58de3ac..3e0bebd3 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -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( diff --git a/orchestra/contrib/orchestration/methods.py b/orchestra/contrib/orchestration/methods.py index a0786a0c..ba9dd456 100644 --- a/orchestra/contrib/orchestration/methods.py +++ b/orchestra/contrib/orchestration/methods.py @@ -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 """ diff --git a/orchestra/contrib/orders/actions.py b/orchestra/contrib/orders/actions.py index a7e74922..fbfb9369 100644 --- a/orchestra/contrib/orders/actions.py +++ b/orchestra/contrib/orders/actions.py @@ -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) diff --git a/orchestra/contrib/orders/admin.py b/orchestra/contrib/orders/admin.py index a966295a..bc6fa5d3 100644 --- a/orchestra/contrib/orders/admin.py +++ b/orchestra/contrib/orders/admin.py @@ -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,) diff --git a/orchestra/contrib/orders/templates/admin/orders/order/report.html b/orchestra/contrib/orders/templates/admin/orders/order/report.html new file mode 100644 index 00000000..e2886d68 --- /dev/null +++ b/orchestra/contrib/orders/templates/admin/orders/order/report.html @@ -0,0 +1,66 @@ +{% load i18n utils %} + + + + Transaction Report + + + + + +
{% trans "Number" %} {% trans "VAT number" %} {% trans "Contact" %}
+ + + + + + + + +{% for service, info in services %} + + + + + + + + +{% endfor %} + + + + + + + +
{% trans "Services" %}{% trans "Active" %}{% trans "Cancelled" %}{% trans "Nominal price" %}{% trans "Number" %}{% trans "Profit" %}
{{ service }}{{ info.0 }}{{ info.1 }}{{ info.2 }}{{ info.3 }}{{ info.2|mul:info.3 }}
{% trans "TOTAL" %}{{ totals.0 }}{{ totals.1 }}{{ totals.2 }}{{ totals.3 }}
+ +# TODO calculate profit better: order.get_price() for everyperiod / metric, etc + + diff --git a/orchestra/contrib/payments/actions.py b/orchestra/contrib/payments/actions.py index 2e22165a..86930f44 100644 --- a/orchestra/contrib/payments/actions.py +++ b/orchestra/contrib/payments/actions.py @@ -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) diff --git a/orchestra/contrib/payments/templates/admin/payments/transaction/report.html b/orchestra/contrib/payments/templates/admin/payments/transaction/report.html index 89a3ba28..185e1b70 100644 --- a/orchestra/contrib/payments/templates/admin/payments/transaction/report.html +++ b/orchestra/contrib/payments/templates/admin/payments/transaction/report.html @@ -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; + } + + + + + + +{% for state, amount in states.items %} + + + + +{% endfor %} + + + + +
{% trans "Summary" %}{% trans "Amount" %}
{{ state }}{{ amount }}
{% trans "TOTAL" %}{{ total }}
+ + @@ -47,8 +72,8 @@ - - + + {% endfor %}
ID{{ transaction.source.data.iban }} {{ transaction.amount }} {{ transaction.get_state_display }}{{ transaction.created_at|date }}{{ transaction.modified_at|date }}{{ transaction.created_at|date }}{% if transaction.created_at|date != transaction.modified_at|date %}{{ transaction.modified_at|date }}{% else %} --- {% endif %}
diff --git a/orchestra/templatetags/utils.py b/orchestra/templatetags/utils.py index f94bc3f5..e883bd4c 100644 --- a/orchestra/templatetags/utils.py +++ b/orchestra/templatetags/utils.py @@ -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 +