from datetime import timedelta from django.template.defaultfilters import floatformat from django.utils.html import format_html from django.utils.safestring import SafeString import django_tables2 as tables from .models import EventAttendee, EventExt, Member class DurationColumn(tables.Column): def render(self, value: timedelta): if value is None: return None return floatformat(value.total_seconds() / 60 / 60, -2) def value(self, value: timedelta): if value is None: return None return value.total_seconds() / 60 / 60 class MoneyColumn(tables.columns.Column): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attrs["cell"] = {"class": "text-end", **self.attrs.get("cell", {})} def render(self, value) -> str: return f"{super().render(value):.2f}" def value(self, **kwargs): return kwargs["value"] class CurrencySymbolMoneyColumn(MoneyColumn): def render(self, value) -> str: return f"${super().render(value)}" class EventTable(tables.Table): title = tables.TemplateColumn( template_code=( '{{ value }} ' ' ' ' ' ' ' ), accessor="unescaped_title", ) occurred = tables.BooleanColumn(visible=False) start = tables.DateColumn("N d, Y") duration = DurationColumn() person_hours = DurationColumn() meetings = tables.Column() gross_revenue = MoneyColumn() total_due_to_instructor = MoneyColumn() net_revenue = MoneyColumn() invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted") invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid") class Meta: model = EventExt fields = ( "title", "occurred", "start", "instructor", "category", "count", "cap", "meetings", "duration", "person_hours", "gross_revenue", "total_due_to_instructor", "net_revenue", ) row_attrs = { "class": lambda record: ( "" if record.occurred else "text-decoration-line-through table-danger" ) } class EventRegistrationsTable(tables.Table): total_ticket_count = tables.Column(empty_values=()) non_member_ticket_count = tables.Column("Non-Member ticket count", empty_values=()) name = tables.Column(accessor="Full name") email = tables.EmailColumn(accessor="Email") phone = tables.Column(accessor="Phone") emergency_contact_name = tables.Column(accessor="Emergency Contact Name:") emergency_contact_phone_number = tables.Column( accessor="Emergency Contact Phone Number:" ) emergency_contact_relation = tables.Column(accessor="Emergency Contact Relation:") def render_total_ticket_count(self, record): return sum(int(v) for k, v in record.items() if k.startswith("Ticket: ")) def render_non_member_ticket_count(self, record): # TODO: this is somewhat brittle return sum(int(v) for k, v in record.items() if k == "Ticket: CMS Non-Members") class Meta: row_attrs = { "class": lambda table, record: ( "" if table.render_total_ticket_count(record) > 0 else "text-decoration-line-through table-danger" ) } class EventSummaryTable(tables.Table): event_count = tables.Column("Events") canceled_event_count = tables.Column("Canceled Events") count__sum = tables.Column("Tickets") instructor__count = tables.Column("Unique Instructors") meetings__sum = tables.Column("Meetings") duration__sum = DurationColumn("Class Hours") person_hours__sum = DurationColumn("Person Hours") gross_revenue__sum = MoneyColumn("Gross Revenue") total_due_to_instructor__sum = MoneyColumn("Total Due to Instructor") net_revenue__sum = MoneyColumn("Net Revenue") class UserEventTable(EventTable): title = tables.Column(linkify=True, accessor="unescaped_title") instructor = None person_hours = None gross_revenue = None net_revenue = None class CurrentAndUpcomingEventTable(EventTable): next_meeting = tables.DateTimeColumn("N d, Y H:i", accessor="next_meeting_start") person_hours = None total_due_to_instructor = None invoice__date_submitted = None invoice__date_paid = None gross_revenue = None net_revenue = None class Meta(EventTable.Meta): row_attrs = { "class": lambda table, record: ( "" if record.cap is None or record.cap > 0 else "text-decoration-line-through table-danger" ) } sequence = ("title", "start", "next_meeting") class MoneyFooterColumn(MoneyColumn): def render_footer(self, bound_column, table): value = getattr(table.event, bound_column.accessor) if value is not None: return f"${value:.2f}" else: return bound_column.default class InvoiceTable(tables.Table): def __init__(self, *args, **kwargs): self.event = kwargs.pop("event") super().__init__(*args, **kwargs) @staticmethod def _math_header(name: str, formula: str) -> SafeString: return format_html( '{}
[{}]
', name, formula, ) label = tables.Column("Ticket Type", footer="Subtotals") list_price = CurrencySymbolMoneyColumn("Ticket Price") actual_price = CurrencySymbolMoneyColumn(_math_header("Actual Price", "P")) quantity = tables.Column( _math_header("Quantity", "Q"), footer=lambda table: table.event.quantity, ) amount = CurrencySymbolMoneyColumn(_math_header("Amount", "A=P*Q")) materials = CurrencySymbolMoneyColumn( _math_header("CMS Collected Materials Fee", "M=m*Q") ) amount_without_materials = CurrencySymbolMoneyColumn( _math_header("Event Revenue Base", "B=A-M") ) instructor_revenue = CurrencySymbolMoneyColumn( _math_header("Instructor Percentage Revenue", "R=B*I"), ) instructor_amount = CurrencySymbolMoneyColumn( _math_header("Amount Due to Instructor", "R+M") ) class Meta: attrs = { "class": "table table-sm mx-auto w-auto", "tbody": {"class": "table-group-divider"}, "tfoot": {"class": "table-group-divider"}, } orderable = False class EventAttendeeTable(tables.Table): class Meta: model = EventAttendee fields = ("name", "email") class MissingPaperworkTable(tables.Table): policy_agreement = tables.BooleanColumn() authorize_charge = tables.BooleanColumn() class Meta: model = Member fields = [ "first_name", "last_name", "membership", "billing_method", "join_date", "membership_agreement_signed_and_on_file_date", "waiver_form_signed_and_on_file_date", "policy_agreement", "authorize_charge", ]