forked from CGM_Public/pretix_original
Connect giftcards with customer accounts (#5126)
Connect giftcards with customer accounts, show giftcards during checkout and in account , show giftcard list in backend customer view
This commit is contained in:
19
src/pretix/base/migrations/0292_giftcard_customer.py
Normal file
19
src/pretix/base/migrations/0292_giftcard_customer.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.19 on 2025-05-19 11:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0291_alter_logentry_object_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='giftcard',
|
||||
name='customer',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='customer_gift_cards', to='pretixbase.customer'),
|
||||
),
|
||||
]
|
||||
@@ -19,6 +19,8 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from decimal import Decimal
|
||||
|
||||
import pycountry
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import (
|
||||
@@ -27,7 +29,11 @@ from django.contrib.auth.hashers import (
|
||||
from django.core.validators import RegexValidator, URLValidator
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.aggregates import Sum
|
||||
from django.db.models.expressions import OuterRef, Subquery
|
||||
from django.db.models.functions.comparison import Coalesce
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField
|
||||
@@ -36,6 +42,7 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.giftcards import GiftCardTransaction
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.helpers.countries import FastCountryField
|
||||
@@ -288,6 +295,19 @@ class Customer(LoggedModel):
|
||||
organizer=self.organizer,
|
||||
)
|
||||
|
||||
def usable_gift_cards(self, used_cards=[]):
|
||||
s = GiftCardTransaction.objects.filter(
|
||||
card=OuterRef('pk')
|
||||
).order_by().values('card').annotate(s=Sum('value')).values('s')
|
||||
qs = self.customer_gift_cards.annotate(
|
||||
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
|
||||
)
|
||||
ne_qs = qs.filter(
|
||||
Q(expires__isnull=True) | Q(expires__gte=now()),
|
||||
)
|
||||
ex_qs = ne_qs.exclude(id__in=used_cards)
|
||||
return ex_qs.filter(cached_value__gt=0)
|
||||
|
||||
|
||||
class AttendeeProfile(models.Model):
|
||||
customer = models.ForeignKey(
|
||||
|
||||
@@ -80,6 +80,13 @@ class GiftCard(LoggedModel):
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Owned by ticket holder')
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
'Customer',
|
||||
related_name='customer_gift_cards',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True,
|
||||
verbose_name=_('Owned by customer account')
|
||||
)
|
||||
issuance = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ import json
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from functools import cached_property
|
||||
from typing import Any, Dict, Union
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -57,8 +58,8 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
|
||||
OrderRefund, Quota, TaxRule,
|
||||
CartPosition, Customer, Event, GiftCard, InvoiceAddress, Order,
|
||||
OrderPayment, OrderRefund, Quota, TaxRule,
|
||||
)
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
@@ -99,6 +100,7 @@ class PaymentProviderForm(Form):
|
||||
|
||||
class GiftCardPaymentForm(PaymentProviderForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.customer_gift_cards = kwargs.pop('customer_gift_cards') if 'customer_gift_cards' in kwargs else None
|
||||
self.event = kwargs.pop('event')
|
||||
self.testmode = kwargs.pop('testmode')
|
||||
self.positions = kwargs.pop('positions')
|
||||
@@ -1373,6 +1375,31 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
payment_form_class = GiftCardPaymentForm
|
||||
payment_form_template_name = 'pretixcontrol/giftcards/checkout.html'
|
||||
|
||||
@cached_property
|
||||
def customer_gift_cards(self):
|
||||
if not self.request:
|
||||
return None
|
||||
if not self.used_cards:
|
||||
self.used_cards = []
|
||||
cs = None
|
||||
if 'checkout' in self.request.resolver_match.url_name:
|
||||
cs = cart_session(self.request)
|
||||
customer = getattr(self.request, "customer", None)
|
||||
if customer:
|
||||
return customer.usable_gift_cards(self.used_cards)
|
||||
elif cs and cs.get('customer_mode', 'guest') == 'login':
|
||||
try:
|
||||
customer = self.request.organizer.customers.get(pk=cs["customer"])
|
||||
return customer.usable_gift_cards(self.used_cards)
|
||||
except Customer.DoesNotExist:
|
||||
return None
|
||||
|
||||
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order = None) -> str:
|
||||
form = self.payment_form(request)
|
||||
template = get_template(self.payment_form_template_name)
|
||||
ctx = {'request': request, 'form': form, 'customer_gift_cards': form.customer_gift_cards, }
|
||||
return template.render(ctx)
|
||||
|
||||
def payment_form(self, request: HttpRequest) -> Form:
|
||||
# Unfortunately, in payment_form we do not know if we're in checkout
|
||||
# or in an existing order. But we need to do the validation logic in the
|
||||
@@ -1392,8 +1419,12 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
positions = order.positions.all()
|
||||
testmode = order.testmode
|
||||
|
||||
self.request = request
|
||||
self.used_cards = used_cards
|
||||
|
||||
form = self.payment_form_class(
|
||||
event=self.event,
|
||||
customer_gift_cards=self.customer_gift_cards,
|
||||
used_cards=used_cards,
|
||||
positions=positions,
|
||||
testmode=testmode,
|
||||
|
||||
@@ -3087,6 +3087,7 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
|
||||
expires=order.event.organizer.default_gift_card_expiry if giftcard_expires is _unset else giftcard_expires,
|
||||
conditions=giftcard_conditions,
|
||||
currency=order.event.currency,
|
||||
customer=order.customer,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', data={})
|
||||
|
||||
@@ -731,6 +731,21 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
kwargs['initial'] = initial
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.organizer.settings.customer_accounts:
|
||||
self.fields['customer'].queryset = self.organizer.customers.all()
|
||||
self.fields['customer'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
|
||||
'organizer': self.organizer.slug,
|
||||
}),
|
||||
}
|
||||
)
|
||||
self.fields['customer'].widget.choices = self.fields['customer'].choices
|
||||
self.fields['customer'].required = False
|
||||
else:
|
||||
del self.fields['customer']
|
||||
|
||||
def clean_secret(self):
|
||||
s = self.cleaned_data['secret']
|
||||
if GiftCard.objects.filter(
|
||||
@@ -749,9 +764,10 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['secret', 'currency', 'testmode', 'expires', 'conditions']
|
||||
fields = ['secret', 'currency', 'testmode', 'expires', 'conditions', 'customer']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField
|
||||
'expires': SplitDateTimeField,
|
||||
'customer': SafeModelChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
@@ -762,10 +778,11 @@ class GiftCardCreateForm(forms.ModelForm):
|
||||
class GiftCardUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ['expires', 'conditions', 'owner_ticket']
|
||||
fields = ['expires', 'conditions', 'owner_ticket', 'customer']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField,
|
||||
'owner_ticket': SafeOrderPositionChoiceField,
|
||||
'customer': SafeModelChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
@@ -788,6 +805,21 @@ class GiftCardUpdateForm(forms.ModelForm):
|
||||
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
|
||||
self.fields['owner_ticket'].required = False
|
||||
|
||||
if organizer.settings.customer_accounts:
|
||||
self.fields['customer'].queryset = organizer.customers.all()
|
||||
self.fields['customer'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
|
||||
'organizer': organizer.slug,
|
||||
}),
|
||||
}
|
||||
)
|
||||
self.fields['customer'].widget.choices = self.fields['customer'].choices
|
||||
self.fields['customer'].required = False
|
||||
else:
|
||||
del self.fields['customer']
|
||||
|
||||
|
||||
class ReusableMediumUpdateForm(forms.ModelForm):
|
||||
error_messages = {
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% load rich_text %}
|
||||
|
||||
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
|
||||
|
||||
{% if customer_gift_cards %}
|
||||
<p><strong>
|
||||
<span class="sr-only">{% trans "Information" %}</span>
|
||||
{% trans "The following gift cards are available in your customer account:" %}
|
||||
</strong></p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<ul class="list-group">
|
||||
{% for c in customer_gift_cards %}
|
||||
<li class="list-group-item row row-no-gutters">
|
||||
<div class="col-sm-8 col-md-9" id="gc-code-{{ forloop.counter }}">
|
||||
{{ c }}
|
||||
</div>
|
||||
<div class="col-sm-2 text-right" id="gc-value-{{ forloop.counter }}">
|
||||
{{ c.value|money:c.currency }}
|
||||
</div>
|
||||
<div class="col-sm-2 col-md-1 text-right">
|
||||
<button name="use_giftcard" class="btn btn-primary btn-xs use_giftcard" data-value="{{ c.secret }}"
|
||||
title="{% trans "Use gift card" %}"
|
||||
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}">
|
||||
{% trans "Apply" %}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% bootstrap_form form layout='checkout' %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
{% load money %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
@@ -278,6 +279,53 @@
|
||||
</table>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
</div>
|
||||
<div class="panel panel-default items">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Gift cards" %}
|
||||
</h3>
|
||||
</div>
|
||||
<table class="panel-body table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Gift card code" %}
|
||||
<th>{% trans "Creation date" %}
|
||||
<th>{% trans "Expiry date" %}
|
||||
<th>{% trans "Last transaction" %}
|
||||
<th class="text-right">{% trans "Current value" %}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gc in gift_cards %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
|
||||
<strong>{{ gc.secret }}</strong></a>
|
||||
{% if gc.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if gc.expired %}
|
||||
<span class="label label-danger">{% trans "Expired" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td>{% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
|
||||
<td>{% if gc.last_tx %}{{ gc.last_tx|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
|
||||
<td class="text-right flip">
|
||||
<p class="text-right">{{ gc.value|money:gc.currency }}</p>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
|
||||
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -54,6 +54,14 @@
|
||||
{{ card.owner_ticket.order.code }}</a>-{{ card.owner_ticket.positionid }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if card.customer %}
|
||||
<dt>{% trans "Customer account" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=card.customer.identifier %}">
|
||||
{{ card.customer.identifier }} – {{ card.customer.email }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
{% bootstrap_field form.testmode layout="control" %}
|
||||
{% bootstrap_field form.expires layout="control" %}
|
||||
{% bootstrap_field form.conditions layout="control" %}
|
||||
{% if form.customer %}
|
||||
{% bootstrap_field form.customer layout="control" %}
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{% bootstrap_field form.expires layout="control" %}
|
||||
{% bootstrap_field form.owner_ticket layout="control" %}
|
||||
{% bootstrap_field form.conditions layout="control" %}
|
||||
{% if form.customer %}
|
||||
{% bootstrap_field form.customer layout="control" %}
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -1221,6 +1221,7 @@ class OrderRefundView(OrderView):
|
||||
giftcard = self.request.organizer.issued_gift_cards.create(
|
||||
expires=expires,
|
||||
currency=self.request.event.currency,
|
||||
customer=order.customer,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||
|
||||
@@ -3085,6 +3085,8 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
.annotate(spending=Sum("total"))
|
||||
)
|
||||
|
||||
ctx["gift_cards"] = self.customer.customer_gift_cards.all()
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,34 @@
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h3 class="sr-only">{% trans "Payment" %}</h3>
|
||||
{% if customer_gift_cards %}
|
||||
<p><strong>
|
||||
<span class="sr-only">{% trans "Information" %}</span>
|
||||
{% trans "The following gift cards are available in your customer account:" %}
|
||||
</strong></p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<ul class="list-group">
|
||||
{% for c in customer_gift_cards %}
|
||||
<li class="list-group-item row">
|
||||
<div class="col-xs-9" id="gc-code-{{ forloop.counter }}">
|
||||
{{ c }}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right" id="gc-value-{{ forloop.counter }}">
|
||||
{{ c.value|money:c.currency }}
|
||||
</div>
|
||||
<div class="col-xs-1 text-right">
|
||||
<button name="use_giftcard" value="{{ c.secret }}" title="{% trans "Use gift card" %}"
|
||||
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}"
|
||||
class="btn btn-primary btn-xs">
|
||||
{% trans "Apply" %}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if current_payments %}
|
||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||
<form method="post">
|
||||
@@ -77,6 +105,15 @@
|
||||
aria-controls="payment_{{ p.provider.identifier }}"
|
||||
data-wallets="{{ p.provider.walletqueries|join:"|" }}" />
|
||||
<strong class="accordion-label-text">{{ p.provider.public_name }}</strong>
|
||||
{% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %}
|
||||
<small>
|
||||
{% blocktrans trimmed count count=p.provider.customer_gift_cards|length %}
|
||||
({{ count }} available)
|
||||
{% plural %}
|
||||
({{ count }} available)
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
</legend>
|
||||
|
||||
@@ -40,6 +40,15 @@
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
data-wallets="{{ p.provider.walletqueries|join:"|" }}"/>
|
||||
<strong class="accordion-label-text">{{ p.provider.public_name }}</strong>
|
||||
{% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %}
|
||||
<small>
|
||||
{% blocktrans trimmed count count=p.provider.customer_gift_cards|length %}
|
||||
({{ count }} available)
|
||||
{% plural %}
|
||||
({{ count }} available)
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
</legend>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "pretixpresale/organizers/customer_base.html" %}
|
||||
{% load money %}
|
||||
{% load i18n %}
|
||||
{% load icon %}
|
||||
{% load eventurl %}
|
||||
{% load textbubble %}
|
||||
{% block title %}{% trans "Gift cards" %}{% endblock %}
|
||||
{% block inner %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% icon "gift" %}
|
||||
<strong>{% trans "Gift cards" %}</strong> ({{ page_obj.paginator.count }})
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if gift_cards %}
|
||||
<div class="event-list full-width-list alternating-rows">
|
||||
{% for gc in gift_cards %}
|
||||
<article class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>
|
||||
{% icon "gift" %} {{ gc }}
|
||||
</h4>
|
||||
{% if gc.issuance %}
|
||||
<p class="text-muted">
|
||||
{% icon "calendar" %}
|
||||
{% blocktrans trimmed with date=gc.issuance|date:"SHORT_DATE_FORMAT" %}
|
||||
Issued on {{ date }}
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
{% if gc.expired %}
|
||||
{% icon "clock-o" %}
|
||||
{% if gc.expires %}
|
||||
{% blocktrans trimmed with date=gc.expires|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Expired since {{ date }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Expired" %}
|
||||
{% endif %}
|
||||
{% elif gc.expires %}
|
||||
{% icon "check" %}
|
||||
{% blocktrans trimmed with date=gc.expires|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Valid until {{ date }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% icon "check" %}
|
||||
{% trans "Valid" %}
|
||||
{% endif %}
|
||||
</small>
|
||||
</p>
|
||||
{% if gc.testmode %}
|
||||
<p>
|
||||
<small>
|
||||
{% textbubble "warning" %}
|
||||
{% trans "TEST MODE" %}
|
||||
{% endtextbubble %}
|
||||
</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
{% blocktrans trimmed with value=gc.value|money:gc.currency %}
|
||||
Remaining value:
|
||||
{% endblocktrans %}
|
||||
<p class="text-right">{{ value }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">
|
||||
{% trans "You don’t have any gift cards in your account currently." %}
|
||||
{% trans "Currently, only gift cards resulting from refunds show up here, any purchased gift cards show up under the orders tab." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
@@ -214,6 +214,8 @@ organizer_patterns = [
|
||||
re_path(r'^account/confirmchange$', pretix.presale.views.customer.ConfirmChangeView.as_view(), name='organizer.customer.change.confirm'),
|
||||
re_path(r'^account/memberships$', pretix.presale.views.customer.MembershipView.as_view(), name='organizer.customer.memberships'),
|
||||
re_path(r'^account/memberships/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
|
||||
re_path(r'^account/giftcards$', pretix.presale.views.customer.GiftcardView.as_view(),
|
||||
name='organizer.customer.giftcards'),
|
||||
re_path(r'^account/addresses$', pretix.presale.views.customer.AddressView.as_view(), name='organizer.customer.addresses'),
|
||||
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$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profiles'),
|
||||
|
||||
@@ -368,6 +368,12 @@ class CustomerAccountBaseMixin(CustomerRequiredMixin):
|
||||
'active': url_name.startswith('organizer.customer.membership'),
|
||||
'icon': 'id-badge',
|
||||
},
|
||||
{
|
||||
'label': _('Gift cards'),
|
||||
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.giftcards', kwargs={}),
|
||||
'active': url_name.startswith('organizer.customer.giftcard'),
|
||||
'icon': 'gift',
|
||||
},
|
||||
{
|
||||
'label': _('Addresses'),
|
||||
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.addresses', kwargs={}),
|
||||
@@ -461,6 +467,15 @@ class MembershipUsageView(CustomerAccountBaseMixin, ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class GiftcardView(CustomerAccountBaseMixin, ListView):
|
||||
template_name = 'pretixpresale/organizers/customer_giftcards.html'
|
||||
context_object_name = 'gift_cards'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.customer.customer_gift_cards.all()
|
||||
|
||||
|
||||
class AddressView(CustomerAccountBaseMixin, ListView):
|
||||
template_name = 'pretixpresale/organizers/customer_addresses.html'
|
||||
context_object_name = 'invoice_addresses'
|
||||
|
||||
@@ -222,6 +222,12 @@ var form_handlers = function (el) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
el.find('.use_giftcard').on("click", function () {
|
||||
var value = $(this).data('value');
|
||||
$('#id_payment_giftcard-code').val(value)
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
function setup_basics(el) {
|
||||
|
||||
@@ -5488,3 +5488,22 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
with scopes_disabled():
|
||||
assert order.positions.first().used_membership == m_correct1
|
||||
assert order.positions.first().attendee_name == 'John Doe'
|
||||
|
||||
def test_giftcard_customer_offered(self):
|
||||
with scopes_disabled():
|
||||
gc = self.orga.issued_gift_cards.create(currency="EUR", customer=self.customer)
|
||||
gc.transactions.create(value=23, acceptor=self.orga)
|
||||
ugc = self.customer.usable_gift_cards()
|
||||
assert len(ugc) == 1
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
|
||||
'customer_mode': 'login',
|
||||
'login-email': 'john@example.org',
|
||||
'login-password': 'foo',
|
||||
}, follow=True)
|
||||
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
assert 'Gift card' in response.content.decode()
|
||||
assert '(1 available)' in response.content.decode()
|
||||
|
||||
@@ -1915,6 +1915,47 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
r = self.order.refunds.get()
|
||||
assert r.provider == 'giftcard'
|
||||
|
||||
def test_refund_giftcard_to_customer_account(self):
|
||||
with scopes_disabled():
|
||||
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
customer.save()
|
||||
|
||||
self.order.customer = customer
|
||||
self.event.settings.cancel_allow_user_paid_refund_as_giftcard = 'force'
|
||||
with scopes_disabled():
|
||||
gc = customer.usable_gift_cards()
|
||||
assert len(gc) == 0
|
||||
OrderPosition.objects.create(
|
||||
order=self.order,
|
||||
item=self.workshop1,
|
||||
variation=None,
|
||||
price=Decimal("12"),
|
||||
addon_to=self.ticket_pos,
|
||||
attendee_name_parts={'full_name': "Peter"},
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.total += Decimal("12")
|
||||
self.order.save()
|
||||
self.order.payments.create(provider='testdummy_partialrefund', amount=self.order.total,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED)
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {},
|
||||
follow=True
|
||||
)
|
||||
doc = BeautifulSoup(response.content.decode(), "lxml")
|
||||
form_data = extract_form_fields(doc.select('.main-box form')[0])
|
||||
form_data['confirm'] = 'true'
|
||||
self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
form_data, follow=True
|
||||
)
|
||||
|
||||
with scopes_disabled():
|
||||
gc = customer.usable_gift_cards()
|
||||
assert len(gc) == 1
|
||||
|
||||
def test_attendee(self):
|
||||
self.workshop2a.default_price = Decimal('0.00')
|
||||
self.workshop2a.save()
|
||||
|
||||
@@ -702,6 +702,46 @@ class OrdersTest(BaseOrdersTest):
|
||||
assert r.provider == "giftcard"
|
||||
assert r.amount == Decimal('20.00')
|
||||
|
||||
def test_orders_cancel_autorefund_gift_card_customer(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
with scopes_disabled():
|
||||
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
customer.save()
|
||||
self.order.customer = customer
|
||||
self.order.save()
|
||||
with scopes_disabled():
|
||||
self.order.payments.create(provider='testdummy_partialrefund', amount=self.order.total,
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED)
|
||||
self.event.settings.cancel_allow_user_paid = True
|
||||
self.event.settings.cancel_allow_user_paid_refund_as_giftcard = 'option'
|
||||
gc = customer.usable_gift_cards()
|
||||
assert len(gc) == 0
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert 'manually' not in response.content.decode()
|
||||
assert "gift card" in response.content.decode()
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/cancel/do' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'giftcard': 'true'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response,
|
||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret),
|
||||
target_status_code=200)
|
||||
assert "gift card" in response.content.decode()
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
with scopes_disabled():
|
||||
r = self.order.refunds.get()
|
||||
assert r.provider == "giftcard"
|
||||
assert r.amount == Decimal('23.00')
|
||||
gc = customer.usable_gift_cards()
|
||||
assert len(gc) == 1
|
||||
|
||||
def test_orders_cancel_unpaid_fee(self):
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.save()
|
||||
@@ -1583,6 +1623,35 @@ class OrdersTest(BaseOrdersTest):
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
assert gc.value == Decimal('87.00')
|
||||
|
||||
def test_change_paymentmethod_customeraccount_giftcard_offered(self):
|
||||
with scopes_disabled():
|
||||
self.orga.settings.customer_accounts = True
|
||||
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
|
||||
customer.set_password('foo')
|
||||
customer.save()
|
||||
self.order.customer = customer
|
||||
self.order.payments.create(
|
||||
provider='manual',
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
amount=Decimal('10.00'),
|
||||
)
|
||||
gc = self.orga.issued_gift_cards.create(currency="EUR", customer=customer)
|
||||
gc.transactions.create(value=100, acceptor=self.orga)
|
||||
ugc = customer.usable_gift_cards()
|
||||
assert len(ugc) == 1
|
||||
r = self.client.post('/%s/account/login' % (self.orga.slug), {
|
||||
'email': 'john@example.org',
|
||||
'password': 'foo',
|
||||
})
|
||||
assert r.status_code == 302
|
||||
r = self.client.get('/%s/account/' % (self.orga.slug))
|
||||
assert r.status_code == 200
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
)
|
||||
assert 'Gift card' in response.content.decode()
|
||||
assert '1 available' in response.content.decode()
|
||||
|
||||
def test_answer_download_token(self):
|
||||
with scopes_disabled():
|
||||
q = self.event.questions.create(question="Foo", type="F")
|
||||
|
||||
Reference in New Issue
Block a user