Source code for club_sessions.views.reports

import csv
import json

import math
import xlsxwriter
from decimal import Decimal

from django.contrib.auth.decorators import login_required
from django.db.models import Sum, Count
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.utils.html import strip_tags

from accounts.models import User
from club_sessions.views.core import (
    PLAYING_DIRECTOR,
    SITOUT,
    VISITOR,
    load_session_entry_static,
)
from club_sessions.views.decorators import user_is_club_director
from club_sessions.models import Session, SessionEntry, SessionMiscPayment
from cobalt.settings import GLOBAL_ORG, COBALT_HOSTNAME, GLOBAL_TITLE
from cobalt.version import COBALT_VERSION
from payments.models import MemberTransaction, OrgPaymentMethod
from rbac.core import rbac_user_has_role
from rbac.views import rbac_forbidden
from utils.views.xls import XLSXFormat, XLSXStyles


def _build_empty_report_data_structure(session_fees):
    """Build the empty data structure for the report table"""

    summary_table = {}

    # Now build the other rows
    for membership_type in session_fees:

        summary_table[membership_type] = {}

        for payment_method in session_fees[membership_type]:
            summary_table[membership_type][payment_method] = {}
            # Handle membership and payment e.g. Guest Cash
            summary_table[membership_type][payment_method][
                "default_fee"
            ] = session_fees[membership_type][payment_method]
            summary_table[membership_type][payment_method]["fee"] = Decimal(0.0)
            summary_table[membership_type][payment_method]["paid"] = Decimal(0.0)

        # Also create a total for the row, e.g. total for Guests for all payment types
        summary_table[membership_type]["row_total"] = {}
        summary_table[membership_type]["row_total"]["fee"] = Decimal(0.0)
        summary_table[membership_type]["row_total"]["paid"] = Decimal(0.0)

    # Add totals at the bottom - again use Guest as our reference row
    summary_table["Totals"] = {}
    for payment_method in session_fees["Guest"]:
        summary_table["Totals"][payment_method] = {}
        summary_table["Totals"][payment_method]["fee"] = Decimal(0.0)
        summary_table["Totals"][payment_method]["paid"] = Decimal(0.0)

    # Grand totals
    summary_table["Totals"]["row_total"] = {}
    summary_table["Totals"]["row_total"]["paid"] = Decimal(0.0)
    summary_table["Totals"]["row_total"]["fee"] = Decimal(0.0)

    return summary_table


def _add_data_to_report_data_structure(
    summary_table, session_entries, membership_type_dict
):
    """Add in the data to the empty summary_table from the session_entries"""

    for session_entry in session_entries:

        # Skip sit outs and directors
        if session_entry.system_number not in [PLAYING_DIRECTOR, SITOUT]:
            membership_type = membership_type_dict.get(
                session_entry.system_number, "Guest"
            )
            payment_method = session_entry.payment_method.payment_method

            # This cell
            if session_entry.is_paid:
                summary_table[membership_type][payment_method][
                    "paid"
                ] += session_entry.fee
            summary_table[membership_type][payment_method]["fee"] += session_entry.fee

            # Row totals
            if session_entry.is_paid:
                summary_table[membership_type]["row_total"]["paid"] += session_entry.fee
            summary_table[membership_type]["row_total"]["fee"] += session_entry.fee

            # Column totals
            if session_entry.is_paid:
                summary_table["Totals"][payment_method]["paid"] += session_entry.fee
            summary_table["Totals"][payment_method]["fee"] += session_entry.fee

            # Grand total
            if session_entry.is_paid:
                summary_table["Totals"]["row_total"]["paid"] += session_entry.fee
            summary_table["Totals"]["row_total"]["fee"] += session_entry.fee

    return summary_table


