From ed4789dc4fb34160d634482907042cafa48ba617 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 17 Oct 2014 23:55:54 +0200 Subject: [PATCH] Add quota and order models --- .../migrations/0017_auto_20141017_2148.py | 125 +++++++++++ src/tixlbase/models.py | 207 +++++++++++++++++- .../migrations/0004_auto_20141017_2148.py | 25 +++ 3 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 src/tixlbase/migrations/0017_auto_20141017_2148.py create mode 100644 src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py diff --git a/src/tixlbase/migrations/0017_auto_20141017_2148.py b/src/tixlbase/migrations/0017_auto_20141017_2148.py new file mode 100644 index 000000000..0e14a1bdb --- /dev/null +++ b/src/tixlbase/migrations/0017_auto_20141017_2148.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import tixlbase.models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('tixlbase', '0016_event_plugins'), + ] + + operations = [ + migrations.CreateModel( + name='CartPosition', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('session', models.CharField(null=True, max_length=255, blank=True, verbose_name='Session key')), + ('total', models.DecimalField(max_digits=10, verbose_name='Price', decimal_places=2)), + ('datetime', models.DateTimeField(verbose_name='Datetime')), + ('expires', models.DateTimeField(verbose_name='Expiration date')), + ('event', models.ForeignKey(to='tixlbase.Event', verbose_name='Event')), + ('item', models.ForeignKey(to='tixlbase.Item', verbose_name='Item')), + ('user', models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ('variation', models.ForeignKey(null=True, blank=True, to='tixlbase.ItemVariation', verbose_name='Variation')), + ], + options={ + 'verbose_name_plural': 'Cart positions', + 'verbose_name': 'Cart position', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('status', models.CharField(max_length=3, choices=[('p', 'pending'), ('n', 'paid'), ('e', 'expired'), ('c', 'cancelled')], verbose_name='Status')), + ('datetime', models.DateTimeField(auto_now_add=True, verbose_name='Date')), + ('expires', models.DateTimeField(verbose_name='Expiration date')), + ('payment_date', models.DateTimeField(verbose_name='Payment date')), + ('payment_info', models.TextField(verbose_name='Payment information')), + ('total', models.DecimalField(max_digits=10, verbose_name='Total amount', decimal_places=2)), + ('event', models.ForeignKey(to='tixlbase.Event', verbose_name='Event')), + ('user', models.ForeignKey(null=True, blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name_plural': 'Orders', + 'verbose_name': 'Order', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='OrderPosition', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('price', models.DecimalField(max_digits=10, verbose_name='Price', decimal_places=2)), + ], + options={ + 'verbose_name_plural': 'Order positions', + 'verbose_name': 'Order position', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='QuestionAnswer', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('answer', models.TextField()), + ('cartposition', models.ForeignKey(null=True, blank=True, to='tixlbase.CartPosition')), + ('orderposition', models.ForeignKey(null=True, blank=True, to='tixlbase.OrderPosition')), + ('question', models.ForeignKey(to='tixlbase.Question')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Quota', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('name', models.CharField(max_length=200, verbose_name='Name')), + ('size', models.PositiveIntegerField(verbose_name='Total capacity')), + ('items', models.ManyToManyField(to='tixlbase.Item', blank=True, verbose_name='Item')), + ('lock_cache', models.ManyToManyField(to='tixlbase.CartPosition', blank=True)), + ('order_cache', models.ManyToManyField(to='tixlbase.OrderPosition', blank=True)), + ('variations', tixlbase.models.VariationsField(to='tixlbase.ItemVariation', blank=True, verbose_name='Variations')), + ], + options={ + 'verbose_name_plural': 'Quotas', + 'verbose_name': 'Quota', + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='orderposition', + name='answers', + field=models.ManyToManyField(to='tixlbase.Question', through='tixlbase.QuestionAnswer', verbose_name='Answers'), + preserve_default=True, + ), + migrations.AddField( + model_name='orderposition', + name='item', + field=models.ForeignKey(to='tixlbase.Item', verbose_name='Item'), + preserve_default=True, + ), + migrations.AddField( + model_name='orderposition', + name='order', + field=models.ForeignKey(to='tixlbase.Order', verbose_name='Order'), + preserve_default=True, + ), + migrations.AddField( + model_name='orderposition', + name='variation', + field=models.ForeignKey(null=True, blank=True, to='tixlbase.ItemVariation', verbose_name='Variation'), + preserve_default=True, + ), + migrations.AlterField( + model_name='event', + name='payment_term_days', + field=models.PositiveIntegerField(verbose_name='Payment term in days', default=14, help_text='The number of days after placing an order the user has to pay to preserve his reservation.'), + ), + ] diff --git a/src/tixlbase/models.py b/src/tixlbase/models.py index 528831fec..a0ab0386d 100644 --- a/src/tixlbase/models.py +++ b/src/tixlbase/models.py @@ -266,7 +266,7 @@ class Event(models.Model): verbose_name=_("Start of presale"), help_text=_("No items will be sold before this date."), ) - payment_term_days = models.IntegerField( + payment_term_days = models.PositiveIntegerField( default=14, verbose_name=_("Payment term in days"), help_text=_("The number of days after placing an order the user has to pay to preserve his reservation."), @@ -709,13 +709,14 @@ class BaseRestriction(models.Model): ) item = models.ForeignKey( Item, - blank=True, - null=True, + blank=True, null=True, + verbose_name=_("Item"), related_name="restrictions_%(app_label)s_%(class)s", ) variations = VariationsField( ItemVariation, blank=True, + verbose_name=_("Variations"), related_name="restrictions_%(app_label)s_%(class)s", ) @@ -728,3 +729,203 @@ class BaseRestriction(models.Model): if self.event: self.event.get_cache().clear() return super().save(*args, **kwargs) + + +class Quota(models.Model): + """ + A quota is a "pool of tickets". It is there to limit the number of items + of a certain type to be sold. For example, you could have a quota of 500 + applied to all your items (because you only have that much space in your + building), and also a quota of 100 applied to the VIP tickets for + exclusivity. In this case, no more than 500 tickets will be sold in total + and no more than 100 of them will be VIP tickets (but 450 normal and 50 + VIP tickets will be fine). + + As always, a quota can not only be tied to an item, but also to a specific + variation. We follow the general rule here: If there are no variations + speficied, the quota applies to all of them, and if there are variations + specified, the quota applies to those. + + This object holds two fields, "order_cache" and "lock_cache", which are + implementation specific and are considered private. It is planned that they + are being used as a fallback solution if redis is not available. + """ + name = models.CharField( + max_length=200, + verbose_name=_("Name") + ) + size = models.PositiveIntegerField( + verbose_name=_("Total capacity") + ) + items = models.ManyToManyField( + Item, + verbose_name=_("Item"), + blank=True + ) + variations = VariationsField( + ItemVariation, + blank=True, + verbose_name=_("Variations") + ) + order_cache = models.ManyToManyField( + 'OrderPosition', + blank=True + ) + lock_cache = models.ManyToManyField( + 'CartPosition', + blank=True + ) + + class Meta: + verbose_name = _("Quota") + verbose_name_plural = _("Quotas") + + +class Order(models.Model): + """ + An order is created when a user clicks 'buy' on his cart. It holds + several OrderPositions and is connected to an user. It has an + expiration date: If items run out of capacity, orders which are over + their expiration date might be cancelled. + + Important: An order holds its total monetary value, as an order is a + piece of 'history' and must not change due to a change in item prices. + """ + + STATUS_PENDING = "n" + STATUS_PAID = "p" + STATUS_EXPIRED = "e" + STATUS_CANCELLED = "c" + STATUS_CHOICE = ( + (STATUS_PAID, _("pending")), + (STATUS_PENDING, _("paid")), + (STATUS_EXPIRED, _("expired")), + (STATUS_CANCELLED, _("cancelled")), + ) + + status = models.CharField( + max_length=3, + choices=STATUS_CHOICE, + verbose_name=_("Status") + ) + event = models.ForeignKey( + Event, + verbose_name=_("Event") + ) + user = models.ForeignKey( + User, null=True, blank=True, + verbose_name=_("User") + ) + datetime = models.DateTimeField( + auto_now_add=True, + verbose_name=_("Date") + ) + expires = models.DateTimeField( + verbose_name=_("Expiration date") + ) + payment_date = models.DateTimeField( + verbose_name=_("Payment date") + ) + payment_info = models.TextField( + verbose_name=_("Payment information") + ) + total = models.DecimalField( + decimal_places=2, max_digits=10, + verbose_name=_("Total amount") + ) + + class Meta: + verbose_name = _("Order") + verbose_name_plural = _("Orders") + + +class QuestionAnswer(models.Model): + """ + The answer to a Question, connected to an OrderPosition or CartPosition + """ + orderposition = models.ForeignKey('OrderPosition', null=True, blank=True) + cartposition = models.ForeignKey('CartPosition', null=True, blank=True) + question = models.ForeignKey(Question) + answer = models.TextField() + + +class OrderPosition(models.Model): + """ + An OrderPosition is one line of an order, representing one ordered items + of a specified type (or variation). + + Important: An OrderPosition holds its total monetary value, as an order is a + piece of 'history' and must not change due to a change in item prices. + """ + order = models.ForeignKey( + Order, + verbose_name=_("Order") + ) + item = models.ForeignKey( + Item, + verbose_name=_("Item") + ) + variation = models.ForeignKey( + ItemVariation, + null=True, blank=True, + verbose_name=_("Variation") + ) + price = models.DecimalField( + decimal_places=2, max_digits=10, + verbose_name=_("Price") + ) + answers = models.ManyToManyField( + Question, + through=QuestionAnswer, + verbose_name=_("Answers") + ) + + class Meta: + verbose_name = _("Order position") + verbose_name_plural = _("Order positions") + + +class CartPosition(models.Model): + """ + 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. + """ + event = models.ForeignKey( + Event, + verbose_name=_("Event") + ) + user = models.ForeignKey( + User, null=True, blank=True, + verbose_name=_("User") + ) + session = models.CharField( + max_length=255, null=True, blank=True, + verbose_name=_("Session key") + ) + item = models.ForeignKey( + Item, + verbose_name=_("Item") + ) + variation = models.ForeignKey( + ItemVariation, + null=True, blank=True, + verbose_name=_("Variation") + ) + total = models.DecimalField( + decimal_places=2, max_digits=10, + verbose_name=_("Price") + ) + datetime = models.DateTimeField( + verbose_name=_("Datetime") + ) + expires = models.DateTimeField( + verbose_name=_("Expiration date") + ) + + class Meta: + verbose_name = _("Cart position") + verbose_name_plural = _("Cart positions") diff --git a/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py b/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py new file mode 100644 index 000000000..fb386bee8 --- /dev/null +++ b/src/tixlplugins/timerestriction/migrations/0004_auto_20141017_2148.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import tixlbase.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('timerestriction', '0003_auto_20141013_1811'), + ] + + operations = [ + migrations.AlterField( + model_name='timerestriction', + name='item', + field=models.ForeignKey(null=True, blank=True, related_name='restrictions_timerestriction_timerestriction', to='tixlbase.Item', verbose_name='Item'), + ), + migrations.AlterField( + model_name='timerestriction', + name='variations', + field=tixlbase.models.VariationsField(related_name='restrictions_timerestriction_timerestriction', to='tixlbase.ItemVariation', blank=True, verbose_name='Variations'), + ), + ]