forked from CGM_Public/pretix_original
Allow to save invoice addresses and attendee profiles to customer account (#2084)
Co-authored-by: Raphael Michel <michel@rami.io> Co-authored-by: Richard Schreiber <wiffbi@gmail.com> Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
@@ -59,6 +59,7 @@ from pretix.base.services.cart import (
|
||||
)
|
||||
from pretix.base.services.memberships import validate_memberships_in_order
|
||||
from pretix.base.services.orders import perform_order
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import validate_cart_addons
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.base.templatetags.rich_text import rich_text_snippet
|
||||
@@ -227,7 +228,7 @@ class TemplateFlowStep(TemplateResponseMixin, BaseCheckoutFlowStep):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CustomerStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
class CustomerStep(CartMixin, TemplateFlowStep):
|
||||
priority = 45
|
||||
identifier = "customer"
|
||||
template_name = "pretixpresale/event/checkout_customer.html"
|
||||
@@ -352,7 +353,7 @@ class CustomerStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
return ctx
|
||||
|
||||
|
||||
class MembershipStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
class MembershipStep(CartMixin, TemplateFlowStep):
|
||||
priority = 47
|
||||
identifier = "membership"
|
||||
template_name = "pretixpresale/event/checkout_membership.html"
|
||||
@@ -772,6 +773,9 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
'name_parts': self.cart_customer.name_parts
|
||||
})
|
||||
|
||||
if 'saved_invoice_address' in self.cart_session:
|
||||
initial['saved_id'] = self.cart_session['saved_invoice_address']
|
||||
|
||||
override_sets = self._contact_override_sets
|
||||
for overrides in override_sets:
|
||||
initial.update({
|
||||
@@ -791,6 +795,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
request=self.request,
|
||||
initial=initial,
|
||||
instance=self.invoice_address,
|
||||
allow_save=bool(self.cart_customer),
|
||||
validate_vat_id=self.eu_reverse_charge_relevant, all_optional=self.all_optional)
|
||||
for name, field in f.fields.items():
|
||||
if wd_initial.get(name) and wd.get('fix', '') == 'true':
|
||||
@@ -828,6 +833,23 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
self.cart_session['contact_form_data'] = d
|
||||
if self.address_asked or self.request.event.settings.invoice_name_required:
|
||||
addr = self.invoice_form.save()
|
||||
|
||||
if self.cart_customer and self.invoice_form.cleaned_data.get('save'):
|
||||
if self.invoice_form.cleaned_data.get('saved_id'):
|
||||
saved = InvoiceAddress.profiles.filter(
|
||||
customer=self.cart_customer, pk=self.invoice_form.cleaned_data.get('saved_id')
|
||||
).first() or InvoiceAddress(customer=self.cart_customer)
|
||||
else:
|
||||
saved = InvoiceAddress(customer=self.cart_customer)
|
||||
|
||||
for f in InvoiceAddress._meta.fields:
|
||||
if f.name not in ('order', 'customer', 'last_modified', 'pk', 'id'):
|
||||
val = getattr(addr, f.name)
|
||||
setattr(saved, f.name, copy.deepcopy(val))
|
||||
|
||||
saved.save()
|
||||
self.cart_session['saved_invoice_address'] = saved.pk
|
||||
|
||||
try:
|
||||
diff = update_tax_rates(
|
||||
event=request.event,
|
||||
@@ -946,6 +968,93 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
ctx['cart'] = self.get_cart()
|
||||
ctx['cart_session'] = self.cart_session
|
||||
ctx['invoice_address_asked'] = self.address_asked
|
||||
|
||||
if self.cart_customer:
|
||||
if self.address_asked:
|
||||
addresses = self.cart_customer.stored_addresses.all()
|
||||
addresses_list = []
|
||||
for a in addresses:
|
||||
data = {
|
||||
"_pk": a.pk,
|
||||
"_country_for_address": a.country.name,
|
||||
"_state_for_address": a.state_for_address,
|
||||
"_name": a.name,
|
||||
"is_business": "business" if a.is_business else "individual",
|
||||
}
|
||||
if a.name_parts:
|
||||
name_parts = a.name_parts
|
||||
# map full_name to name_parts and vice versa
|
||||
scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
|
||||
available_keys = name_parts.keys()
|
||||
asked_keys = [k for (k, l, w) in scheme["fields"]]
|
||||
if not set(available_keys).intersection(asked_keys):
|
||||
if "full_name" in available_keys:
|
||||
name_keys = ("given_name", "family_name")
|
||||
name_split = name_parts.get("full_name").rsplit(" ", 1)
|
||||
name_parts = dict(zip(name_keys, name_split))
|
||||
elif "full_name" in asked_keys:
|
||||
name_parts = {
|
||||
"full_name": a.name
|
||||
}
|
||||
for i, k in enumerate(asked_keys):
|
||||
data[f"name_parts_{i}"] = name_parts.get(k) or ""
|
||||
|
||||
for k in (
|
||||
"company", "street", "zipcode", "city", "country", "state",
|
||||
"state_for_address", "vat_id", "custom_field", "internal_reference", "beneficiary"
|
||||
):
|
||||
v = getattr(a, k) or ""
|
||||
# always add all values of an address even when empty,
|
||||
# so an address always gets fully overwritten client-side
|
||||
data[k] = str(v)
|
||||
|
||||
addresses_list.append(data)
|
||||
|
||||
ctx['addresses_data'] = addresses_list
|
||||
|
||||
profiles = list(self.cart_customer.attendee_profiles.all())
|
||||
profiles_list = []
|
||||
for p in profiles:
|
||||
data = {
|
||||
"_pk": p.pk,
|
||||
"_country_for_address": p.country.name,
|
||||
"_state_for_address": p.state_for_address,
|
||||
"_attendee_name": p.attendee_name,
|
||||
}
|
||||
if p.attendee_name_parts:
|
||||
name_parts = p.attendee_name_parts
|
||||
# map full_name to name_parts and vice versa
|
||||
scheme = PERSON_NAME_SCHEMES[self.request.event.settings.name_scheme]
|
||||
available_keys = name_parts.keys()
|
||||
asked_keys = [k for (k, l, w) in scheme["fields"]]
|
||||
if not set(available_keys).intersection(asked_keys):
|
||||
if "full_name" in available_keys:
|
||||
name_keys = ("given_name", "family_name")
|
||||
name_split = name_parts.get("full_name").rsplit(" ", 1)
|
||||
name_parts = dict(zip(name_keys, name_split))
|
||||
elif "full_name" in asked_keys:
|
||||
name_parts = {
|
||||
"full_name": p.attendee_name
|
||||
}
|
||||
|
||||
for i, k in enumerate(asked_keys):
|
||||
data[f"attendee_name_parts_{i}"] = name_parts.get(k) or ""
|
||||
|
||||
for k in ("attendee_email", "company", "street", "zipcode", "city", "country", "state"):
|
||||
v = getattr(p, k) or ""
|
||||
# always add all values of an address even when empty,
|
||||
# so an address always gets fully overwritten client-side
|
||||
data[k] = str(v)
|
||||
|
||||
for a in p.answers:
|
||||
data[a["field_name"]] = {
|
||||
"label": a["field_label"],
|
||||
"value": a["value"],
|
||||
"identifier": a["question_identifier"],
|
||||
"type": a["question_type"],
|
||||
}
|
||||
profiles_list.append(data)
|
||||
ctx['profiles_data'] = profiles_list
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +124,22 @@ class InvoiceAddressForm(BaseInvoiceAddressForm):
|
||||
required_css_class = 'required'
|
||||
vat_warning = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
allow_save = kwargs.pop('allow_save', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
if allow_save:
|
||||
self.fields['saved_id'] = forms.IntegerField(
|
||||
required=False,
|
||||
help_text=" ", # non-breaking-space, will be overwritten by JavaScript, needed here for HTML-output
|
||||
label=_("Save to address"),
|
||||
widget=forms.Select(choices=(("", _("Create new address")),))
|
||||
)
|
||||
self.fields['save'] = forms.BooleanField(
|
||||
label=_('Save address in my customer account for future purchases'),
|
||||
required=False,
|
||||
initial=False,
|
||||
)
|
||||
|
||||
|
||||
class InvoiceNameForm(InvoiceAddressForm):
|
||||
|
||||
@@ -142,6 +158,22 @@ class QuestionsForm(BaseQuestionsForm):
|
||||
"""
|
||||
required_css_class = 'required'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
allow_save = kwargs.pop('allow_save', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
if allow_save and self.fields:
|
||||
self.fields['save'] = forms.BooleanField(
|
||||
label=_('Save answers to my customer profiles for future purchases'),
|
||||
required=False,
|
||||
initial=False,
|
||||
)
|
||||
self.fields['saved_id'] = forms.IntegerField(
|
||||
required=False,
|
||||
help_text=" ", # non-breaking-space, will be overwritten by JavaScript, needed here for HTML-output
|
||||
label=_("Save to profile"),
|
||||
widget=forms.Select(choices=(("", _("Create new profile")),))
|
||||
)
|
||||
|
||||
|
||||
class AddOnRadioSelect(forms.RadioSelect):
|
||||
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
|
||||
|
||||
@@ -29,11 +29,11 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import pgettext
|
||||
|
||||
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False, is_valid=None):
|
||||
def render_label(content, label_for=None, label_class=None, label_title='', label_id='', optional=False, is_valid=None, attrs=None):
|
||||
"""
|
||||
Render a label with content
|
||||
"""
|
||||
attrs = {}
|
||||
attrs = attrs or {}
|
||||
if label_for:
|
||||
attrs['for'] = label_for
|
||||
if label_class:
|
||||
@@ -118,6 +118,7 @@ class CheckoutFieldRenderer(FieldRenderer):
|
||||
widget.attrs["aria-describedby"] = " ".join(help_ids)
|
||||
|
||||
def add_label(self, html):
|
||||
attrs = {}
|
||||
label = self.get_label()
|
||||
|
||||
if hasattr(self.field.field, '_show_required'):
|
||||
@@ -141,11 +142,15 @@ class CheckoutFieldRenderer(FieldRenderer):
|
||||
label_for = self.field.id_for_label
|
||||
label_id = ""
|
||||
|
||||
if hasattr(self.field.field, 'question') and self.field.field.question.identifier:
|
||||
attrs["data-identifier"] = self.field.field.question.identifier
|
||||
|
||||
html = render_label(
|
||||
label,
|
||||
label_for=label_for,
|
||||
label_class=self.get_label_class(),
|
||||
label_id=label_id,
|
||||
attrs=attrs,
|
||||
optional=not required and not isinstance(self.widget, CheckboxInput),
|
||||
is_valid=is_valid
|
||||
) + html
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% load lists %}
|
||||
{% load escapejson %}
|
||||
{% block inner %}
|
||||
<p>{% trans "Before we continue, we need you to answer some questions." %}</p>
|
||||
<p class="required-legend" aria-hidden="true">
|
||||
@@ -9,6 +11,9 @@
|
||||
You need to fill all fields that are marked with <span>*</span> to continue.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if profiles_data %}
|
||||
{{ profiles_data|json_script:"profiles_json" }}
|
||||
{% endif %}
|
||||
<form class="form-horizontal" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="panel-group" id="questions_group">
|
||||
@@ -39,8 +44,25 @@
|
||||
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
|
||||
</h4>
|
||||
</summary>
|
||||
<div id="invoice">
|
||||
{% if addresses_data %}
|
||||
{{ addresses_data|json_script:"addresses_json" }}
|
||||
{% endif %}
|
||||
<div id="invoice" class="profile-scope" data-profiles-id="addresses_json">
|
||||
<div class="panel-body">
|
||||
{% if addresses_data %}
|
||||
<div class="form-group profile-select-container">
|
||||
<label class="col-md-3 control-label" for="address-list-select">{% trans "Auto-fill with address" %}</label>
|
||||
<div class="col-md-9">
|
||||
<p class="profile-select-control">
|
||||
<select class="profile-select form-control" id="address-list-select"></select>
|
||||
</p>
|
||||
<p class="help-block profile-desc" id="selected-address-desc"></p>
|
||||
<p><button type="button" class="profile-apply btn btn-default" aria-describedby="selected-address-desc"
|
||||
><i class="fa fa-address-card-o fa-lg" aria-hidden="true"></i> {% trans "Fill form" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if event.settings.invoice_address_explanation_text %}
|
||||
<div>
|
||||
{{ event.settings.invoice_address_explanation_text|rich_text }}
|
||||
@@ -127,7 +149,7 @@
|
||||
{% for form in forms %}
|
||||
{% if form.pos.item != pos.item %}
|
||||
{# Add-Ons #}
|
||||
<legend>
|
||||
<legend{% if profiles_data %} class="profile-add-on-legend"{% endif %}>
|
||||
{% if form.show_copy_answers_to_addon_button and event.settings.checkout_show_copy_answers_button %}
|
||||
<span class="pull-right flip">
|
||||
<button type="button" data-id="{{ forloop.parentloop.counter0 }}" data-addonid="{{ forloop.counter0 }}" name="copy" class="js-copy-answers-addon btn btn-default btn-xs">{% trans "Copy answers" %}</button>
|
||||
@@ -136,7 +158,24 @@
|
||||
+ {{ form.pos.item.name }}{% if form.pos.variation %} – {{ form.pos.variation.value }}{% endif %}
|
||||
</legend>
|
||||
{% endif %}
|
||||
<div data-idx="{{ forloop.parentloop.counter0 }}" data-addonidx="{{ forloop.counter0 }}">
|
||||
<div data-idx="{{ forloop.parentloop.counter0 }}" data-addonidx="{{ forloop.counter0 }}" class="profile-scope{% if form.pos.item != pos.item %}{% if profiles_data %} profile-add-on{% endif %}{% endif %}">
|
||||
{% if profiles_data %}
|
||||
<div class="form-group profile-select-container">
|
||||
<label class="col-md-3 control-label" for="profile-select-{{ forloop.parentloop.counter0 }}-{{ forloop.counter0 }}">{% trans "Auto-fill with profile" %}</label>
|
||||
<div class="col-md-9">
|
||||
<p class="profile-select-control">
|
||||
<select class="profile-select form-control" id="profile-select-{{ forloop.parentloop.counter0 }}-{{ forloop.counter0 }}"></select>
|
||||
</p>
|
||||
<p class="help-block profile-desc" id="selected-profile-desc-{{ forloop.parentloop.counter0 }}-{{ forloop.counter0 }}"></p>
|
||||
<p>
|
||||
<button type="button" class="profile-apply btn btn-default"
|
||||
aria-describedby="selected-profile-desc-{{ forloop.parentloop.counter0 }}-{{ forloop.counter0 }}"
|
||||
><i class="fa fa-address-card-o fa-lg" aria-hidden="true"></i> {% trans "Fill form" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_form form layout="checkout" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% block title %}{% trans "Delete address" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% trans "Delete address" %}
|
||||
</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% trans "Do you really want to delete the following address from your account?" %}
|
||||
</p>
|
||||
<address>
|
||||
{{ address.describe|linebreaksbr }}
|
||||
</address>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}">
|
||||
{% trans "Go back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-sm-6">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -37,125 +37,203 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Memberships" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Membership type" %}</th>
|
||||
<th>{% trans "Valid from" %}</th>
|
||||
<th>{% trans "Valid until" %}</th>
|
||||
<th>{% trans "Attendee name" %}</th>
|
||||
<th>{% trans "Usages" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in memberships %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if m.canceled %}<del>{% endif %}
|
||||
{{ m.membership_type.name }}
|
||||
{% if m.canceled %}</del>{% endif %}
|
||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.attendee_name }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ m.percent }}">
|
||||
<div>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#orders" aria-controls="orders" role="tab" data-toggle="tab">{% trans "Orders" %}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#memberships" aria-controls="memberships" role="tab" data-toggle="tab">{% trans "Memberships" %}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#addresses" aria-controls="addresses" role="tab" data-toggle="tab">{% trans "Addresses" %}</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#profiles" aria-controls="profiles" role="tab" data-toggle="tab">{% trans "Attendee profiles" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="orders">
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Order total" %}</th>
|
||||
<th class="text-right">{% trans "Positions" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}" target="_blank">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.event }}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.customer_id != customer.pk %}
|
||||
<span class="fa fa-link text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Matched to the account based on the email address." %}"
|
||||
></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ o.total|money:o.event.currency }}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.count_positions|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixpresale/event/fragment_order_status.html" with order=o event=o.event %}</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}"
|
||||
target="_blank"
|
||||
class="btn btn-default">
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="memberships">
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Membership type" %}</th>
|
||||
<th>{% trans "Valid from" %}</th>
|
||||
<th>{% trans "Valid until" %}</th>
|
||||
<th>{% trans "Attendee name" %}</th>
|
||||
<th>{% trans "Usages" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in memberships %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if m.canceled %}<del>{% endif %}
|
||||
{{ m.membership_type.name }}
|
||||
{% if m.canceled %}</del>{% endif %}
|
||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_end|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.attendee_name }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="quotabox">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ m.percent }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ m.usages }} /
|
||||
{{ m.membership_type.max_usages|default_if_none:"∞" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ m.usages }} /
|
||||
{{ m.membership_type.max_usages|default_if_none:"∞" }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Details" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-list"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Orders" %}
|
||||
</h3>
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.membership" id=m.id %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Details" %}"
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-list"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No memberships are stored in your account." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="addresses">
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ia in invoice_addresses %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ ia.describe|linebreaksbr }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.address.delete" id=ia.id %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Delete" %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center">
|
||||
{% trans "No addresses are stored in your account." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="profiles">
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Profile" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ap in customer.attendee_profiles.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ ap.describe|linebreaksbr }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.profile.delete" id=ap.id %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Delete" %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-center">
|
||||
{% trans "No attendee profiles are stored in your account." %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<table class="panel-body table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Event" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right">{% trans "Order total" %}</th>
|
||||
<th class="text-right">{% trans "Positions" %}</th>
|
||||
<th class="text-right">{% trans "Status" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}" target="_blank">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.event }}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.customer_id != customer.pk %}
|
||||
<span class="fa fa-link text-muted"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Matched to the account based on the email address." %}"
|
||||
></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ o.total|money:o.event.currency }}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.count_positions|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixpresale/event/fragment_order_status.html" with order=o event=o.event %}</td>
|
||||
<td class="text-right flip">
|
||||
<a href="{% abseventurl o.event "presale:event.order" order=o.code secret=o.secret %}"
|
||||
target="_blank"
|
||||
class="btn btn-default">
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% block title %}{% trans "Delete profile" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% trans "Delete profile" %}
|
||||
</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% trans "Do you really want to delete the following profile from your account?" %}
|
||||
</p>
|
||||
<address>
|
||||
{{ profile.describe|linebreaksbr }}
|
||||
</address>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% abseventurl request.organizer "presale:organizer.customer.profile" %}">
|
||||
{% trans "Go back" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4 col-sm-6">
|
||||
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -179,6 +179,8 @@ organizer_patterns = [
|
||||
re_path(r'^account/change$', pretix.presale.views.customer.ChangeInformationView.as_view(), name='organizer.customer.change'),
|
||||
re_path(r'^account/confirmchange$', pretix.presale.views.customer.ConfirmChangeView.as_view(), name='organizer.customer.change.confirm'),
|
||||
re_path(r'^account/membership/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
|
||||
re_path(r'^account/addresses/(?P<id>\d+)/delete$', pretix.presale.views.customer.AddressDeleteView.as_view(), name='organizer.customer.address.delete'),
|
||||
re_path(r'^account/profiles/(?P<id>\d+)/delete$', pretix.presale.views.customer.ProfileDeleteView.as_view(), name='organizer.customer.profile.delete'),
|
||||
re_path(r'^account/$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profile'),
|
||||
]
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import FormView, ListView, View
|
||||
from django.views.generic import DeleteView, FormView, ListView, View
|
||||
|
||||
from pretix.base.models import Customer, Order, OrderPosition
|
||||
from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||
from pretix.presale.forms.customer import (
|
||||
@@ -299,6 +299,7 @@ class ProfileView(CustomerRequiredMixin, ListView):
|
||||
ctx['memberships'] = self.request.customer.memberships.with_usages().select_related(
|
||||
'membership_type', 'granted_in', 'granted_in__order', 'granted_in__order__event'
|
||||
)
|
||||
ctx['invoice_addresses'] = InvoiceAddress.profiles.filter(customer=self.request.customer)
|
||||
ctx['is_paginated'] = True
|
||||
|
||||
for m in ctx['memberships']:
|
||||
@@ -353,6 +354,28 @@ class MembershipUsageView(CustomerRequiredMixin, ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class AddressDeleteView(CustomerRequiredMixin, DeleteView):
|
||||
template_name = 'pretixpresale/organizers/customer_address_delete.html'
|
||||
context_object_name = 'address'
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
return get_object_or_404(InvoiceAddress.profiles, customer=self.request.customer, pk=self.kwargs.get('id'))
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
|
||||
|
||||
class ProfileDeleteView(CustomerRequiredMixin, DeleteView):
|
||||
template_name = 'pretixpresale/organizers/customer_profile_delete.html'
|
||||
context_object_name = 'profile'
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
return get_object_or_404(self.request.customer.attendee_profiles, pk=self.kwargs.get('id'))
|
||||
|
||||
def get_success_url(self):
|
||||
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
|
||||
|
||||
|
||||
class ChangePasswordView(CustomerRequiredMixin, FormView):
|
||||
template_name = 'pretixpresale/organizers/customer_password.html'
|
||||
form_class = ChangePasswordForm
|
||||
|
||||
@@ -47,3 +47,14 @@ class QuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
def _positions_for_questions(self):
|
||||
cart = get_cart(self.request)
|
||||
return sorted(list(cart), key=self._keyfunc)
|
||||
|
||||
def question_form_kwargs(self, cr):
|
||||
d = {
|
||||
'allow_save': bool(self.cart_customer),
|
||||
'initial': {},
|
||||
}
|
||||
|
||||
if f'saved_attendee_profile_{cr.pk}' in self.cart_session:
|
||||
d['initial']['saved_id'] = self.cart_session[f'saved_attendee_profile_{cr.pk}']
|
||||
|
||||
return d
|
||||
|
||||
Reference in New Issue
Block a user