def _mark_rows_with_data_in_report_data_structure(summary_table):
    """We want to show if a row has data or not to remove clutter (blank rows)"""

    # Does this table have any data?
    any_data = False

    row_has_data = {}

    for membership_type in summary_table:
        row_has_data[membership_type] = False
        for payment_method in summary_table[membership_type]:

            if (
                summary_table[membership_type][payment_method]["fee"] != 0
                or summary_table[membership_type][payment_method]["paid"] != 0
            ):
                row_has_data[membership_type] = True
                any_data = True

    return any_data, row_has_data


def _mark_columns_with_data_in_report_data_structure(summary_table):
    """We want to show if a column has data or not to remove clutter (blank payment type columns)"""

    column_has_data = {}

    # Go through data
    for membership_type in summary_table:
        for payment_method in summary_table[membership_type]:

            # Add key if not set yet
            if payment_method not in column_has_data:
                column_has_data[payment_method] = False

            # If we have data already, move on
            if column_has_data[payment_method]:
                continue

            # Check for data
            if (
                summary_table[membership_type][payment_method]["fee"] != 0
                or summary_table[membership_type][payment_method]["paid"] != 0
            ):
                column_has_data[payment_method] = True

    return column_has_data


@user_is_club_director()
def reconciliation_htmx(request, club, session):
    """Basic report of a session"""

    # Load starting data
    (
        session_entries,
        mixed_dict,
        session_fees,
        membership_type_dict,
    ) = load_session_entry_static(session, club)

    # We want the column names so use the Guest row which is always present
    # This will give us e.g. ['Bank Transfer', 'Bridge Credits', 'Cash', 'IOU']
    column_headings = sorted(session_fees["Guest"])

    # Build summary around session_fees - start by building structure
    summary_table = _build_empty_report_data_structure(session_fees)

    # Add the session data in
    summary_table = _add_data_to_report_data_structure(
        summary_table, session_entries, membership_type_dict
    )

    # Go through and mark which rows/columns have data
    _, row_has_data = _mark_rows_with_data_in_report_data_structure(summary_table)
    column_has_data = _mark_columns_with_data_in_report_data_structure(summary_table)

    # We need to sort the entries in the summary table by payment method, the same as the headers
    new_table = {}
    for row in summary_table:
        new_row = dict(sorted(summary_table[row].items()))
        new_table[row] = new_row
    summary_table = new_table

    # See if user wants to see blank stuff in the report
    show_blanks = bool(request.POST.get("show_blanks", False))

    (
        extras_summary_table,
        extras_any_data,
        extras_row_has_data,
        extras_column_has_data,
    ) = _reconciliation_extras(session, column_headings)

    return render(
        request,
        "club_sessions/reports/reconciliation.html",
        {
            "club": club,
            "session": session,
            "column_headings": column_headings,
            "summary_table": summary_table,
            "row_has_data": row_has_data,
            "column_has_data": column_has_data,
            "extras_summary_table": extras_summary_table,
            "extras_row_has_data": extras_row_has_data,
            "extras_column_has_data": extras_column_has_data,
            "extras_any_data": extras_any_data,
            "show_blanks": show_blanks,
        },
    )


