Compare commits

..

No commits in common. "0ce441336f214db56035d185f61a4b2c51efa0c5" and "8c294625881ca3bbe32db30bc31d58aeb58d03a5" have entirely different histories.

23 changed files with 98 additions and 790 deletions

1
.gitignore vendored
View File

@ -2,4 +2,3 @@ __pycache__/
*.sqlite3
/__pypackages__/
/markdownx/
/media/

View File

@ -42,8 +42,6 @@ INSTALLED_APPS = [
"django_filters",
"django_db_views",
"django_mysql",
"django_sendfile",
"django_bootstrap5",
"tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig",
@ -120,10 +118,6 @@ LOGGING = {
},
}
MEDIA_ROOT = "media"
MEDIA_URL = "media/"
SENDFILE_ROOT = str(Path(__file__).parents[2] / "media" / "protected")
WIKI_URL = "https://wiki.claremontmakerspace.org"
# Django Rest Framework

View File

@ -21,5 +21,3 @@ MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa:
configure_hypothesis_profiles()
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
SENDFILE_BACKEND = "django_sendfile.backends.development"

View File

@ -48,6 +48,3 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
)
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
AUTH_LDAP_MIRROR_GROUPS = True
SENDFILE_BACKEND = "django_sendfile.backends.nginx"
SENDFILE_URL = "/media/protected"

View File

@ -15,7 +15,6 @@ Including another URLconf
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import include, path
@ -64,4 +63,3 @@ urlpatterns = [
if settings.DEBUG:
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -14,7 +14,6 @@ from .models import (
Event,
EventExt,
EventInstructor,
EventInvoice,
EventMeetingTime,
Flag,
Member,
@ -120,15 +119,11 @@ class EventInstructorAdmin(admin.ModelAdmin):
search_fields = ["name", "member__account_name"]
class EventInvoiceInline(admin.StackedInline):
model = EventInvoice
@admin.register(EventExt)
class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
inlines = [EventInvoiceInline, EventMeetingTimeInline]
inlines = [EventMeetingTimeInline]
list_display = [
"unescaped_title",
"title",
"start",
"duration",
"count",
@ -163,10 +158,6 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
fields.append("_details_timestamp")
return fields
@admin.display(ordering="title")
def unescaped_title(self, obj):
return obj.unescaped_title
@admin.display(ordering="duration")
def duration(self, obj):
return obj.duration

View File

@ -2,15 +2,12 @@ from django.urls import reverse
import dashboard
from dashboard import Link
from membershipworks.models import EventExt, Member
@dashboard.register
class MembershipworksDashboardFragment(dashboard.LinksCardDashboardFragment):
name = "MembershipWorks"
@property
def links(self):
links = [
Link(
"Upcoming Events",
@ -34,16 +31,3 @@ class MembershipworksDashboardFragment(dashboard.LinksCardDashboardFragment):
permission="membershipworks.view_member",
),
]
member = Member.from_user(self.request.user)
if (
member is not None
and EventExt.objects.filter(instructor__member=member).exists()
):
links.append(
Link(
"My Events", reverse("membershipworks:user-events"), permission=None
)
)
return links

View File

@ -1,38 +0,0 @@
from django import forms
from django.contrib.auth.models import AbstractBaseUser
from membershipworks.models import EventExt
class EventInvoiceForm(forms.Form):
reviewed_invoice = forms.BooleanField(
label="I have reviewed this invoice and confirm that the course information, materials fee, course fees, enrollment information, invoice amount and all other details are correct.",
help_text='Please contact us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a> if corrections are required.',
required=True,
)
verified_payment_and_contact = forms.BooleanField(
label="I have verified my contact and payment information.",
help_text='You can <a href="https://claremontmakerspace.org/membersonly/">change your payment information here</a>. It may take up to an hour for your changes to take effect.',
required=True,
)
def __init__(self, *args, event: EventExt, user: AbstractBaseUser, **kwargs):
self.event = event
self.user = user
super().__init__(*args, **kwargs)
def clean(self):
if self.event.total_due_to_instructor is None:
raise forms.ValidationError(
"Event missing required information to generate invoice"
)
if not self.event.user_is_instructor(self.user):
raise forms.ValidationError(
"You are not the listed as the instructor for this event"
)
if hasattr(self.event, "eventinvoice"):
raise forms.ValidationError("Invoice already exists for this event")
return super().clean()

View File

@ -1,65 +0,0 @@
from django.conf import settings
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template import loader
import mdformat
from markdownify import markdownify
from membershipworks.models import EventInvoice
def make_multipart_email(
subject: str, html_body: str, to: tuple[str]
) -> EmailMultiAlternatives:
plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
email = EmailMultiAlternatives(
subject,
plain_body,
from_email="CMS Invoices <invoices@claremontmakerspace.org>",
to=to,
reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
)
email.attach_alternative(html_body, "text/html")
return email
def make_instructor_email(
invoice: EventInvoice, pdf: bytes, event_url: str
) -> EmailMessage:
template = loader.get_template(
"membershipworks/email/event_invoice_instructor.dj.html"
)
html_body = template.render({"invoice": invoice, "event_url": event_url})
message = make_multipart_email(
f'Your CMS instructor invoice has been received for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
html_body,
(invoice.event.instructor.member.sanitized_mailbox(),),
)
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message
def make_admin_email(invoice: EventInvoice, pdf: bytes, event_url: str) -> EmailMessage:
template = loader.get_template("membershipworks/email/event_invoice_admin.dj.html")
html_body = template.render({"invoice": invoice, "event_url": event_url})
message = make_multipart_email(
f'CMS instructor invoice created for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
html_body,
# TODO: should this be in database instead?
settings.INVOICE_HANDLERS,
)
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message
def make_invoice_emails(
invoice: EventInvoice, pdf: bytes, event_url: str
) -> list[EmailMessage]:
return [
make_instructor_email(invoice, pdf, event_url),
make_admin_email(invoice, pdf, event_url),
]

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.2 on 2024-03-08 21:30
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0015_eventmeetingtime_end_after_start"),
]
operations = [
migrations.CreateModel(
name="EventInvoice",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("date_submitted", models.DateField()),
("date_paid", models.DateField(blank=True, null=True)),
("pdf", models.FileField(upload_to="invoices/%Y/%m/%d/")),
("amount", models.DecimalField(decimal_places=4, max_digits=13)),
(
"event",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="invoice",
to="membershipworks.eventext",
),
),
],
),
]

