Manage billing from the control panel
A Billing tab (tiers, their dated prices, and everything we are owed), a billing panel on each club (plan, periods, payment history, invoice), and the dues on the dashboard and the club tables. Every state change goes through the billing service, and a BillingError surfaces as a message rather than a 500 -- so "that period is waived", "no price in force", "already billed for that period" and a missing PDF library all explain themselves instead of crashing. The dashboard now separates the two pots of money that were previously one word. "Revenue per month" was CLUB SHOP revenue -- members paying their clubs, which is never ours -- sitting on our dashboard under a label that implied it was income. It is now "Platform dues per month" (what clubs paid us) with the club-shop series renamed club_revenue, and the club tables carry a Plan column and what each club owes us, annotated in the same single query. Rate changes are add-only in the UI as well as the model: the price form creates a dated row and never edits the last one, and a test asserts that raising the rate leaves an already-open period at the amount it was billed at. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
40
controlpanel/templates/controlpanel/_billing_form.html
Normal file
40
controlpanel/templates/controlpanel/_billing_form.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% load lucide ui %}
|
||||
|
||||
{% comment %}
|
||||
The shell every billing form uses: card, fields, cancel + submit. Included with
|
||||
`heading`, `blurb`, `submit_label`, `submit_icon` and `cancel_url`.
|
||||
{% endcomment %}
|
||||
<div class="card max-w-xl bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{% if blurb %}<p class="text-sm opacity-70">{{ blurb }}</p>{% endif %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="alert alert-error my-2">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for field in form %}
|
||||
<div class="form-control my-3 w-full">
|
||||
{% if field.field.widget.input_type == "checkbox" %}
|
||||
<label class="label cursor-pointer justify-start gap-3" for="{{ field.id_for_label }}">
|
||||
{{ field|daisy }}
|
||||
<span class="label-text">{{ field.label }}</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<label class="label" for="{{ field.id_for_label }}">
|
||||
<span class="label-text">{{ field.label }}</span>
|
||||
</label>
|
||||
{{ field|daisy }}
|
||||
{% endif %}
|
||||
{% if field.help_text %}<span class="label-text-alt mt-1 block text-base-content/70">{{ field.help_text }}</span>{% endif %}
|
||||
{% for error in field.errors %}<span class="label-text-alt mt-1 text-error">{{ error }}</span>{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="card-actions justify-end pt-2">
|
||||
<a class="btn btn-outline gap-2" href="{{ cancel_url }}">{% lucide "arrow-left" size=16 %} Cancel</a>
|
||||
<button class="btn btn-primary gap-2" type="submit">{% lucide submit_icon|default:"check" size=16 %} {{ submit_label|default:"Save" }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,6 +16,8 @@
|
||||
<th class="text-right">Members</th>
|
||||
<th class="text-right">Unpaid</th>
|
||||
<th class="text-right">Owed</th>
|
||||
<th>Plan</th>
|
||||
<th class="text-right">Dues</th>
|
||||
<th class="text-right">Teams</th>
|
||||
<th class="text-right">Upcoming</th>
|
||||
<th class="text-right">Admins</th>
|
||||
@@ -43,6 +45,25 @@
|
||||
<td class="text-right tabular-nums">{{ club.active_members }}</td>
|
||||
<td class="text-right tabular-nums {% if club.unpaid_members %}text-warning{% endif %}">{{ club.unpaid_members }}</td>
|
||||
<td class="text-right tabular-nums {% if club.outstanding %}font-semibold text-error{% endif %}">€{{ club.outstanding|floatformat:2 }}</td>
|
||||
<td>
|
||||
{% if club.tier_name %}
|
||||
<span class="text-sm">{{ club.tier_name }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning badge-xs">Not billed</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right tabular-nums">
|
||||
{% if not club.dues_owed %}
|
||||
{% if club.tier_name %}<span class="badge badge-success badge-xs">Paid</span>{% endif %}
|
||||
{% else %}
|
||||
<span class="font-semibold">€{{ club.dues_owed|floatformat:2 }}</span>
|
||||
{% if club.dues_grace_until < today %}
|
||||
<span class="badge badge-error badge-xs">Overdue</span>
|
||||
{% elif club.dues_period_end < today %}
|
||||
<span class="badge badge-warning badge-xs">Grace</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right tabular-nums">
|
||||
{{ club.team_count }}
|
||||
{% if club.teams_without_coach %}
|
||||
@@ -54,7 +75,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center opacity-60">{{ empty_message|default:"No clubs yet." }}</td>
|
||||
<td colspan="9" class="text-center opacity-60">{{ empty_message|default:"No clubs yet." }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<div role="tablist" class="tabs-boxed tabs mb-6 w-fit">
|
||||
<a role="tab" href="{% url 'controlpanel:dashboard' %}" class="tab gap-2 {% if nav == 'dashboard' %}tab-active{% endif %}">{% lucide "layout-dashboard" size=16 %} Dashboard</a>
|
||||
<a role="tab" href="{% url 'controlpanel:club_list' %}" class="tab gap-2 {% if nav == 'clubs' %}tab-active{% endif %}">{% lucide "building-2" size=16 %} Clubs</a>
|
||||
<a role="tab" href="{% url 'controlpanel:billing' %}" class="tab gap-2 {% if nav == 'billing' %}tab-active{% endif %}">{% lucide "receipt-euro" size=16 %} Billing</a>
|
||||
<a role="tab" href="{% url 'controlpanel:features' %}" class="tab gap-2 {% if nav == 'features' %}tab-active{% endif %}">{% lucide "toggle-right" size=16 %} Features</a>
|
||||
{% if user.is_superuser %}
|
||||
<a role="tab" href="{% url 'controlpanel:admins' %}" class="tab gap-2 {% if nav == 'admins' %}tab-active{% endif %}">{% lucide "user-cog" size=16 %} Admins</a>
|
||||
|
||||
117
controlpanel/templates/controlpanel/billing.html
Normal file
117
controlpanel/templates/controlpanel/billing.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
{% load lucide %}
|
||||
|
||||
{% block heading %}Billing{% endblock heading %}
|
||||
{% block subheading %}<p class="text-sm opacity-70">What the platform charges its clubs.</p>{% endblock subheading %}
|
||||
|
||||
{% block actions %}
|
||||
<a class="btn btn-primary gap-2" href="{% url 'controlpanel:tier_create' %}">{% lucide "plus" size=16 %} New tier</a>
|
||||
{% endblock actions %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="card mb-6 bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "layers" size=18 %} Tiers</h2>
|
||||
{% comment %}
|
||||
Prices are dated, not edited. A rate change is a new row with a future
|
||||
active_from; every period already opened keeps the amount it was billed at,
|
||||
so raising the price cannot rewrite an invoice you have already sent.
|
||||
{% endcomment %}
|
||||
<p class="text-sm opacity-70">A rate change is a new dated price. Periods already billed keep the amount they were issued at.</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tier</th>
|
||||
<th class="text-right">Clubs</th>
|
||||
<th>Prices</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tier in tiers %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-medium">{{ tier.name }}</div>
|
||||
{% if not tier.is_active %}<span class="badge badge-ghost badge-xs">Retired</span>{% endif %}
|
||||
{% if tier.description %}<div class="text-xs opacity-60">{{ tier.description }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="text-right tabular-nums">{{ tier.club_count }}</td>
|
||||
<td>
|
||||
{% for price in tier.prices.all %}
|
||||
<div class="text-sm tabular-nums">
|
||||
€{{ price.amount|floatformat:2 }}
|
||||
<span class="opacity-60">from {{ price.active_from|date:"j M Y" }}</span>
|
||||
{% if price.active_from > today %}<span class="badge badge-info badge-xs">Scheduled</span>{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<span class="badge badge-error badge-sm">No price — cannot be billed</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-ghost btn-xs gap-1" href="{% url 'controlpanel:tier_price_create' tier.pk %}">{% lucide "euro" size=14 %} New price</a>
|
||||
<a class="btn btn-ghost btn-xs gap-1" href="{% url 'controlpanel:tier_update' tier.pk %}">{% lucide "pencil" size=14 %} Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center opacity-60">No tiers yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "receipt-euro" size=18 %} Owed</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Club</th>
|
||||
<th>Period</th>
|
||||
<th class="text-right">Owed</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for due in owing %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="link link-hover font-medium" href="{% url 'controlpanel:club_detail' due.club.pk %}">{{ due.club.name }}</a>
|
||||
<div class="text-xs opacity-60">{{ due.tier.name }}</div>
|
||||
</td>
|
||||
<td class="text-sm">
|
||||
{{ due.period_start|date:"j M Y" }} — {{ due.period_end|date:"j M Y" }}
|
||||
<div class="text-xs opacity-60">Grace to {{ due.grace_until|date:"j M Y" }}</div>
|
||||
</td>
|
||||
<td class="text-right font-semibold tabular-nums">€{{ due.balance|floatformat:2 }}</td>
|
||||
<td>
|
||||
{% if due.grace_until < today %}
|
||||
<span class="badge badge-error gap-1">{% lucide "triangle-alert" size=12 %} Overdue</span>
|
||||
{% elif due.period_end < today %}
|
||||
<span class="badge badge-warning gap-1">{% lucide "hourglass" size=12 %} In grace</span>
|
||||
{% else %}
|
||||
<span class="badge badge-ghost">{{ due.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-primary btn-xs gap-1" href="{% url 'controlpanel:due_pay' due.pk %}">{% lucide "banknote" size=14 %} Record payment</a>
|
||||
<a class="btn btn-ghost btn-xs gap-1" href="{% url 'controlpanel:due_invoice' due.pk %}">{% lucide "file-text" size=14 %} Invoice</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center opacity-60">Nothing outstanding.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock panel %}
|
||||
@@ -213,6 +213,102 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-6 bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">{% lucide "receipt-euro" size=18 %} Billing</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline btn-sm gap-2" href="{% url 'controlpanel:club_subscribe' club.pk %}">
|
||||
{% lucide "layers" size=14 %} {% if subscription %}Change plan{% else %}Start billing{% endif %}
|
||||
</a>
|
||||
{% if subscription %}
|
||||
<a class="btn btn-primary btn-sm gap-2" href="{% url 'controlpanel:club_open_period' club.pk %}">
|
||||
{% lucide "calendar-plus" size=14 %} {% if club.is_archived %}Reactivate{% else %}Open period{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not subscription %}
|
||||
<p class="text-sm opacity-70">This club is not billed for anything. Put it on a tier to start.</p>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-70">
|
||||
On <strong>{{ subscription.tier.name }}</strong>.
|
||||
{% if subscription.auto_archive %}
|
||||
Archived automatically when a period goes unpaid past its grace period.
|
||||
{% else %}
|
||||
<span class="badge badge-warning badge-sm">Auto-archive off</span> — it will never be archived for non-payment.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th class="text-right">Billed</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for due in dues %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ due.period_start|date:"j M Y" }} — {{ due.period_end|date:"j M Y" }}
|
||||
<div class="text-xs opacity-60">{{ due.tier.name }} · {{ due.invoice.number }} · grace to {{ due.grace_until|date:"j M Y" }}</div>
|
||||
</td>
|
||||
<td class="text-right tabular-nums">€{{ due.amount|floatformat:2 }}</td>
|
||||
<td class="text-right tabular-nums">€{{ due.amount_paid|floatformat:2 }}</td>
|
||||
<td>
|
||||
{% if due.status == "paid" %}
|
||||
<span class="badge badge-success gap-1">{% lucide "check" size=12 %} Paid</span>
|
||||
{% elif due.status == "waived" %}
|
||||
<span class="badge badge-ghost">Waived</span>
|
||||
{% elif due.grace_until < today %}
|
||||
<span class="badge badge-error gap-1">{% lucide "triangle-alert" size=12 %} Overdue</span>
|
||||
{% elif due.period_end < today %}
|
||||
<span class="badge badge-warning gap-1">{% lucide "hourglass" size=12 %} In grace</span>
|
||||
{% else %}
|
||||
<span class="badge badge-ghost">{{ due.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if due.is_owing %}
|
||||
<a class="btn btn-primary btn-xs gap-1" href="{% url 'controlpanel:due_pay' due.pk %}">{% lucide "banknote" size=14 %} Pay</a>
|
||||
{% if not due.payments.all %}
|
||||
<form class="inline" method="post" action="{% url 'controlpanel:due_waive' due.pk %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-ghost btn-xs gap-1" type="submit">{% lucide "ban" size=14 %} Waive</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a class="btn btn-ghost btn-xs gap-1" href="{% url 'controlpanel:due_invoice' due.pk %}">{% lucide "file-text" size=14 %} Invoice</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for payment in due.payments.all %}
|
||||
<tr class="text-xs opacity-70">
|
||||
<td colspan="2" class="pl-8">
|
||||
{% lucide "corner-down-right" size=12 %}
|
||||
{{ payment.paid_at|date:"j M Y" }} · {{ payment.get_method_display }}{% if payment.reference %} · {{ payment.reference }}{% endif %}
|
||||
</td>
|
||||
<td class="text-right tabular-nums">€{{ payment.amount|floatformat:2 }}</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center opacity-60">No periods billed yet.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -37,11 +37,18 @@
|
||||
<div class="text-xs opacity-60">Admins locked out until they enrol</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow {% if attention.outstanding %}border-l-4 border-error{% endif %}">
|
||||
<div class="card bg-base-100 shadow {% if attention.dues_owed %}border-l-4 border-error{% endif %}">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "banknote" size=16 %} Outstanding</div>
|
||||
<div class="text-3xl font-bold tabular-nums">€{{ attention.outstanding|floatformat:2 }}</div>
|
||||
<div class="text-xs opacity-60">Unpaid across every club</div>
|
||||
{% comment %}
|
||||
What the CLUBS owe US. Not to be confused with the club-shop money below,
|
||||
which members owe their clubs and is never ours.
|
||||
{% endcomment %}
|
||||
<div class="flex items-center gap-2 text-sm opacity-70">{% lucide "receipt-euro" size=16 %} Dues owed</div>
|
||||
<div class="text-3xl font-bold tabular-nums">€{{ attention.dues_owed|floatformat:2 }}</div>
|
||||
<div class="text-xs opacity-60">
|
||||
{{ attention.dues_in_grace }} in grace ·
|
||||
<span class="{% if attention.dues_overdue %}font-semibold text-error{% endif %}">{{ attention.dues_overdue }} overdue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +65,8 @@
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "euro" size=18 %} Revenue per month</h2>
|
||||
<h2 class="card-title text-base">{% lucide "receipt-euro" size=18 %} Platform dues per month</h2>
|
||||
<p class="text-sm opacity-70">What clubs paid us. Club-shop money is theirs, not ours.</p>
|
||||
<div class="h-56">
|
||||
<canvas id="revenue-chart"></canvas>
|
||||
</div>
|
||||
@@ -138,6 +146,11 @@
|
||||
<div class="stat-title">Club admins</div>
|
||||
<div class="stat-value">{{ totals.admins }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Not billed</div>
|
||||
<div class="stat-value {% if attention.clubs_unbilled %}text-warning{% endif %}">{{ attention.clubs_unbilled }}</div>
|
||||
<div class="stat-desc">Clubs on no tier</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
@@ -221,7 +234,7 @@
|
||||
},
|
||||
});
|
||||
|
||||
return [signups, build("revenue-chart", "Revenue", data.revenue, css("--color-accent", "#0ea5e9"), "line", true)];
|
||||
return [signups, build("revenue-chart", "Dues", data.dues, css("--color-accent", "#0ea5e9"), "bar", true)];
|
||||
};
|
||||
|
||||
let charts = render();
|
||||
|
||||
14
controlpanel/templates/controlpanel/payment_form.html
Normal file
14
controlpanel/templates/controlpanel/payment_form.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
|
||||
{% block heading %}Record payment — {{ due.club }}{% endblock heading %}
|
||||
|
||||
{% block subheading %}
|
||||
<p class="text-sm opacity-70">
|
||||
{{ due.period_start|date:"j M Y" }} to {{ due.period_end|date:"j M Y" }} · €{{ due.amount|floatformat:2 }} billed · €{{ due.balance|floatformat:2 }} outstanding
|
||||
</p>
|
||||
{% endblock subheading %}
|
||||
|
||||
{% block panel %}
|
||||
{% url 'controlpanel:club_detail' due.club.pk as club_url %}
|
||||
{% include "controlpanel/_billing_form.html" with cancel_url=club_url submit_label="Record payment" submit_icon="banknote" blurb="Part payments are fine: they accumulate against the period until it is settled." %}
|
||||
{% endblock panel %}
|
||||
12
controlpanel/templates/controlpanel/period_form.html
Normal file
12
controlpanel/templates/controlpanel/period_form.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
|
||||
{% block heading %}{% if club.is_archived %}Reactivate{% else %}Open period{% endif %} — {{ club }}{% endblock heading %}
|
||||
|
||||
{% block subheading %}
|
||||
<p class="text-sm opacity-70">Next period starts {{ next_start|date:"j M Y" }} unless you say otherwise.</p>
|
||||
{% endblock subheading %}
|
||||
|
||||
{% block panel %}
|
||||
{% url 'controlpanel:club_detail' club.pk as club_url %}
|
||||
{% include "controlpanel/_billing_form.html" with cancel_url=club_url submit_label="Open period" submit_icon="calendar-plus" blurb="By default the period continues from the end of the last one, so a lapsed year is still owed. Pick a start date to forgive the gap." %}
|
||||
{% endblock panel %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
|
||||
{% block heading %}{% if subscription %}Change plan{% else %}Start billing{% endif %} — {{ club }}{% endblock heading %}
|
||||
|
||||
{% block panel %}
|
||||
{% url 'controlpanel:club_detail' club.pk as club_url %}
|
||||
{% include "controlpanel/_billing_form.html" with cancel_url=club_url submit_label="Save plan" submit_icon="layers" blurb="Changing tier does not re-bill: the current period keeps the amount it was issued at, and the new rate applies from the next one." %}
|
||||
{% endblock panel %}
|
||||
8
controlpanel/templates/controlpanel/tier_form.html
Normal file
8
controlpanel/templates/controlpanel/tier_form.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
|
||||
{% block heading %}{% if object %}Edit {{ object }}{% else %}New tier{% endif %}{% endblock heading %}
|
||||
|
||||
{% block panel %}
|
||||
{% url 'controlpanel:billing' as billing_url %}
|
||||
{% include "controlpanel/_billing_form.html" with cancel_url=billing_url submit_label="Save tier" submit_icon="layers" blurb="A tier has no price until you add one. A tier with no price cannot be billed." %}
|
||||
{% endblock panel %}
|
||||
8
controlpanel/templates/controlpanel/tier_price_form.html
Normal file
8
controlpanel/templates/controlpanel/tier_price_form.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "controlpanel/base.html" %}
|
||||
|
||||
{% block heading %}New price for {{ tier }}{% endblock heading %}
|
||||
|
||||
{% block panel %}
|
||||
{% url 'controlpanel:billing' as billing_url %}
|
||||
{% include "controlpanel/_billing_form.html" with cancel_url=billing_url submit_label="Add price" submit_icon="euro" blurb="Prices are dated, never edited: periods already opened keep the amount they were billed at, so this cannot rewrite an invoice you have already sent." %}
|
||||
{% endblock panel %}
|
||||
Reference in New Issue
Block a user