def _reconciliation_extras(session, column_headings):
    """Get summarised view of extras for a session

    We use the same column headings as the table fees table - payment types

    For the rows we use the description

    """

    extras_summary_table = {}

    # Get data
    extras = (
        SessionMiscPayment.objects.filter(session_entry__session=session)
        .order_by("description", "payment_method__payment_method")
        .select_related("payment_method")
    )

    # Build table structure first
    for extra in extras:
        if extra.description not in extras_summary_table:
            extras_summary_table[extra.description] = {}
            for column_heading in column_headings:
                extras_summary_table[extra.description][column_heading] = {
                    "fee": Decimal(0),
                    "paid": Decimal(0),
                }

            # Also create a total for the row, e.g. total for Coffee for all payment types
            extras_summary_table[extra.description]["row_total"] = {
                "fee": Decimal(0),
                "paid": Decimal(0),
            }

    # Add totals at the bottom
    extras_summary_table["Totals"] = {}
    for column_heading in column_headings:
        extras_summary_table["Totals"][column_heading] = {
            "fee": Decimal(0),
            "paid": Decimal(0),
        }

    # Grand totals
    extras_summary_table["Totals"]["row_total"] = {
        "fee": Decimal(0),
        "paid": Decimal(0),
    }

    # Now fill in data
    for extra in extras:
        extras_summary_table[extra.description][extra.payment_method.payment_method][
            "fee"
        ] += extra.amount
        if extra.payment_made:
            extras_summary_table[extra.description][
                extra.payment_method.payment_method
            ]["paid"] += extra.amount

        # Row totals
        if extra.payment_made:
            extras_summary_table[extra.description]["row_total"]["paid"] += extra.amount
        extras_summary_table[extra.description]["row_total"]["fee"] += extra.amount

        # Column totals
        if extra.payment_made:
            extras_summary_table["Totals"][extra.payment_method.payment_method][
                "paid"
            ] += extra.amount
        extras_summary_table["Totals"][extra.payment_method.payment_method][
            "fee"
        ] += extra.amount

        # Grand total
        if extra.payment_made:
            extras_summary_table["Totals"]["row_total"]["paid"] += extra.amount
        extras_summary_table["Totals"]["row_total"]["fee"] += extra.amount

    # Go through and mark which rows/columns have data
    any_data, extras_row_has_data = _mark_rows_with_data_in_report_data_structure(
        extras_summary_table
    )
    extras_column_has_data = _mark_columns_with_data_in_report_data_structure(
        extras_summary_table
    )

    return extras_summary_table, any_data, extras_row_has_data, extras_column_has_data


def _get_name_for_csv(session_entry, mixed_dict):
    """helper to get the name to use in the csv"""

    if session_entry.system_number == PLAYING_DIRECTOR:
        return "Playing Director"
    elif session_entry.system_number == SITOUT:
        return "Sitout"
    elif session_entry.system_number == VISITOR:
        return session_entry.player_name_from_file
    else:
        match = mixed_dict.get(session_entry.system_number)
        return (
            match.get("value").full_name
            if match
            else session_entry.player_name_from_file
        )