View File

@ -1,10 +1,8 @@
import uuid
from datetime import datetime
from typing import Optional
import django.core.mail.message
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.db.models import (
Case,
@ -17,14 +15,11 @@ from django.db.models import (
Q,
Subquery,
Sum,
Value,
When,
)
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import timezone
import nh3
from django_db_views.db_view import DBView
@ -429,12 +424,8 @@ class Event(BaseModel):
_allowed_missing_fields = ["cap", "edp", "adn"]
@property
def unescaped_title(self):
return nh3.clean(self.title, tags=set())
def __str__(self):
return self.unescaped_title
return self.title
class EventInstructor(models.Model):
@ -480,7 +471,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
"amount",
"materials",
"amount_without_materials",
"instructor_revenue",
"instructor_fee",
"instructor_amount",
]
},
@ -552,15 +543,6 @@ class EventExt(Event):
)
details = models.JSONField(null=True, blank=True)
def get_absolute_url(self) -> str:
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
def user_is_instructor(self, user: AbstractBaseUser) -> bool:
member = Member.from_user(user)
if member is not None:
return self.instructor.member == member
return False
class Meta:
verbose_name = "event"
ordering = ["-start"]
@ -588,42 +570,6 @@ class EventMeetingTime(models.Model):
]
class EventInvoice(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.OneToOneField(
EventExt, on_delete=models.PROTECT, related_name="invoice"
)
date_submitted = models.DateField()
date_paid = models.DateField(blank=True, null=True)
pdf = models.FileField(upload_to="protected/invoices/%Y/%m/%d/")
amount = models.DecimalField(max_digits=13, decimal_places=4)
def __str__(self) -> str:
return f'"{self.event}" submitted={self.date_submitted} paid:{self.date_paid} ${self.amount}'
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
def group_by_ticket_type(self):
return self.values("is_members_ticket").annotate(
label=Case(
When(Q(is_members_ticket=True), Value("Members")),
default=Value("Non-Members"),
),
actual_price=F("actual_price"),
**{
field: Sum(field)
for field in [
"quantity",
"materials",
"amount",
"amount_without_materials",
"instructor_revenue",
"instructor_amount",
]
},
)
class EventTicketTypeManager(models.Manager["EventTicketType"]):
def get_queryset(self) -> models.QuerySet["EventTicketType"]:
members_folder = Subquery(
@ -669,19 +615,18 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
amount_without_materials=ExpressionWrapper(
F("amount") - F("materials"), output_field=models.DecimalField()
),
instructor_revenue=ExpressionWrapper(
instructor_fee=ExpressionWrapper(
F("amount_without_materials") * F("event__instructor_percentage"),
output_field=models.DecimalField(),
),
instructor_amount=ExpressionWrapper(
F("instructor_revenue") + F("materials"),
output_field=models.DecimalField(),
F("instructor_fee") + F("materials"), output_field=models.DecimalField()
),
)
class EventTicketType(DBView):
objects = EventTicketTypeManager.from_queryset(EventTicketTypeQuerySet)()
objects = EventTicketTypeManager()
event = models.ForeignKey(
EventExt, on_delete=models.CASCADE, related_name="ticket_types"

View File

@ -1,82 +0,0 @@
@page {
size: letter portrait;
margin: 1.2in 0.7in;
@top-center {
content: "Event Invoice";
font-size: 1.2em;
color: #444;
vertical-align: bottom;
}
@top-left {
vertical-align: bottom;
content: element(header);
}
@top-right {
content: "";
vertical-align: bottom;
background-position: bottom;
background-image: url("https://claremontmakerspace.org/wp-content/uploads/2018/06/cms_logo.png");
background-repeat: no-repeat;
background-size: 100%;
width: 7em;
}
@bottom-left {
content: element(footer-left);
}
@bottom-right {
content: element(footer-right);
}
}
footer {
font-size: 8pt;
}
footer .left {
position: running(footer-left);
margin-right: 1em;
}
footer .right {
position: running(footer-right);
text-align: right;
}
header {
position: running(header);
}
/* TODO: probably should just fix this server side */
:root {
--bs-font-sans-serif: roboto;
}
body {
margin-top: 1em;
}
p {
margin-bottom: 0.2em;
}
/* Bootstrap fixes for Weasyprint */
.table-group-divider {
border-top: 2px solid currentcolor;
}
.d-md-block {
display: block !important;
}
.col-md-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-md-4 {
flex: 0 0 auto;
width: 33.33333333%;
}

View File

@ -1,6 +0,0 @@
<p>
New invoice received from
<b>{{ invoice.event.instructor }}</b> on <b>{{ invoice.date_submitted }}</b>.
</p>
{% include "membershipworks/email/event_invoice_details_fragment.dj.html" %}

View File

@ -1,19 +0,0 @@
<p>
<div>
<b>Invoice #:</b> {{ invoice.uuid }}
</div>
<div>
<b>Event id:</b> {{ invoice.event.eid }}
</div>
<div>
<b>Event title:</b> {{ invoice.event }}
</div>
<div>
<b>Event duration:</b> {{ invoice.event.start }} - {{ invoice.event.end }}
</div>
<div>
<a href="{{ event_url }}">
<b>View this event and invoice in CMSManage</b>
</a>
</div>
</p>

View File

@ -1,5 +0,0 @@
<p>
Your invoice for <b>{{ invoice.event }}</b> has been received. A copy is attached for your records.
</p>
{% include "membershipworks/email/event_invoice_details_fragment.dj.html" %}

View File

@ -1,46 +0,0 @@
{% extends "base.dj.html" %}
{% load nh3_tags %}
{% load django_bootstrap5 %}
{% block title %}Event Invoice for {{ event.details.ttl|nh3 }}{% endblock %}
{% block admin_link %}
{% url 'admin:membershipworks_eventext_change' event.pk %}
{% endblock %}
{% block content %}
<div class="container">
{% include "membershipworks/event_invoice.dj.html" %}
<div class="card w-auto mt-5">
<div class="card-body">
{% if event.invoice %}
<div class="card-text text-center">
<p>
Invoice submitted on {{ event.invoice.date_submitted }}
for ${{ event.invoice.amount|floatformat:2 }},
{% if event.invoice.date_paid %}
paid on {{ event.invoice.date_paid }}
{% else %}
not paid yet
{% endif %}
</p>
<a class="btn btn-primary"
href="{% url 'membershipworks:event-invoice-pdf' event.invoice.pk %}">View PDF</a>
</div>
{% elif event.total_due_to_instructor is None %}
<p class="card-text text-center">
This event is missing required information to generate an invoice. Please contact us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a>.
</p>
{% elif user_is_instructor %}
<form method="post" class="card-text">
{% csrf_token %}
{% bootstrap_form form %}
<div class="text-center">{% bootstrap_button button_type="submit" content="Submit Invoice" %}</div>
</form>
{% else %}
<p>No invoice has been created for this event, and you are not listed as the the instructor.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,16 @@
{% extends "base.dj.html" %}
{% load nh3_tags %}
{% load render_table from django_tables2 %}
<div class="row">
<div class="col-12 col-md-7">
{% block title %}Event Invoice for {{ event.details.ttl|nh3 }}{% endblock %}
{% block admin_link %}
{% url 'admin:membershipworks_eventext_change' event.pk %}
{% endblock %}
{% block content %}
<p>
<b>Event Name:</b> {{ event.details.ttl|nh3 }}
</p>
<p>
<b>Event ID:</b> {{ event.eid }}
</p>
<p>
<b>Instructor:</b> {{ event.instructor }}
</p>
@ -17,11 +19,11 @@
{% if meeting_times|length == 0 %}
<b>Dates of Event:</b> Not set
{% elif meeting_times|length == 1 %}
<b>Date of Event:</b> {{ meeting_times.0.start }} {{ meeting_times.0.end }}
<b>Date of Event:</b> {{ meeting_times.0.start }} - {{ meeting_times.0.end }}
{% else %}
<b>Dates of Event:</b>
<ul>
{% for meeting_time in meeting_times %}<li>{{ meeting_time.start }} {{ meeting_time.end }}</li>{% endfor %}
{% for meeting_time in meeting_times %}<li>{{ meeting_time.start }} - {{ meeting_time.end }}</li>{% endfor %}
</ul>
{% endif %}
{% endwith %}
@ -30,7 +32,7 @@
<b>Attendees:</b> {{ event.details.cnt }}/{{ event.details.cap }}
</p>
<p>
<b>Materials Fee <span class="font-monospace fw-light">[m]</span>:</b>
<b>Materials Fee [m]:</b>
{% if event.materials_fee_included_in_price is None %}
Unknown if included in price
{% elif event.materials_fee_included_in_price %}
@ -44,22 +46,7 @@
{% endif %}
</p>
<p>
<b>Instructor Percentage <span class="font-monospace fw-light">[I]</span>:</b> {{ event.instructor_percentage }}
<b>Instructor Percentage [I]:</b> {{ event.instructor_percentage }}
</p>
</div>
<div class="vr d-none d-md-block m-4 p-0"></div>
<div class="col-12 col-md-4">
<div>
<h3>Remit to:</h3>
<div>{{ event.instructor.member.account_name }}</div>
<div>{{ event.instructor.member.address_street }}</div>
<div>
{{ event.instructor.member.address_city }},
{{ event.instructor.member.address_state_province }}
{{ event.instructor.member.address_postal_code }}
</div>
<div>{{ event.instructor.member.email }}</div>
</div>
</div>
</div>
{% render_table table "membershipworks/tables/invoice_table.dj.html" %}
{% endblock %}

View File

@ -1,29 +0,0 @@
{% load static %}
{% load nh3_tags %}
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<link rel="stylesheet" href="{% static "membershipworks/css/event_invoice_pdf.css" %}" media="print">
<title>Event Invoice for {{ event.details.ttl|nh3 }}</title>
</head>
<body style="font-size: 10pt;">
<header>
<div>TwinState MakerSpaces, Inc.</div>
<div>PO Box 100</div>
<div>Lebanon, NH 03766-0100</div>
</header>
<footer>
<div class="left">Generated for {{ user }} on {{ now|date:"Y-m-d" }} {{ now|time:"H:i T" }} by CMSManage</div>
<div class="right">
Invoice ID: <span class="font-monospace text-nowrap">{{ invoice_uuid }}</span>
</div>
</footer>
{% include "membershipworks/event_invoice.dj.html" %}
</body>

View File

@ -1,10 +0,0 @@
{% extends "base.dj.html" %}
{% load render_table from django_tables2 %}
{% block title %}My Events{% endblock %}
{% block content %}
{% include "cmsmanage/components/download_table.dj.html" %}
{% render_table table %}
{% endblock %}

View File

@ -31,19 +31,9 @@ urlpatterns = [
name="event-month-report",
),
path(
"my-events/",
views.UserEventView.as_view(),
name="user-events",
),
path(
"event/<eid>",
views.EventDetailView.as_view(),
name="event-detail",
),
path(
"event/invoice/<uuid:uuid>.pdf",
views.EventInvoicePDFView.as_view(),
name="event-invoice-pdf",
"event-invoice/<eid>",
views.EventInvoiceView.as_view(),
name="event-invoice",
),
path(
"event-attendees",

View File

@ -1,51 +1,31 @@
import uuid
from datetime import datetime, timedelta
from typing import Any
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import (
AccessMixin,
PermissionRequiredMixin,
)
from django.core import mail
from django.core.files.base import ContentFile
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import TruncMonth, TruncYear
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template.defaultfilters import floatformat
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.views.generic import DetailView, ListView
from django.views.generic.dates import (
ArchiveIndexView,
MonthArchiveView,
YearArchiveView,
)
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import FormMixin, ProcessFormView
import django_filters
import django_tables2 as tables
import weasyprint
from dal import autocomplete
from django_filters.views import BaseFilterView
from django_mysql.models import GroupConcat
from django_sendfile import sendfile
from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin
from django_weasyprint.utils import django_url_fetcher
from membershipworks.membershipworks_api import MembershipWorks
from .forms import EventInvoiceForm
from .invoice_email import make_invoice_emails
from .models import EventAttendee, EventExt, EventInvoice, Member
from .models import EventAttendee, EventExt, Member
class MemberAutocomplete(autocomplete.Select2QuerySetView):
@ -155,9 +135,8 @@ class EventTable(tables.Table):
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="Invoice" href="{% url "membershipworks:event-invoice" record.pk %}"><i class="bi bi-receipt"></i></a> '
),
accessor="unescaped_title",
)
occurred = tables.BooleanColumn(visible=False)
start = tables.DateColumn("N d, Y")
@ -167,8 +146,6 @@ class EventTable(tables.Table):
gross_revenue = tables.Column()
total_due_to_instructor = tables.Column()
net_revenue = tables.Column()
invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted")
invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid")
class Meta:
model = EventExt
@ -295,7 +272,7 @@ class EventMonthReport(
return (
super()
.get_table_data()
.select_related("category", "instructor", "invoice")
.select_related("category", "instructor")
.with_financials()
)
@ -303,42 +280,6 @@ class EventMonthReport(
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
class UserEventTable(EventTable):
title = tables.Column(linkify=True, accessor="unescaped_title")
instructor = None
person_hours = None
gross_revenue = None
net_revenue = None
class UserEventView(SingleTableMixin, ListView):
model = EventExt
table_class = UserEventTable
export_formats = ("csv", "xlsx", "ods")
template_name = "membershipworks/user_event_list.dj.html"
@cached_property
def member(self) -> Member | None:
return Member.from_user(self.request.user)
def get_queryset(self):
if self.member is None:
return self.model.objects.none()
else:
return super().get_queryset().filter(instructor__member=self.member)
def get_table_data(self):
return (
super()
.get_table_data()
.select_related("category", "instructor", "invoice")
.with_financials()
)
def get_export_filename(self, export_format):
return f"my_events_{self.member.uid if self.member is not None else 'no-uid'}.{export_format}"
class InvoiceMoneyColumn(tables.columns.Column):
def render(self, value):
return f"${super().render(value):.2f}"
@ -358,173 +299,40 @@ class InvoiceTable(tables.Table):
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 = InvoiceMoneyColumn("Ticket Price")
actual_price = InvoiceMoneyColumn(_math_header("Actual Price", "P"))
quantity = tables.Column(
_math_header("Quantity", "Q"),
footer=lambda table: table.event.quantity,
)
amount = InvoiceMoneyFooterColumn(_math_header("Amount", "A=P*Q"))
materials = InvoiceMoneyFooterColumn(
_math_header("CMS Collected Materials Fee", "M=m*Q")
)
actual_price = InvoiceMoneyColumn("Actual Price [P]")
quantity = tables.Column("Quantity [Q]", footer=lambda table: table.event.quantity)
amount = InvoiceMoneyFooterColumn("Amount [A = P * Q]")
materials = InvoiceMoneyFooterColumn("CMS Collected Materials Fee [M = m * Q]")
amount_without_materials = InvoiceMoneyFooterColumn(
_math_header("Event Revenue Base", "B=A-M")
)
instructor_revenue = InvoiceMoneyFooterColumn(
_math_header("Instructor Percentage Revenue", "R=B*I")
)
instructor_amount = InvoiceMoneyFooterColumn(
_math_header("Amount Due to Instructor", "R+M")
"Event Revenue Base [R = A - M]"
)
instructor_fee = InvoiceMoneyFooterColumn("Instructor Fee [F = R * I]")
instructor_amount = InvoiceMoneyFooterColumn("Amount Due to Instructor [F + 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 EventDetailView(
SingleTableMixin, FormMixin, AccessMixin, DetailView, ProcessFormView
):
class EventInvoiceView(SingleTableMixin, PermissionRequiredMixin, DetailView):
permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.with_financials().all()
pk_url_kwarg = "eid"
context_object_name = "event"
template_name = "membershipworks/event_detail.dj.html"
template_name = "membershipworks/event_invoice.dj.html"
table_pagination = False
table_class = InvoiceTable
form_class = EventInvoiceForm
def render_to_response(
self, context: dict[str, Any], **response_kwargs: Any
) -> HttpResponse:
if self.request.user.has_perm(
self.permission_required
) or self.object.user_is_instructor(self.request.user):
return super().render_to_response(context, **response_kwargs)
else:
return self.handle_no_permission()
def display_instructor_version(self):
return (
self.request.method == "POST" # generating a PDF
or not self.request.user.has_perm(self.permission_required)
or self.request.GET.get("instructor_view")
)
def get_table_data(self):
if self.display_instructor_version():
return self.object.ticket_types.group_by_ticket_type()
else:
return self.object.ticket_types.all()
def get_table_kwargs(self):
kwargs = super().get_table_kwargs()
kwargs["event"] = self.object
if self.display_instructor_version():
kwargs["exclude"] = [
"list_price",
]
return kwargs
def get_success_url(self):
return self.request.build_absolute_uri()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["user_is_instructor"] = self.object.user_is_instructor(
self.request.user
)
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["event"] = self.object
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form: EventInvoiceForm):
self.object = self.get_object()
event = self.object
invoice_uuid = uuid.uuid4()
pdf_context = self.get_context_data(object=event)
pdf_context.update({"now": timezone.now(), "invoice_uuid": invoice_uuid})
weasy_html = weasyprint.HTML(
string=render_to_string(
"membershipworks/event_invoice_pdf.dj.html",
context=pdf_context,
request=self.request,
),
url_fetcher=django_url_fetcher,
base_url="file://",
)
pdf = weasy_html.write_pdf()
# the result will be None only if a target was provided
assert pdf is not None
# NOTE: this is only saved AFTER the emails are successfully sent
invoice = EventInvoice(
uuid=invoice_uuid,
event=event,
# NOTE: this needs to be resolved before the object is
# saved, so cannot use the Now() db function
date_submitted=timezone.now(),
amount=event.total_due_to_instructor,
)
# removed), currently used in event_invoice_admin.dj.html.
emails = make_invoice_emails(
invoice, pdf, self.request.build_absolute_uri(event.get_absolute_url())
)
with mail.get_connection() as conn:
conn.send_messages(emails)
# this also saves the invoice object
invoice.pdf.save(f"{event.eid}.pdf", ContentFile(pdf))
messages.success(
self.request,
"Created Invoice! You should receive a confirmation email shortly.",
)
return super().form_valid(form)
class EventInvoicePDFView(AccessMixin, BaseDetailView):
model = EventInvoice
pk_url_kwarg = "uuid"
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
invoice = self.get_object()
if request.user.has_perm(
"membershipworks.view_eventinvoice"
) or invoice.event.user_is_instructor(request.user):
# return HttpResponse(invoice.pdf.path)
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
else:
return self.handle_no_permission()
return {"event": self.object}
class EventAttendeeTable(tables.Table):

View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:8d03a3352b70059a88842e3f316ee61450b7690e39a4516b7ce8ebf22aaae5b2"
content_hash = "sha256:4d5083f84a5eddb0e66de8dbd7c6cf2b6c78321aca649b9283d1e0ebe972455d"
[[package]]
name = "aiohttp"
@ -502,19 +502,6 @@ files = [
{file = "django-autocomplete-light-3.11.0.tar.gz", hash = "sha256:212576a17e3308ef7ca77e280b86684167916d2091d4b73640f38845d9516328"},
]
[[package]]
name = "django-bootstrap5"
version = "23.4"
requires_python = ">=3.8"
summary = "Bootstrap 5 for Django"
dependencies = [
"Django>=3.2",
]
files = [
{file = "django-bootstrap5-23.4.tar.gz", hash = "sha256:fbf9942a17e1f48b4142e78df9e85afb65e4066a20e38ec7c497d36ae5ef7256"},
{file = "django_bootstrap5-23.4-py3-none-any.whl", hash = "sha256:5181bf1e97afae6211e963f28f48d4a90c937a3b036b3f752f52260f0029f3bc"},
]
[[package]]
name = "django-db-views"
version = "0.1.6"
@ -659,18 +646,6 @@ files = [
{file = "django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5"},
]
[[package]]
name = "django-sendfile2"
version = "0.7.1"
summary = "Abstraction to offload file uploads to web-server (e.g. Apache with mod_xsendfile) once Django has checked permissions etc."
dependencies = [
"django",
]
files = [
{file = "django-sendfile2-0.7.1.tar.gz", hash = "sha256:b5bec07f1c9b1875a60ea74beb306e9aba964bd8b54f00b4432cb77cc35bc58c"},
{file = "django_sendfile2-0.7.1-py3-none-any.whl", hash = "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a"},
]
[[package]]
name = "django-stubs"
version = "4.2.7"
@ -729,20 +704,6 @@ files = [
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"},
]
[[package]]
name = "django-weasyprint"
version = "2.3.0"
requires_python = ">=3.8"
summary = "Django WeasyPrint CBV"
dependencies = [
"Django>=3.2",
"WeasyPrint>=53",
]
files = [
{file = "django-weasyprint-2.3.0.tar.gz", hash = "sha256:2f849e15bfd6c1b2a58512097b9042eddf3533651d37d2e096cd6f7d8be6442b"},
{file = "django_weasyprint-2.3.0-py3-none-any.whl", hash = "sha256:807cb3b16332123d97c8bbe2ac9c70286103fe353235351803ffd33b67284735"},
]
[[package]]
name = "django-widget-tweaks"
version = "1.5.0"

View File

@ -35,9 +35,6 @@ dependencies = [
"django-filter~=24.2",
"django-db-views~=0.1",
"django-mysql~=4.12",
"django-weasyprint~=2.3",
"django-sendfile2~=0.7",
"django-bootstrap5~=23.4",
]
requires-python = ">=3.11"
@ -54,7 +51,7 @@ admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
line-length = 88
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM", "FIX003"]
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM"]
[tool.ruff.lint.isort]
known-first-party = [
@ -135,7 +132,7 @@ dev = [
[tool.pdm.scripts]
start = "./manage.py runserver"
fmt.shell = "ruff check --fix ; ruff format . ; djlint --reformat ."
fmt.shell = "ruff check --fix && ruff format . && djlint --reformat ."
[build-system]
requires = ["pdm-backend"]