forked from CGM_Public/pretix_original
Added voucher data model [backwards incompatible]
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-13 11:44
|
||||
# Generated by Django 1.9 on 2016-02-09 09:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@@ -46,17 +47,18 @@ class Migration(migrations.Migration):
|
||||
name='CartPosition',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cart_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
|
||||
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('cart_id', models.CharField(blank=True, max_length=255, null=True, verbose_name='Cart ID (e.g. session key)')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
|
||||
('expires', models.DateTimeField(verbose_name='Expiration date')),
|
||||
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Cart positions',
|
||||
'verbose_name': 'Cart position',
|
||||
},
|
||||
bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
@@ -161,7 +163,7 @@ class Migration(migrations.Migration):
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variations', to='pretixbase.Item')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('position',),
|
||||
'ordering': ('position', 'id'),
|
||||
'verbose_name_plural': 'Product variations',
|
||||
'verbose_name': 'Product variation',
|
||||
},
|
||||
@@ -211,7 +213,9 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Price')),
|
||||
('attendee_name', models.CharField(blank=True, help_text='Empty, if this product is not an admission ticket', max_length=255, null=True, verbose_name='Attendee name')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Item', verbose_name='Item')),
|
||||
('voucher_discount', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=10)),
|
||||
('base_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='positions', to='pretixbase.Order', verbose_name='Order')),
|
||||
('variation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation')),
|
||||
],
|
||||
@@ -219,7 +223,6 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Order positions',
|
||||
'verbose_name': 'Order position',
|
||||
},
|
||||
bases=(pretix.base.models.orders.ObjectWithAnswers, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Organizer',
|
||||
@@ -299,11 +302,33 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Voucher',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=255, verbose_name='Voucher code')),
|
||||
('valid_until', models.DateTimeField(blank=True, null=True, verbose_name='Valid until')),
|
||||
('block_quota', models.BooleanField(default=False, help_text="If activated, this voucher will be substracted from the affected product's quotas, such that it is guaranteed that anyone with this voucher code does receive a ticket.", verbose_name='Reserve ticket from quota')),
|
||||
('allow_ignore_quota', models.BooleanField(default=False, help_text='If activated, a holder of this voucher code can buy tickets, even if there are none left.', verbose_name='Allow to bypass quota')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Set product price to')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vouchers', to='pretixbase.Event', verbose_name='Event')),
|
||||
('item', models.ManyToManyField(help_text="This product is added to the user's cart if the voucher is redeemed.", related_name='vouchers', to='pretixbase.Item', verbose_name='Product')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Vouchers',
|
||||
'verbose_name': 'Voucher',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizer',
|
||||
name='permitted',
|
||||
field=models.ManyToManyField(related_name='organizers', through='pretixbase.OrganizerPermission', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='category',
|
||||
@@ -332,16 +357,25 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item', verbose_name='Item'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Item', verbose_name='Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='variation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation', verbose_name='Variation'),
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.ItemVariation', verbose_name='Variation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='voucher',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Voucher'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cachedticket',
|
||||
name='order',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='voucher',
|
||||
unique_together=set([('event', 'code')]),
|
||||
),
|
||||
]
|
||||
@@ -6,14 +6,16 @@ from .items import (
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .orders import (
|
||||
CachedTicket, CartPosition, ObjectWithAnswers, Order, OrderPosition,
|
||||
AbstractPosition, CachedTicket, CartPosition, Order, OrderPosition,
|
||||
QuestionAnswer, generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
||||
from .vouchers import Voucher
|
||||
|
||||
__all__ = [
|
||||
'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||
'ItemCategory', 'Item', 'ItemVariation', 'Question', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer',
|
||||
'ObjectWithAnswers', 'OrderPosition', 'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock',
|
||||
'cachedfile_name', 'itempicture_upload_to', 'generate_secret', 'LogEntry'
|
||||
'Versionable', 'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
|
||||
'BaseRestriction', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'AbstractPosition', 'OrderPosition',
|
||||
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
|
||||
'generate_secret', 'Voucher', 'LogEntry'
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
@@ -263,7 +264,63 @@ class QuestionAnswer(models.Model):
|
||||
answer = models.TextField()
|
||||
|
||||
|
||||
class ObjectWithAnswers:
|
||||
class AbstractPosition(models.Model):
|
||||
"""
|
||||
A position can either be one line of an order or an item placed in a cart.
|
||||
|
||||
:param item: The selected item
|
||||
:type item: Item
|
||||
:param variation: The selected ItemVariation or null, if the item has no properties
|
||||
:type variation: ItemVariation
|
||||
:param datetime: The datetime this item was put into the cart
|
||||
:type datetime: datetime
|
||||
:param expires: The date until this item is guarenteed to be reserved
|
||||
:type expires: datetime
|
||||
:param price: The price of this item
|
||||
:type price: decimal.Decimal
|
||||
:param attendee_name: The attendee's name, if entered.
|
||||
:type attendee_name: str
|
||||
:param voucher: A voucher that has been applied to this sale
|
||||
:type voucher: Voucher
|
||||
:param voucher_discount: The absolute discount granted by the applied voucher
|
||||
:type voucher_discount: decimal.Decimal
|
||||
:param base_price: The base price without any discounts applied
|
||||
:type base_price: decimal.Decimal
|
||||
"""
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
verbose_name=_("Item"),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
variation = models.ForeignKey(
|
||||
ItemVariation,
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Variation"),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
attendee_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Attendee name"),
|
||||
blank=True, null=True,
|
||||
help_text=_("Empty, if this product is not an admission ticket")
|
||||
)
|
||||
voucher = models.ForeignKey(
|
||||
'Voucher', null=True, blank=True
|
||||
)
|
||||
voucher_discount = models.DecimalField(
|
||||
default=Decimal('0.00'), decimal_places=2, max_digits=10
|
||||
)
|
||||
base_price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10, null=True, blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def cache_answers(self):
|
||||
"""
|
||||
Creates two properties on the object.
|
||||
@@ -281,22 +338,22 @@ class ObjectWithAnswers:
|
||||
q.answer = ""
|
||||
self.questions.append(q)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.voucher is None and self.base_price is None:
|
||||
self.base_price = self.price
|
||||
if self.voucher_discount != Decimal('0.00') and self.base_price is not None:
|
||||
self.price = self.base_price - self.voucher_discount
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class OrderPosition(ObjectWithAnswers, models.Model):
|
||||
|
||||
class OrderPosition(AbstractPosition):
|
||||
"""
|
||||
An OrderPosition is one line of an order, representing one ordered items
|
||||
of a specified type (or variation).
|
||||
of a specified type (or variation). This has all properties of
|
||||
AbstractPosition.
|
||||
|
||||
:param order: The order this is a part of
|
||||
:type order: Order
|
||||
:param item: The ordered item
|
||||
:type item: Item
|
||||
:param variation: The ordered ItemVariation or null, if the item has no properties
|
||||
:type variation: ItemVariation
|
||||
:param price: The price of this item
|
||||
:type price: decimal.Decimal
|
||||
:param attendee_name: The attendee's name, if entered.
|
||||
:type attendee_name: str
|
||||
"""
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
@@ -304,28 +361,6 @@ class OrderPosition(ObjectWithAnswers, models.Model):
|
||||
related_name='positions',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
verbose_name=_("Item"),
|
||||
related_name='positions',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
variation = models.ForeignKey(
|
||||
ItemVariation,
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Variation"),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
attendee_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Attendee name"),
|
||||
blank=True, null=True,
|
||||
help_text=_("Empty, if this product is not an admission ticket")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Order position")
|
||||
@@ -335,11 +370,9 @@ class OrderPosition(ObjectWithAnswers, models.Model):
|
||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||
ops = []
|
||||
for cartpos in cp:
|
||||
op = OrderPosition(
|
||||
order=order, item=cartpos.item, variation=cartpos.variation,
|
||||
price=cartpos.price, attendee_name=cartpos.attendee_name
|
||||
)
|
||||
op.save()
|
||||
op = OrderPosition(order=order)
|
||||
for f in AbstractPosition._meta.fields:
|
||||
setattr(op, f.name, getattr(cartpos, f.name))
|
||||
for answ in cartpos.answers.all():
|
||||
answ.orderposition = op
|
||||
answ.cartposition = None
|
||||
@@ -348,31 +381,19 @@ class OrderPosition(ObjectWithAnswers, models.Model):
|
||||
ops.append(op)
|
||||
|
||||
|
||||
class CartPosition(ObjectWithAnswers, models.Model):
|
||||
class CartPosition(AbstractPosition):
|
||||
"""
|
||||
A cart position is similar to a order line, except that it is not
|
||||
yet part of a binding order but just placed by some user in his or
|
||||
her cart. It therefore normally has a much shorter expiration time
|
||||
than an ordered position, but still blocks an item in the quota pool
|
||||
as we do not want to throw out users while they're clicking through
|
||||
the checkout process.
|
||||
the checkout process. This has all properties of AbstractPosition.
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Evnt
|
||||
:param item: The selected item
|
||||
:type item: Item
|
||||
:param cart_id: The user session that contains this cart position
|
||||
:type cart_id: str
|
||||
:param variation: The selected ItemVariation or null, if the item has no properties
|
||||
:type variation: ItemVariation
|
||||
:param datetime: The datetime this item was put into the cart
|
||||
:type datetime: datetime
|
||||
:param expires: The date until this item is guarenteed to be reserved
|
||||
:type expires: datetime
|
||||
:param price: The price of this item
|
||||
:type price: decimal.Decimal
|
||||
:param attendee_name: The attendee's name, if entered.
|
||||
:type attendee_name: str
|
||||
"""
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -382,21 +403,6 @@ class CartPosition(ObjectWithAnswers, models.Model):
|
||||
max_length=255, null=True, blank=True,
|
||||
verbose_name=_("Cart ID (e.g. session key)")
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
verbose_name=_("Item"),
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
variation = models.ForeignKey(
|
||||
ItemVariation,
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Variation"),
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
price = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Price")
|
||||
)
|
||||
datetime = models.DateTimeField(
|
||||
verbose_name=_("Date"),
|
||||
auto_now_add=True
|
||||
@@ -404,12 +410,6 @@ class CartPosition(ObjectWithAnswers, models.Model):
|
||||
expires = models.DateTimeField(
|
||||
verbose_name=_("Expiration date")
|
||||
)
|
||||
attendee_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Attendee name"),
|
||||
blank=True, null=True,
|
||||
help_text=_("Empty, if this product is not an admission ticket")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Cart position")
|
||||
|
||||
68
src/pretix/base/models/vouchers.py
Normal file
68
src/pretix/base/models/vouchers.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .event import Event
|
||||
from .items import Item
|
||||
from .orders import CartPosition, OrderPosition
|
||||
|
||||
|
||||
class Voucher(models.Model):
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="vouchers",
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
code = models.CharField(
|
||||
verbose_name=_("Voucher code"),
|
||||
max_length=255
|
||||
)
|
||||
valid_until = models.DateTimeField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Valid until")
|
||||
)
|
||||
block_quota = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Reserve ticket from quota"),
|
||||
help_text=_(
|
||||
"If activated, this voucher will be substracted from the affected product\'s quotas, such that it is "
|
||||
"guaranteed that anyone with this voucher code does receive a ticket."
|
||||
)
|
||||
)
|
||||
allow_ignore_quota = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Allow to bypass quota"),
|
||||
help_text=_(
|
||||
"If activated, a holder of this voucher code can buy tickets, even if there are none left."
|
||||
)
|
||||
)
|
||||
price = models.DecimalField(
|
||||
verbose_name=_("Set product price to"),
|
||||
decimal_places=2, max_digits=10, null=True, blank=True
|
||||
)
|
||||
item = models.ManyToManyField(
|
||||
Item, related_name='vouchers',
|
||||
verbose_name=_("Product"),
|
||||
help_text=_(
|
||||
"This product is added to the user's cart if the voucher is redeemed."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Voucher")
|
||||
verbose_name_plural = _("Vouchers")
|
||||
unique_together = (("event", "code"),)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.code = self.code.upper()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def is_ordered(self) -> int:
|
||||
return OrderPosition.objects.current.filter(
|
||||
voucher=self.voucher
|
||||
).exists()
|
||||
|
||||
def is_in_cart(self) -> int:
|
||||
return CartPosition.objects.current.filter(
|
||||
voucher=self.voucher
|
||||
).count()
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9 on 2015-12-13 11:44
|
||||
# Generated by Django 1.9 on 2016-02-09 09:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
@@ -11,7 +11,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0002_auto_20151213_1144'),
|
||||
('pretixbase', '0002_auto_20160209_0940'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
Reference in New Issue
Block a user