[docs] @login_required() def csv_download(request, session_id): """Download CSV of player details from a session""" session = get_object_or_404(Session, pk=session_id) club = session.session_type.organisation # Check access club_role = f"club_sessions.sessions.{club.id}.edit" if not rbac_user_has_role(request.user, club_role): return rbac_forbidden(request, club_role) # Get dictionaries ( session_entries, mixed_dict, session_fees, membership_type_dict, ) = load_session_entry_static(session, club) # Manipulate the data for the report for session_entry in session_entries: # Payment method if session_entry.payment_method: session_entry.payment_method_display = ( session_entry.payment_method.payment_method ) else: session_entry.payment_method_display = "" # Fudge the session entries for non-players to show as free if no payment taken if ( session_entry.system_number in [PLAYING_DIRECTOR, SITOUT] and session_entry.fee == 0 ): session_entry.payment_method_display = "Free" # Get extras extras = SessionMiscPayment.objects.filter( session_entry__session=session ).select_related("session_entry") # Create CSV response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="{session}.csv"' # the csv writer writer = csv.writer(response) writer.writerow([club.name, f"Downloaded by {request.user.full_name}"]) # basic session details writer.writerow([]) if session.status == Session.SessionStatus.COMPLETE: writer.writerow(["Status", "Complete"]) else: writer.writerow(["Status", "Incomplete"]) writer.writerow(["Director", session.director]) writer.writerow(["Description", session.description]) writer.writerow(["Session Date", session.session_date]) writer.writerow(["Session Type", session.session_type.name]) writer.writerow(["Time of Day", session.time_of_day]) if session.venue: writer.writerow(["Venue", session.venue]) writer.writerow([]) # notes if we have any if session.director_notes: writer.writerow(["Director Notes", session.director_notes]) writer.writerow([]) # Write a first row with header information field_names = [ "Session", "Date", "Name", f"{GLOBAL_ORG} Number", "Pair Team Number", "Seat", "Payment Method", "Fee", "Processed", ] writer.writerow(field_names) # Write data rows for session_entry in session_entries: # Payment status is_paid = "Yes" if session_entry.is_paid else "No" # Don't show values for free players if session_entry.payment_method_display == "Free": is_paid = "" session_entry.fee = "" values = [ session.description, session.session_date, _get_name_for_csv(session_entry, mixed_dict), session_entry.system_number, session_entry.pair_team_number, session_entry.seat, session_entry.payment_method_display, session_entry.fee, is_paid, ] writer.writerow(values) # Extras if extras: writer.writerow([]) writer.writerow(["Extras"]) writer.writerow([]) # Write a first row with header information field_names = [ "Session", "Date", "Name", f"{GLOBAL_ORG} Number", "Description", "", "Payment Method", "Fee", "Processed", ] writer.writerow(field_names) # Write data rows for extra in extras: payment_made = "Yes" if extra.payment_made else "No" values = [ extra.session_entry.session.description, session.session_date, _get_name_for_csv(extra.session_entry, mixed_dict), extra.session_entry.system_number, extra.description, "", extra.payment_method.payment_method, extra.amount, payment_made, ] writer.writerow(values) # Now do payment method summaries payment_methods, extras = payment_method_summary(session) writer.writerow([]) writer.writerow(["Payment Methods Summary"]) writer.writerow([]) # Write a first row with header information field_names = [ "Payment Method", "Players Paid", "Players Un-Paid", "Amount Paid", "Amount Un-Paid", ] writer.writerow(field_names) for payment_method in payment_methods: payment_method_display = payment_method or "Free" values = [ payment_method_display, payment_methods[payment_method]["paid"]["count"], payment_methods[payment_method]["unpaid"]["count"], payment_methods[payment_method]["paid"]["total"], payment_methods[payment_method]["unpaid"]["total"], ] writer.writerow(values) if extras: writer.writerow([]) writer.writerow(["Extras Summary"]) writer.writerow([]) # Write a first row with header information field_names = [ "Payment Method", "Number Paid", "Number Un-Paid", "Amount Paid", "Amount Un-Paid", ] writer.writerow(field_names) for extra in extras: values = [ extra, extras[extra]["paid"]["count"], extras[extra]["unpaid"]["count"], extras[extra]["paid"]["total"], extras[extra]["unpaid"]["total"], ] writer.writerow(values) return response
[docs] @login_required() def xlsx_download(request, session_id): """Download XLS File of player details from a session""" # Get session and club data session = get_object_or_404(Session, pk=session_id) club = session.session_type.organisation # Check access club_role = f"club_sessions.sessions.{club.id}.edit" if not rbac_user_has_role(request.user, club_role): return rbac_forbidden(request, club_role) # Get the data session_entries, extras, mixed_dict, membership_type_dict = _xlsx_download_get_data( session, club ) # Create HttpResponse to put the Excel file into response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) response[ "Content-Disposition" ] = f'attachment; filename="{session.description}.xlsx"' # Create an Excel file and add worksheets workbook = xlsxwriter.Workbook(response) summary_sheet = workbook.add_worksheet("Summary") details_sheet = workbook.add_worksheet("Table Fees") extras_sheet = workbook.add_worksheet("Extras") if extras else None # Create styles formats = XLSXStyles(workbook) # Do the headers and basic info details_row, extras_row, summary_row = _xlsx_download_basic_structure( workbook, formats, summary_sheet, details_sheet, extras_sheet, session, club, request, extras, ) # fill in the summary tab _xlsx_download_summary(session, summary_sheet, workbook, formats, summary_row) # fill in the details tab _xlsx_download_details( mixed_dict, membership_type_dict, workbook, formats, details_sheet, session_entries, extras, details_row, ) # fill in the extras tab if we have any if extras: _xlsx_download_details_extras( extras, workbook, formats, extras_sheet, extras_row, mixed_dict ) workbook.close() return response
def _xlsx_download_get_data(session, club): """sub of xlsx download. Loads and formats data""" # Get dictionaries ( session_entries, mixed_dict, session_fees, membership_type_dict, ) = load_session_entry_static(session, club) # Manipulate the data for the report for session_entry in session_entries: # Payment method if session_entry.payment_method: session_entry.payment_method_display = ( session_entry.payment_method.payment_method ) else: session_entry.payment_method_display = "" # Fudge the session entries for non-players to show as free if no payment taken if ( session_entry.system_number in [PLAYING_DIRECTOR, SITOUT] and session_entry.fee == 0 ): session_entry.payment_method_display = "Free" # Get extras extras = SessionMiscPayment.objects.filter( session_entry__session=session ).select_related("session_entry") return session_entries, extras, mixed_dict, membership_type_dict def _xlsx_download_basic_structure( workbook, formats, summary_sheet, details_sheet, extras_sheet, session, club, request, extras, ): """add basic structure to tabs This puts in the headers etc on the tabs. It doesn't do any of the data. Extras and extras_sheet may be None in which case we don't process them """ # same headers on both/all tabs sheet_list = [summary_sheet, details_sheet] if extras: sheet_list.append(extras_sheet) # How wide for the title title_width = {summary_sheet: 4, details_sheet: 7, extras_sheet: 5} for sheet in sheet_list: # Put cursor away from title sheet.set_selection(6, 0, 6, 0) # Title sheet.merge_range(0, 0, 3, title_width[sheet], club.name, formats.h1) sheet.merge_range( 4, 0, 4, title_width[sheet], f"{session.description} on {session.session_date:%A %d %B %Y}", formats.h2, ) sheet.merge_range( 5, 0, 5, title_width[sheet], f"Downloaded by {request.user.full_name}", formats.h3, ) # Buffer sheet.merge_range(6, 0, 6, title_width[sheet], "") # Tell them about the other tab summary_sheet.write( 7, 3, "You can change to the Table Fees tab below to see a list of the transactions", formats.info, ) # Buttons # summary_sheet.insert_button(8,3,{'macro': 'say_hello', 'caption': 'Press Me'}) url = reverse("club_sessions:manage_session", kwargs={"session_id": session.id}) summary_sheet.write_url( 9, 3, f"https://{COBALT_HOSTNAME}{url}", formats.link, string="Click to Open Session", ) # Session details summary_sheet.merge_range(7, 0, 8, 1, "Session Details", formats.summary_heading) summary_sheet.set_column("A:B", 30) # Status summary_sheet.write(9, 0, "Status", formats.summary_row_title) if session.status == Session.SessionStatus.COMPLETE: summary_sheet.write(9, 1, "Complete", formats.summary_row_data) else: summary_sheet.write(9, 1, "Incomplete", formats.summary_row_data) # Director summary_sheet.write(10, 0, "Director", formats.summary_row_title) summary_sheet.write(10, 1, f"{session.director}", formats.summary_row_data) # Description summary_sheet.write(11, 0, "Description", formats.summary_row_title) summary_sheet.write(11, 1, f"{session.description}", formats.summary_row_data) # Date summary_sheet.write(12, 0, "Session Date", formats.summary_row_title) summary_sheet.write( 12, 1, f"{session.session_date:%A %d %B %Y}", formats.summary_row_data ) # Session Type summary_sheet.write(13, 0, "Session Type", formats.summary_row_title) summary_sheet.write(13, 1, f"{session.session_type.name}", formats.summary_row_data) # Time of Day summary_sheet.write(14, 0, "Time of Day", formats.summary_row_title) summary_sheet.write(14, 1, f"{session.time_of_day}", formats.summary_row_data) # Now we need to use row numbers summary_row_no = 15 # Venue if session.venue: summary_sheet.write(summary_row_no, 0, "Venue", formats.summary_row_title) summary_sheet.write( summary_row_no, 1, f"{session.venue}", formats.summary_row_data ) summary_row_no += 1 # Buffer summary_sheet.merge_range(summary_row_no, 0, summary_row_no, 4, "") summary_row_no += 1 # Notes if session.director_notes: summary_sheet.write( summary_row_no, 0, "Director Notes", formats.summary_row_title ) notes = strip_tags(session.director_notes) summary_sheet.merge_range( summary_row_no, 1, summary_row_no, 2, notes, formats.director_notes ) # Set height - we get about 70 characters per row and 25 whatevers per row height rows = math.ceil(len(notes) / 70) summary_sheet.set_row(summary_row_no, rows * 25) summary_row_no += 1 # Now do headings on detail sheet details_sheet.merge_range(7, 0, 7, 7, "Table Fees", formats.section) details_sheet.write(8, 0, "Table Number", formats.detail_row_title) details_sheet.set_column("A:A", 35) details_sheet.write(8, 1, "Seat", formats.detail_row_title) details_sheet.set_column("B:B", 15) details_sheet.write(8, 2, "Player Name", formats.detail_row_title) details_sheet.set_column("C:C", 30) details_sheet.write(8, 3, f"{GLOBAL_ORG} Number", formats.detail_row_title) details_sheet.set_column("D:D", 25) details_sheet.write(8, 4, "Membership", formats.detail_row_title) details_sheet.set_column("E:E", 25) details_sheet.write(8, 5, "Payment Method", formats.detail_row_title) details_sheet.set_column("F:F", 30) details_sheet.write(8, 6, "Fee", formats.detail_row_title_number) details_sheet.set_column("G:G", 15) details_sheet.write(8, 7, "Processed", formats.detail_row_title_number) details_sheet.set_column("H:H", 15) # now do headings on extras sheet if we have one if extras: extras_sheet.merge_range(7, 0, 7, 5, "Extras", formats.section) extras_sheet.write(8, 0, "Player Name", formats.detail_row_title) extras_sheet.set_column("A:A", 30) extras_sheet.write(8, 1, f"{GLOBAL_ORG} Number", formats.detail_row_title) extras_sheet.set_column("B:B", 25) extras_sheet.write(8, 2, "Description", formats.detail_row_title) extras_sheet.set_column("C:C", 50) extras_sheet.write(8, 3, "Payment Method", formats.detail_row_title) extras_sheet.set_column("D:D", 30) extras_sheet.write(8, 4, "Fee", formats.detail_row_title_number) extras_sheet.set_column("E:E", 15) extras_sheet.write(8, 5, "Processed", formats.detail_row_title_number) extras_sheet.set_column("F:F", 15) # Return current row return 9, 9, summary_row_no def _xlsx_download_summary(session, summary_sheet, workbook, formats, summary_row_no): """fill in the summary data for the summary tab""" # Get summary info payment_methods, extras = payment_method_summary(session) # Print payment_methods summary_row_no = _xlsx_download_summary_sub( "Payment Methods - Table Fees", payment_methods, summary_sheet, workbook, formats, summary_row_no, ) # Print extras if we have any if extras: # Buffer summary_sheet.merge_range(summary_row_no, 0, summary_row_no, 4, "") summary_row_no = _xlsx_download_summary_sub( "Payment Methods - Extras", extras, summary_sheet, workbook, formats, summary_row_no, ) # Insert image and attribution summary_sheet.insert_image( summary_row_no + 2, 0, "cobalt/static/assets/img/abftechlogo.png", {"x_scale": 0.17, "y_scale": 0.17}, ) summary_sheet.write( summary_row_no + 8, 0, f"{GLOBAL_TITLE} Version:{COBALT_VERSION}", formats.attribution, ) def _xlsx_download_summary_sub( heading, payment_types, summary_sheet, workbook, formats, summary_row_no ): """fill in the summary data for the summary tab - subroutine Called with either payment_methods or extras as payment_types - both the same format of data """ # Write headers summary_row_no += 1 summary_sheet.merge_range( summary_row_no, 0, summary_row_no, 4, heading, formats.section ) summary_row_no += 1 summary_sheet.write(summary_row_no, 0, "Payment Method", formats.detail_row_title) summary_sheet.set_column("A:A", 30) summary_sheet.write( summary_row_no, 1, "Players Paid", formats.detail_row_title_number ) summary_sheet.set_column("B:B", 40) summary_sheet.write( summary_row_no, 2, "Players Unpaid", formats.detail_row_title_number ) summary_sheet.set_column("C:C", 40) summary_sheet.write( summary_row_no, 3, "Amount Paid", formats.detail_row_title_number ) summary_sheet.set_column("D:D", 40) summary_sheet.write( summary_row_no, 4, "Amount Unpaid", formats.detail_row_title_number ) summary_sheet.set_column("E:E", 40) summary_row_no += 1 for payment_type in payment_types: payment_method_display = payment_type or "Free" summary_sheet.write( summary_row_no, 0, payment_method_display, formats.detail_row_data ) summary_sheet.write( summary_row_no, 1, payment_types[payment_type]["paid"]["count"], formats.detail_row_number, ) summary_sheet.write( summary_row_no, 2, payment_types[payment_type]["unpaid"]["count"], formats.detail_row_number, ) summary_sheet.write( summary_row_no, 3, payment_types[payment_type]["paid"]["total"], formats.detail_row_money, ) summary_sheet.write( summary_row_no, 4, payment_types[payment_type]["unpaid"]["total"], formats.detail_row_money, ) summary_row_no += 1 return summary_row_no def _xlsx_download_details( mixed_dict, membership_type_dict, workbook, formats, details_sheet, session_entries, extras, details_row, ): """produce the session_entry and extras details for the detail tab""" # Write data rows for session_entry in session_entries: # Payment status is_paid = "Yes" if session_entry.is_paid else "No" # Default format - override if free format_type = formats.detail_row_data format_type_money = formats.detail_row_money # Don't show values for free players if session_entry.payment_method_display == "Free": is_paid = "" session_entry.fee = "" format_type = formats.detail_row_free format_type_money = formats.detail_row_free details_sheet.write(details_row, 0, session_entry.pair_team_number, format_type) details_sheet.write(details_row, 1, session_entry.seat, format_type) details_sheet.write( details_row, 2, _get_name_for_csv(session_entry, mixed_dict).__str__(), format_type, ) details_sheet.write(details_row, 3, session_entry.system_number, format_type) # Get membership type for members membership_type = membership_type_dict.get(session_entry.system_number) # If we have no membership type, then it will be a Guest or director or phantom if not membership_type: if session_entry.system_number in [PLAYING_DIRECTOR, SITOUT]: membership_type = "" else: membership_type = "Guest" details_sheet.write(details_row, 4, membership_type, format_type) details_sheet.write( details_row, 5, session_entry.payment_method_display, format_type ) details_sheet.write(details_row, 6, session_entry.fee, format_type_money) details_sheet.write(details_row, 7, is_paid, format_type_money) details_row += 1 def _xlsx_download_details_extras( extras, workbook, formats, extras_sheet, extras_row, mixed_dict ): """show extras on the detail tab if we have any""" for extra in extras: payment_made = "Yes" if extra.payment_made else "No" extras_sheet.write( extras_row, 0, _get_name_for_csv(extra.session_entry, mixed_dict).__str__(), formats.detail_row_data, ) extras_sheet.write( extras_row, 1, extra.session_entry.system_number, formats.detail_row_data ) extras_sheet.write(extras_row, 2, extra.description, formats.detail_row_data) extras_sheet.write( extras_row, 3, extra.payment_method.payment_method, formats.detail_row_data ) extras_sheet.write(extras_row, 4, extra.amount, formats.detail_row_money) extras_sheet.write(extras_row, 5, payment_made, formats.detail_row_money) extras_row += 1 @user_is_club_director() def import_messages_htmx(request, club, session): """Show the messages generated when we imported the file""" if session.import_messages: messages = json.loads(session.import_messages) else: messages = None return render( request, "club_sessions/reports/import_messages_htmx.html", {"messages": messages}, ) @user_is_club_director() def low_balance_report_htmx(request, club, session): """Show low balances for people in session""" # Get all system numbers player_system_numbers = ( SessionEntry.objects.filter(session=session) .exclude(system_number__in=[PLAYING_DIRECTOR, SITOUT, VISITOR]) .values_list("system_number", flat=True) ) # Get last transaction for those players last_trans = ( MemberTransaction.objects.filter( member__system_number__in=player_system_numbers ) .order_by("member", "-pk") .distinct("member") .select_related("member") ) # Get players without transactions players_with_transactions = last_trans.values("member_id") players_without_transactions = User.objects.filter( system_number__in=player_system_numbers ).exclude(id__in=players_with_transactions) return render( request, "club_sessions/reports/low_balance_htmx.html", { "last_trans": last_trans, "players_without_transactions": players_without_transactions, }, )
[docs] def payment_method_summary_sub(payment_methods, payment_status_field): """sub of payment_method_summary to format the data for sessions and extras with all values including zeros for unpaid or paid amounts. The only difference between SessionEntry and SessionMiscPayment is the field name used to check if payment has been made. We accept this as a parameter. """ # Fill in missing bits payment_methods_display = {} # Set up structure for payment_method in payment_methods: item = payment_method["payment_method__payment_method"] if item not in payment_methods_display: payment_methods_display[item] = { "paid": {"total": Decimal(0), "count": 0}, "unpaid": {"total": Decimal(0), "count": 0}, } # Fill in data for payment_method in payment_methods: item = payment_method["payment_method__payment_method"] if payment_method[payment_status_field]: payment_methods_display[item]["paid"] = { "total": payment_method["total"], "count": payment_method["count"], } else: payment_methods_display[item]["unpaid"] = { "total": payment_method["total"], "count": payment_method["count"], } return payment_methods_display
[docs] def payment_method_summary(session): """Summarise the payment methods and extras - used for both online and CSV reporting We don't use a database query to summarise the sessions as we need to manipulate the non-players for the report. """ # Load the data session_entries = SessionEntry.objects.filter(session=session) # Go through and summarise payment_methods_display = {} for session_entry in session_entries: # Mark as Free if no payment method if session_entry.payment_method: payment_method = session_entry.payment_method.payment_method else: payment_method = "Free" # Also mark as Free for non-player and no fee set if ( session_entry.system_number in [ PLAYING_DIRECTOR, SITOUT, ] and session_entry.fee in [0, -99] ): payment_method = "Free" # Force to paid if Free session_entry.is_paid = True # Add to dict if not present if payment_method not in payment_methods_display: payment_methods_display[payment_method] = { "paid": {"total": Decimal(0), "count": 0}, "unpaid": {"total": Decimal(0), "count": 0}, } # Increment dict if session_entry.is_paid: payment_methods_display[payment_method]["paid"][ "total" ] += session_entry.fee payment_methods_display[payment_method]["paid"]["count"] += 1 else: payment_methods_display[payment_method]["unpaid"][ "total" ] += session_entry.fee payment_methods_display[payment_method]["unpaid"]["count"] += 1 # Get extras - this can use a query extras = ( SessionMiscPayment.objects.filter(session_entry__session=session) .values("payment_method__payment_method", "payment_made") .annotate(total=Sum("amount"), count=Count("pk")) .order_by("payment_method__payment_method") ) # format extras_display = payment_method_summary_sub(extras, "payment_made") return payment_methods_display, extras_display
@user_is_club_director() def payment_methods_htmx(request, club, session): """Show summary by payment methods""" payment_methods, extras = payment_method_summary(session) return render( request, "club_sessions/reports/payment_methods_htmx.html", {"payment_methods": payment_methods, "extras": extras}, )