membershipworks: Add event invoices and financial info to event reports
All checks were successful
Ruff / ruff (push) Successful in 22s
All checks were successful
Ruff / ruff (push) Successful in 22s
This commit is contained in:
parent
fc5a911c76
commit
3d7d428914
@ -40,6 +40,7 @@ INSTALLED_APPS = [
|
||||
"django_nh3",
|
||||
"django_tables2",
|
||||
"django_filters",
|
||||
"django_db_views",
|
||||
"tasks.apps.TasksConfig",
|
||||
"rentals.apps.RentalsConfig",
|
||||
"membershipworks.apps.MembershipworksConfig",
|
||||
|
@ -0,0 +1,79 @@
|
||||
# Generated by Django 5.0.1 on 2024-01-29 19:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import django_db_views.migration_functions
|
||||
import django_db_views.operations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("membershipworks", "0011_eventext_details"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="EventAttendeeStats",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("gross_revenue", models.FloatField()),
|
||||
],
|
||||
options={
|
||||
"managed": False,
|
||||
},
|
||||
),
|
||||
django_db_views.operations.ViewRunPython(
|
||||
code=django_db_views.migration_functions.ForwardViewMigration(
|
||||
"SELECT\n row_number() over () as id,\n eventext.event_ptr_id AS event_id,\n tkt.label,\n tkt.list_price,\n tkt.quantity,\n GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to\n FROM\n membershipworks_eventext AS eventext,\n JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS (\n id FOR ORDINALITY,\n label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR,\n list_price DOUBLE PATH '$.amt' ERROR ON ERROR,\n quantity INTEGER PATH '$.cnt' DEFAULT 0 ON EMPTY ERROR ON ERROR,\n NESTED PATH '$.dsp[*]' COLUMNS (\n restrict_to VARCHAR(100) PATH '$' ERROR ON ERROR\n )\n )) AS tkt\n GROUP BY event_id, id",
|
||||
"membershipworks_eventtickettype",
|
||||
engine="django.db.backends.mysql",
|
||||
),
|
||||
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
|
||||
"", "membershipworks_eventtickettype", engine="django.db.backends.mysql"
|
||||
),
|
||||
atomic=False,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EventTicketType",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("label", models.TextField()),
|
||||
("restrict_to", models.TextField(blank=True, null=True)),
|
||||
("list_price", models.FloatField()),
|
||||
("quantity", models.IntegerField()),
|
||||
],
|
||||
options={
|
||||
"managed": False,
|
||||
"base_manager_name": "objects",
|
||||
},
|
||||
),
|
||||
django_db_views.operations.ViewRunPython(
|
||||
code=django_db_views.migration_functions.ForwardViewMigration(
|
||||
"SELECT eventext.event_ptr_id as event_id, SUM(tkt.s) as gross_revenue\n FROM\n membershipworks_eventext as eventext,\n JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (\n s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY\n )) as tkt\n GROUP BY event_id",
|
||||
"membershipworks_eventattendeestats",
|
||||
engine="django.db.backends.mysql",
|
||||
),
|
||||
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
|
||||
"",
|
||||
"membershipworks_eventattendeestats",
|
||||
engine="django.db.backends.mysql",
|
||||
),
|
||||
atomic=False,
|
||||
),
|
||||
]
|
@ -5,6 +5,7 @@ import django.core.mail.message
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import (
|
||||
Case,
|
||||
Count,
|
||||
Exists,
|
||||
ExpressionWrapper,
|
||||
@ -14,9 +15,13 @@ from django.db.models import (
|
||||
Q,
|
||||
Subquery,
|
||||
Sum,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
|
||||
from django_db_views.db_view import DBView
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
_api_names_override = {}
|
||||
@ -444,6 +449,40 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
|
||||
person_hours__sum=Sum("person_hours", filter=F("occurred")),
|
||||
event_count=Count("eid", filter=F("occurred")),
|
||||
canceled_event_count=Count("eid", filter=~F("occurred")),
|
||||
gross_revenue__sum=Sum("gross_revenue", filter=F("occurred")),
|
||||
total_due_to_instructor__sum=Sum(
|
||||
"total_due_to_instructor", filter=F("occurred")
|
||||
),
|
||||
net_revenue__sum=Sum("net_revenue", filter=F("occurred")),
|
||||
)
|
||||
|
||||
def with_financials(self):
|
||||
return self.annotate(
|
||||
**{
|
||||
field: Subquery(
|
||||
EventTicketType.objects.filter(event=OuterRef("pk"))
|
||||
.values("event__pk")
|
||||
.annotate(d=Sum(field))
|
||||
.values("d"),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
for field in [
|
||||
"quantity",
|
||||
"amount",
|
||||
"materials",
|
||||
"amount_without_materials",
|
||||
"instructor_fee",
|
||||
"instructor_amount",
|
||||
]
|
||||
},
|
||||
total_due_to_instructor=(
|
||||
F("instructor_amount") + F("instructor_flat_rate")
|
||||
),
|
||||
gross_revenue=Coalesce(F("attendee_stats__gross_revenue"), 0.0),
|
||||
net_revenue=ExpressionWrapper(
|
||||
F("gross_revenue") - F("total_due_to_instructor"),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -528,3 +567,124 @@ class EventMeetingTime(models.Model):
|
||||
fields=["event", "start", "end"], name="unique_event_start_end"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class EventTicketTypeManager(models.Manager["EventTicketType"]):
|
||||
def get_queryset(self) -> models.QuerySet["EventTicketType"]:
|
||||
members_folder = Subquery(
|
||||
Flag.objects.filter(name="Members", type="folder").values("id")[:1]
|
||||
)
|
||||
qs = super().get_queryset()
|
||||
return qs.annotate(
|
||||
actual_price=Case(
|
||||
When(
|
||||
Q(restrict_to=members_folder) | Q(restrict_to__isnull=True),
|
||||
"list_price",
|
||||
),
|
||||
# Use Members ticket price for any restricted ticket
|
||||
# which is not the Members ticket
|
||||
default=Subquery(
|
||||
qs.filter(
|
||||
event=OuterRef("event"), restrict_to=members_folder
|
||||
).values("list_price"),
|
||||
output_field=models.FloatField(),
|
||||
),
|
||||
),
|
||||
is_members_ticket=(Q(restrict_to__isnull=False)),
|
||||
materials=Case(
|
||||
When(Q(event__materials_fee_included_in_price__isnull=True), None),
|
||||
When(
|
||||
(
|
||||
Q(event__materials_fee_included_in_price=True)
|
||||
& Q(event__materials_fee__isnull=False)
|
||||
),
|
||||
ExpressionWrapper(
|
||||
F("event__materials_fee") * F("quantity"),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
),
|
||||
default=0,
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
amount=ExpressionWrapper(
|
||||
F("actual_price") * F("quantity"),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
amount_without_materials=ExpressionWrapper(
|
||||
F("amount") - F("materials"), output_field=models.DecimalField()
|
||||
),
|
||||
instructor_fee=ExpressionWrapper(
|
||||
F("amount_without_materials") * F("event__instructor_percentage"),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
instructor_amount=ExpressionWrapper(
|
||||
F("instructor_fee") + F("materials"), output_field=models.DecimalField()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EventTicketType(DBView):
|
||||
objects = EventTicketTypeManager()
|
||||
|
||||
event = models.ForeignKey(
|
||||
EventExt, on_delete=models.CASCADE, related_name="ticket_types"
|
||||
)
|
||||
label = models.TextField()
|
||||
restrict_to = models.TextField(null=True, blank=True)
|
||||
list_price = models.FloatField()
|
||||
quantity = models.IntegerField()
|
||||
|
||||
# Due to the presence of JSON_TABLE, this view must (as of MariaDB
|
||||
# 11.2.2) be created as the root user. See
|
||||
# https://jira.mariadb.org/browse/MDEV-27898
|
||||
|
||||
# nested path/group_concat to workaround inability to create JSON columns using
|
||||
# JSON_TABLE in views
|
||||
view_definition = """
|
||||
SELECT
|
||||
row_number() over () as id,
|
||||
eventext.event_ptr_id AS event_id,
|
||||
tkt.label,
|
||||
tkt.list_price,
|
||||
tkt.quantity,
|
||||
GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to
|
||||
FROM
|
||||
membershipworks_eventext AS eventext,
|
||||
JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS (
|
||||
id FOR ORDINALITY,
|
||||
label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR,
|
||||
list_price DOUBLE PATH '$.amt' ERROR ON ERROR,
|
||||
quantity INTEGER PATH '$.cnt' DEFAULT 0 ON EMPTY ERROR ON ERROR,
|
||||
NESTED PATH '$.dsp[*]' COLUMNS (
|
||||
restrict_to VARCHAR(100) PATH '$' ERROR ON ERROR
|
||||
)
|
||||
)) AS tkt
|
||||
GROUP BY event_id, id
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.label}: {self.quantity} * {self.list_price}"
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
base_manager_name = "objects"
|
||||
|
||||
|
||||
class EventAttendeeStats(DBView):
|
||||
event = models.ForeignKey(
|
||||
EventExt, on_delete=models.CASCADE, related_name="attendee_stats"
|
||||
)
|
||||
gross_revenue = models.FloatField()
|
||||
|
||||
view_definition = """
|
||||
SELECT eventext.event_ptr_id as event_id, SUM(tkt.s) as gross_revenue
|
||||
FROM
|
||||
membershipworks_eventext as eventext,
|
||||
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (
|
||||
s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY
|
||||
)) as tkt
|
||||
GROUP BY event_id
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
|
@ -0,0 +1,52 @@
|
||||
{% 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 %}
|
||||
<p>
|
||||
<b>Event Name:</b> {{ event.details.ttl|nh3 }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Instructor:</b> {{ event.instructor }}
|
||||
</p>
|
||||
<p>
|
||||
{% with meeting_times=event.meeting_times.all %}
|
||||
{% 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 }}
|
||||
{% else %}
|
||||
<b>Dates of Event:</b>
|
||||
<ul>
|
||||
{% for meeting_time in meeting_times %}<li>{{ meeting_time.start }} - {{ meeting_time.end }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
<p>
|
||||
<b>Attendees:</b> {{ event.details.cnt }}/{{ event.details.cap }}
|
||||
</p>
|
||||
<p>
|
||||
<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 %}
|
||||
{% if event.materials_fee is not None %}
|
||||
${{ event.materials_fee|floatformat:2 }}
|
||||
{% else %}
|
||||
Not defined
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Not collected by CMS
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<b>Instructor Percentage [I]:</b> {{ event.instructor_percentage }}
|
||||
</p>
|
||||
{% render_table table "membershipworks/tables/invoice_table.dj.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,45 @@
|
||||
{% extends "django_tables2/bootstrap5.html" %}
|
||||
|
||||
{% block table.tfoot %}
|
||||
{% if table.has_footer %}
|
||||
<tfoot {{ table.attrs.tfoot.as_html }}>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% if forloop.first %}
|
||||
<th for="row" {{ column.attrs.tf.as_html }}>
|
||||
{{ column.footer }}
|
||||
</th>
|
||||
{% else %}
|
||||
<td {{ column.attrs.tf.as_html }}>
|
||||
{{ column.footer }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% if table.event.instructor_flat_rate != 0 %}
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length|add:-2 }}"></td>
|
||||
<th scope="row">Flat Rate</th>
|
||||
<td>
|
||||
{% if table.event.instructor_flat_rate is None %}
|
||||
{{ table.default }}
|
||||
{% else %}
|
||||
${{ table.event.instructor_flat_rate|floatformat:2 }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td colspan="{{ table.columns|length|add:-2 }}"></td>
|
||||
<th scope="row">Total</th>
|
||||
<td>
|
||||
{% if table.event.total_due_to_instructor is None %}
|
||||
{{ table.default }}
|
||||
{% else %}
|
||||
${{ table.event.total_due_to_instructor|floatformat:2 }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
{% endblock table.tfoot %}
|
@ -2,6 +2,7 @@ from django.urls import path
|
||||
|
||||
from .views import (
|
||||
EventIndexReport,
|
||||
EventInvoiceView,
|
||||
EventMonthReport,
|
||||
EventYearReport,
|
||||
MemberAutocomplete,
|
||||
@ -36,4 +37,9 @@ urlpatterns = [
|
||||
EventMonthReport.as_view(month_format="%m"),
|
||||
name="event-month-report",
|
||||
),
|
||||
path(
|
||||
"event-invoice/<eid>",
|
||||
EventInvoiceView.as_view(),
|
||||
name="event-invoice",
|
||||
),
|
||||
]
|
||||
|
@ -7,6 +7,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models.functions import TruncMonth, TruncYear
|
||||
from django.shortcuts import render
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic.dates import (
|
||||
ArchiveIndexView,
|
||||
MonthArchiveView,
|
||||
@ -130,6 +131,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> '
|
||||
),
|
||||
)
|
||||
occurred = tables.BooleanColumn(visible=False)
|
||||
@ -137,6 +139,9 @@ class EventTable(tables.Table):
|
||||
duration = DurationColumn()
|
||||
person_hours = DurationColumn()
|
||||
meetings = tables.Column()
|
||||
gross_revenue = tables.Column()
|
||||
total_due_to_instructor = tables.Column()
|
||||
net_revenue = tables.Column()
|
||||
|
||||
class Meta:
|
||||
model = EventExt
|
||||
@ -151,6 +156,9 @@ class EventTable(tables.Table):
|
||||
"meetings",
|
||||
"duration",
|
||||
"person_hours",
|
||||
"gross_revenue",
|
||||
"total_due_to_instructor",
|
||||
"net_revenue",
|
||||
)
|
||||
row_attrs = {
|
||||
"class": lambda record: (
|
||||
@ -167,6 +175,9 @@ class EventSummaryTable(tables.Table):
|
||||
meetings__sum = tables.Column("Meetings")
|
||||
duration__sum = DurationColumn("Class Hours")
|
||||
person_hours__sum = DurationColumn("Person Hours")
|
||||
gross_revenue__sum = tables.Column("Gross Revenue")
|
||||
total_due_to_instructor__sum = tables.Column("Total Due to Instructor")
|
||||
net_revenue__sum = tables.Column("Net Revenue")
|
||||
|
||||
|
||||
class EventIndexReport(
|
||||
@ -185,6 +196,7 @@ class EventIndexReport(
|
||||
return (
|
||||
super()
|
||||
.get_table_data()
|
||||
.with_financials()
|
||||
.values(year=TruncYear("start"))
|
||||
.summarize()
|
||||
.order_by("year")
|
||||
@ -219,6 +231,7 @@ class EventYearReport(
|
||||
return (
|
||||
super()
|
||||
.get_table_data()
|
||||
.with_financials()
|
||||
.values(month=TruncMonth("start"))
|
||||
.summarize()
|
||||
.order_by("month")
|
||||
@ -252,7 +265,67 @@ class EventMonthReport(
|
||||
export_formats = ("csv", "xlsx", "ods")
|
||||
|
||||
def get_table_data(self):
|
||||
return super().get_table_data().select_related("category", "instructor")
|
||||
return (
|
||||
super()
|
||||
.get_table_data()
|
||||
.select_related("category", "instructor")
|
||||
.with_financials()
|
||||
)
|
||||
|
||||
def get_export_filename(self, export_format):
|
||||
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
|
||||
|
||||
|
||||
class InvoiceMoneyColumn(tables.columns.Column):
|
||||
def render(self, value):
|
||||
return f"${super().render(value):.2f}"
|
||||
|
||||
|
||||
class InvoiceMoneyFooterColumn(InvoiceMoneyColumn):
|
||||
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)
|
||||
|
||||
label = tables.Column("Ticket Type", footer="Subtotals")
|
||||
list_price = InvoiceMoneyColumn("Ticket Price")
|
||||
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(
|
||||
"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 = {
|
||||
"tbody": {"class": "table-group-divider"},
|
||||
"tfoot": {"class": "table-group-divider"},
|
||||
}
|
||||
orderable = False
|
||||
|
||||
|
||||
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_invoice.dj.html"
|
||||
table_pagination = False
|
||||
table_class = InvoiceTable
|
||||
|
||||
def get_table_data(self):
|
||||
return self.object.ticket_types.all()
|
||||
|
||||
def get_table_kwargs(self):
|
||||
return {"event": self.object}
|
||||
|
15
pdm.lock
generated
15
pdm.lock
generated
@ -5,7 +5,7 @@
|
||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:16af371b3e9b9b16889ab6dc4e3bd5d84e0119a6804a6081ad64f86f45a2259f"
|
||||
content_hash = "sha256:81d4d8e17c53271139f785226097e04aab990252f78bfd47c237ccb223a54d81"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@ -432,6 +432,19 @@ files = [
|
||||
{file = "django-autocomplete-light-3.9.7.tar.gz", hash = "sha256:a34f192ac438c4df056dbfd399550799ddc631c4661960134ded924648770373"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-db-views"
|
||||
version = "0.1.6"
|
||||
summary = "Handle database views. Allow to create migrations for database views. View migrations using django code. They can be reversed. Changes in model view definition are detected automatically. Support almost all options as regular makemigrations command"
|
||||
dependencies = [
|
||||
"Django",
|
||||
"six",
|
||||
]
|
||||
files = [
|
||||
{file = "django-db-views-0.1.6.tar.gz", hash = "sha256:05718bb87c819323d577b294ee75f25807e5bb767793aa27f1ecc4c7ae073172"},
|
||||
{file = "django_db_views-0.1.6-py3-none-any.whl", hash = "sha256:1ae8a6b389a2e8a7a2e246050ce7688780343bf4fd4f9622263b607ae27e5524"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-debug-toolbar"
|
||||
version = "4.2.0"
|
||||
|
@ -32,6 +32,7 @@ dependencies = [
|
||||
"django-tables2~=2.7",
|
||||
"tablib[ods,xlsx]~=3.5",
|
||||
"django-filter~=23.5",
|
||||
"django-db-views~=0.1",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user