# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # import os import platform import sys import zoneinfo from datetime import datetime, timedelta from django.conf import settings from django.db.models import Count, Exists, F, Min, OuterRef, Q, Sum from django.utils.formats import date_format from django.utils.timezone import now from reportlab.lib import pagesizes from reportlab.lib.enums import TA_CENTER from reportlab.lib.units import mm from reportlab.platypus import Paragraph, Spacer, Table, TableStyle from pretix import __version__ from pretix.base.models import Order, OrderPayment, Transaction from pretix.base.plugins import get_all_plugins from pretix.base.templatetags.money import money_filter from pretix.plugins.reports.exporters import ReportlabExportMixin from pretix.settings import DATA_DIR class SysReport(ReportlabExportMixin): @property def pagesize(self): return pagesizes.portrait(pagesizes.A4) def __init__(self, start_month, tzname): self.tzname = tzname self.tz = zoneinfo.ZoneInfo(tzname) self.start_month = start_month def page_header(self, canvas, doc): pass def page_footer(self, canvas, doc): from reportlab.lib.units import mm canvas.setFont("OpenSans", 8) canvas.drawString(15 * mm, 10 * mm, "Page %d" % doc.page) canvas.drawRightString( self.pagesize[0] - doc.rightMargin, 10 * mm, "Created: %s" % date_format(now().astimezone(self.tz), "SHORT_DATETIME_FORMAT"), ) def render(self): return "sysreport.pdf", "application/pdf", self.create({}) def get_story(self, doc, form_data): headlinestyle = self.get_style() headlinestyle.fontSize = 15 subheadlinestyle = self.get_style() subheadlinestyle.fontSize = 13 style_small = self.get_style() style_small.fontSize = 6 story = [ Paragraph("System report", headlinestyle), Spacer(1, 5 * mm), Paragraph("Usage", subheadlinestyle), Spacer(1, 5 * mm), self._usage_table(), Spacer(1, 5 * mm), Paragraph("Installed versions", subheadlinestyle), Spacer(1, 5 * mm), self._tech_table(), Spacer(1, 5 * mm), Paragraph("Plugins", subheadlinestyle), Spacer(1, 5 * mm), Paragraph(self._get_plugin_versions(), style_small), Spacer(1, 5 * mm), Paragraph("Custom templates", subheadlinestyle), Spacer(1, 5 * mm), Paragraph(self._get_custom_templates(), style_small), Spacer(1, 5 * mm), ] return story def _tech_table(self): style = self.get_style() style.fontSize = 8 style_small = self.get_style() style_small.fontSize = 6 w = self.pagesize[0] - 30 * mm colwidths = [ a * w for a in ( 0.2, 0.8, ) ] tstyledata = [ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (0, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0), ] tdata = [ [Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)], [Paragraph("pretix version:", style), Paragraph(__version__, style)], [Paragraph("Python version:", style), Paragraph(sys.version, style)], [Paragraph("Platform:", style), Paragraph(platform.platform(), style)], [ Paragraph("Database engine:", style), Paragraph(settings.DATABASES["default"]["ENGINE"], style), ], ] table = Table(tdata, colWidths=colwidths, repeatRows=0) table.setStyle(TableStyle(tstyledata)) return table def _usage_table(self): style = self.get_style() style.fontSize = 8 style_small = self.get_style() style_small.fontSize = 6 style_small.leading = 8 style_small.alignment = TA_CENTER style_small_head = self.get_style() style_small_head.fontSize = 6 style_small_head.leading = 8 style_small_head.alignment = TA_CENTER style_small_head.fontName = "OpenSansBd" w = self.pagesize[0] - 30 * mm successful = ( Q(status=Order.STATUS_PAID) | Q(valid_if_pending=True, status=Order.STATUS_PENDING) | Q( Exists( OrderPayment.objects.filter( order_id=OuterRef("pk"), state__in=( OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED, ), ) ), ) ) orders_q = Order.objects.filter( successful, testmode=False, ) orders_testmode_q = Order.objects.filter( testmode=True, ) orders_unconfirmed_q = Order.objects.filter( ~successful, testmode=False, ) revenue_q = Transaction.objects.filter( Exists( OrderPayment.objects.filter( order_id=OuterRef("order_id"), state__in=( OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED, ), ) ), order__testmode=False, ) currencies = sorted( list( set( Transaction.objects.annotate(c=F("order__event__currency")) .values_list("c", flat=True) .distinct() ) ) ) year_first = orders_q.aggregate(m=Min("datetime__year"))["m"] if not year_first: year_first = now().year elif datetime.now().month - 1 <= self.start_month: year_first -= 1 year_last = now().year tdata = [ [ Paragraph(l, style_small_head) for l in ( "Time frame", "Currency", "Successful orders", "Net revenue", "Testmode orders", "Unsucessful orders", "Positions", "Gross revenue", ) ] ] for year in range(year_first, year_last + 1): for i, c in enumerate(currencies): first_day = datetime( year, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz ) after_day = datetime( year + 1, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz ) orders_count = ( orders_q.filter( datetime__gte=first_day, datetime__lt=after_day ).aggregate(c=Count("*"))["c"] or 0 ) testmode_count = ( orders_testmode_q.filter( datetime__gte=first_day, datetime__lt=after_day ).aggregate(c=Count("*"))["c"] or 0 ) unconfirmed_count = ( orders_unconfirmed_q.filter( datetime__gte=first_day, datetime__lt=after_day ).aggregate(c=Count("*"))["c"] or 0 ) revenue_data = revenue_q.filter( datetime__gte=first_day, datetime__lt=after_day, order__event__currency=c ).aggregate( c=Sum("count"), s_net=Sum(F("price") - F("tax_value")), s_gross=Sum(F("price")), ) tdata.append( ( Paragraph( date_format(first_day, "M Y") + " – " + date_format(after_day - timedelta(days=1), "M Y"), style_small, ), Paragraph(c, style_small), Paragraph(str(orders_count), style_small) if i == 0 else "", Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small), Paragraph(str(testmode_count), style_small) if i == 0 else "", Paragraph(str(unconfirmed_count), style_small) if i == 0 else "", Paragraph(str(revenue_data.get("c") or 0), style_small), Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small), ) ) colwidths = [a * w for a in (0.18,) + (0.82 / 7,) * 7] tstyledata = [ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (0, -1), 0), ("RIGHTPADDING", (-1, 0), (-1, -1), 0), ("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 1), ] table = Table(tdata, colWidths=colwidths, repeatRows=0) table.setStyle(TableStyle(tstyledata)) return table def _get_plugin_versions(self): lines = [] for p in get_all_plugins(): lines.append(f"{p.name} {p.version}") return ", ".join(lines) def _get_custom_templates(self): lines = [] for dirpath, dirnames, filenames in os.walk( os.path.join(DATA_DIR, "templates") ): for f in filenames: lines.append(f"{dirpath}/{f}") d = "
".join(lines[:50]) if len(lines) > 50: d += "
..." if not d: return "–" return d