membershipworks: Add ability for instructors to generate and submit event invoices
This commit is contained in:
parent
60e7fc90aa
commit
0ce441336f
@ -42,6 +42,7 @@ INSTALLED_APPS = [
|
||||
"django_filters",
|
||||
"django_db_views",
|
||||
"django_mysql",
|
||||
"django_sendfile",
|
||||
"django_bootstrap5",
|
||||
"tasks.apps.TasksConfig",
|
||||
"rentals.apps.RentalsConfig",
|
||||
@ -121,6 +122,7 @@ LOGGING = {
|
||||
|
||||
MEDIA_ROOT = "media"
|
||||
MEDIA_URL = "media/"
|
||||
SENDFILE_ROOT = str(Path(__file__).parents[2] / "media" / "protected")
|
||||
|
||||
WIKI_URL = "https://wiki.claremontmakerspace.org"
|
||||
|
||||
|
@ -21,3 +21,5 @@ 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"
|
||||
|
@ -48,3 +48,6 @@ 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"
|
||||
|
@ -2,12 +2,15 @@ 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",
|
||||
@ -31,3 +34,16 @@ 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
|
||||
|
38
membershipworks/forms.py
Normal file
38
membershipworks/forms.py
Normal file
@ -0,0 +1,38 @@
|
||||
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()
|
65
membershipworks/invoice_email.py
Normal file
65
membershipworks/invoice_email.py
Normal file
@ -0,0 +1,65 @@
|
||||
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),
|
||||
]
|
@ -4,6 +4,7 @@ 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,
|
||||
@ -20,6 +21,7 @@ from django.db.models import (
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
import nh3
|
||||
@ -550,6 +552,15 @@ 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"]
|
||||
|
@ -0,0 +1,82 @@
|
||||
@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%;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
<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" %}
|
@ -0,0 +1,19 @@
|
||||
<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>
|
@ -0,0 +1,5 @@
|
||||
<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" %}
|
@ -0,0 +1,46 @@
|
||||
{% 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 %}
|
@ -1,16 +1,14 @@
|
||||
{% extends "base.dj.html" %}
|
||||
|
||||
{% load nh3_tags %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% 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="row">
|
||||
<div class="col-12 col-md-7">
|
||||
<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>
|
||||
@ -19,11 +17,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 %}
|
||||
@ -32,7 +30,7 @@
|
||||
<b>Attendees:</b> {{ event.details.cnt }}/{{ event.details.cap }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Materials Fee [m]:</b>
|
||||
<b>Materials Fee <span class="font-monospace fw-light">[m]</span>:</b>
|
||||
{% if event.materials_fee_included_in_price is None %}
|
||||
Unknown if included in price
|
||||
{% elif event.materials_fee_included_in_price %}
|
||||
@ -46,7 +44,22 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<b>Instructor Percentage [I]:</b> {{ event.instructor_percentage }}
|
||||
<b>Instructor Percentage <span class="font-monospace fw-light">[I]</span>:</b> {{ event.instructor_percentage }}
|
||||
</p>
|
||||
{% render_table table "membershipworks/tables/invoice_table.dj.html" %}
|
||||
{% endblock %}
|
||||
</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" %}
|
||||
|
@ -0,0 +1,29 @@
|
||||
{% 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>
|
@ -0,0 +1,10 @@
|
||||
{% 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 %}
|
@ -31,9 +31,19 @@ urlpatterns = [
|
||||
name="event-month-report",
|
||||
),
|
||||
path(
|
||||
"event-invoice/<eid>",
|
||||
views.EventInvoiceView.as_view(),
|
||||
name="event-invoice",
|
||||
"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",
|
||||
),
|
||||
path(
|
||||
"event-attendees",
|
||||
|
@ -1,13 +1,24 @@
|
||||
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 PermissionRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
AccessMixin,
|
||||
PermissionRequiredMixin,
|
||||
)
|
||||
from django.core import mail
|
||||
from django.core.files.base import ContentFile
|
||||
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
|
||||
@ -16,18 +27,25 @@ from django.views.generic.dates import (
|
||||
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 .models import EventAttendee, EventExt, Member
|
||||
from .forms import EventInvoiceForm
|
||||
from .invoice_email import make_invoice_emails
|
||||
from .models import EventAttendee, EventExt, EventInvoice, Member
|
||||
|
||||
|
||||
class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
||||
@ -137,7 +155,7 @@ 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="Invoice" href="{% url "membershipworks:event-invoice" record.pk %}"><i class="bi bi-receipt"></i></a> '
|
||||
'<a title="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
|
||||
),
|
||||
accessor="unescaped_title",
|
||||
)
|
||||
@ -149,6 +167,8 @@ 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
|
||||
@ -275,7 +295,7 @@ class EventMonthReport(
|
||||
return (
|
||||
super()
|
||||
.get_table_data()
|
||||
.select_related("category", "instructor")
|
||||
.select_related("category", "instructor", "invoice")
|
||||
.with_financials()
|
||||
)
|
||||
|
||||
@ -283,6 +303,42 @@ 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}"
|
||||
@ -333,26 +389,142 @@ class InvoiceTable(tables.Table):
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
"class": "table table-sm mx-auto w-auto",
|
||||
"tbody": {"class": "table-group-divider"},
|
||||
"tfoot": {"class": "table-group-divider"},
|
||||
}
|
||||
orderable = False
|
||||
|
||||
|
||||
class EventInvoiceView(SingleTableMixin, PermissionRequiredMixin, DetailView):
|
||||
class EventDetailView(
|
||||
SingleTableMixin, FormMixin, AccessMixin, DetailView, ProcessFormView
|
||||
):
|
||||
permission_required = "membershipworks.view_eventext"
|
||||
queryset = EventExt.objects.with_financials().all()
|
||||
pk_url_kwarg = "eid"
|
||||
context_object_name = "event"
|
||||
template_name = "membershipworks/event_invoice.dj.html"
|
||||
template_name = "membershipworks/event_detail.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):
|
||||
return {"event": self.object}
|
||||
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()
|
||||
|
||||
|
||||
class EventAttendeeTable(tables.Table):
|
||||
|
28
pdm.lock
28
pdm.lock
@ -5,7 +5,7 @@
|
||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:b6c73855da789a2433a99ac698bd3e7d16ad5a37170752043afd14ca9212f228"
|
||||
content_hash = "sha256:8d03a3352b70059a88842e3f316ee61450b7690e39a4516b7ce8ebf22aaae5b2"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@ -659,6 +659,18 @@ 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"
|
||||
@ -717,6 +729,20 @@ 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"
|
||||
|
@ -35,6 +35,8 @@ 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"
|
||||
|
Loading…
Reference in New Issue
Block a user