cmsmanage/membershipworks/tables.py
Adam Goldsmith 56f49f8784
Some checks are pending
Ruff / ruff (push) Waiting to run
Test / test (push) Waiting to run
membershipworks: Use more consistent and readable format for money columns
2024-09-09 13:50:14 -04:00

231 lines
7.6 KiB
Python

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=(
'<a title="MembershipWorks" href="https://membershipworks.com/admin/#!event/admin/{{ record.url }}">{{ value }}</a> '
'<a title="Admin" href="{% url "admin:membershipworks_eventext_change" record.pk %}"><i class="bi bi-pencil-square"></i></a> '
'<a title="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
'<a title="Registrations" href="{% url "membershipworks:event-registrations" record.pk %}"><i class="bi bi-people"></i></a> '
),
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(
'{} <div class="text-nowrap font-monospace fw-light">[{}]</div>',
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",
]