forked from CGM_Public/pretix_original
First steps on item availability
This commit is contained in:
78
src/pretixbase/migrations/0004_auto_20150211_2330.py
Normal file
78
src/pretixbase/migrations/0004_auto_20150211_2330.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.utils.timezone import utc
|
||||
import versions.models
|
||||
import datetime
|
||||
import pretixbase.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0003_auto_20150211_2042'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='identity',
|
||||
field=models.CharField(default='LEGACY', max_length=36),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='version_birth_date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 3, 234665, tzinfo=utc)),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='version_end_date',
|
||||
field=models.DateTimeField(blank=True, null=True, default=None),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='version_start_date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 3, 234665, tzinfo=utc)),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='identity',
|
||||
field=models.CharField(default='LEGACY', max_length=36),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='version_birth_date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 15, 115790, tzinfo=utc)),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='version_end_date',
|
||||
field=models.DateTimeField(blank=True, null=True, default=None),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='version_start_date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2015, 2, 11, 23, 30, 21, 726769, tzinfo=utc)),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='id',
|
||||
field=models.CharField(primary_key=True, serialize=False, max_length=36),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='id',
|
||||
field=models.CharField(primary_key=True, serialize=False, max_length=36),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
@@ -446,6 +446,13 @@ class ItemCategory(Versionable):
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.position < other.position:
|
||||
return True
|
||||
if self.position == other.position:
|
||||
return self.pk < other.pk
|
||||
return False
|
||||
|
||||
|
||||
class Property(Versionable):
|
||||
"""
|
||||
@@ -517,6 +524,13 @@ class PropertyValue(Versionable):
|
||||
if self.prop:
|
||||
self.prop.event.get_cache().clear()
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.position < other.position:
|
||||
return True
|
||||
if self.position == other.position:
|
||||
return self.pk < other.pk
|
||||
return False
|
||||
|
||||
|
||||
class Question(Versionable):
|
||||
"""
|
||||
@@ -708,14 +722,32 @@ class Item(Versionable):
|
||||
def get_all_available_variations(self):
|
||||
"""
|
||||
This method returns a list of all variations which are theoretically
|
||||
possible for sale. It DOES call all activated restriction plugins, but it
|
||||
DOES NOT take into account quotas. Use is_available on the ItemVariation
|
||||
objects (or the Item it self, if it does not have variations) to determine
|
||||
availability by the terms of quotas.
|
||||
possible for sale. It DOES call all activated restriction plugins, and it
|
||||
DOES only return variations which DO have an ItemVariation object, as all
|
||||
variations without one CAN NOT be part of a Quota and therefore CAN NOT
|
||||
ever be available for sale. The only exception is the empty variation
|
||||
for items without properties, which never has an ItemVariation object.
|
||||
|
||||
This DOES NOT take into account quotas itself. Use is_available on the
|
||||
ItemVariation objects (or the Item it self, if it does not have variations) to
|
||||
determine availability by the terms of quotas.
|
||||
|
||||
It is recommended to call
|
||||
prefetch_related('properties', 'variations__values__prop')
|
||||
when retrieving Item objects you are going to use this method on.
|
||||
"""
|
||||
from .signals import determine_availability
|
||||
|
||||
variations = self.get_all_variations()
|
||||
if self.properties.count() == 0:
|
||||
variations = [VariationDict()]
|
||||
else:
|
||||
all_variations = list(self.variations.all())
|
||||
variations = []
|
||||
for var in all_variations:
|
||||
vardict = VariationDict()
|
||||
for v in var.values.all():
|
||||
vardict[v.prop.identity] = v
|
||||
vardict['variation'] = var
|
||||
variations.append(vardict)
|
||||
responses = determine_availability.send(
|
||||
self.event, item=self,
|
||||
variations=variations, context=None,
|
||||
@@ -746,7 +778,7 @@ class Item(Versionable):
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale. It may return any of the return codes of Quota.availability()
|
||||
"""
|
||||
if self.properties.exist():
|
||||
if self.properties.count() > 0:
|
||||
raise ValueError('Do not call this directly on items which have properties '
|
||||
'but call this on their ItemVariation objects')
|
||||
return max([q.availability() for q in self.quotas.all()])
|
||||
@@ -987,25 +1019,25 @@ class Quota(Versionable):
|
||||
Q(variation__quotas__in=[self])
|
||||
)
|
||||
)
|
||||
paid_orders = OrderPosition.objects.current.filter(
|
||||
paid_orders = OrderPosition.objects.filter(
|
||||
Q(order__status=Order.STATUS_PAID)
|
||||
& quotalookup
|
||||
)
|
||||
).count()
|
||||
if paid_orders >= self.size:
|
||||
return Quota.AVAILABILITY_GONE
|
||||
|
||||
pending_valid_orders = OrderPosition.objects.current.filter(
|
||||
pending_valid_orders = OrderPosition.objects.filter(
|
||||
Q(order__status=Order.STATUS_PENDING)
|
||||
& Q(order__expires__gte=now())
|
||||
& quotalookup
|
||||
)
|
||||
).count()
|
||||
if (paid_orders + pending_valid_orders) >= self.size:
|
||||
return Quota.AVAILABILITY_ORDERED
|
||||
|
||||
valid_cart_positions = CartPosition.objects.current.filter(
|
||||
Q(order__expires__lt=now())
|
||||
valid_cart_positions = CartPosition.objects.filter(
|
||||
Q(expires__gte=now())
|
||||
& quotalookup
|
||||
)
|
||||
).count()
|
||||
if (paid_orders + pending_valid_orders + valid_cart_positions) >= self.size:
|
||||
return Quota.AVAILABILITY_RESERVED
|
||||
|
||||
@@ -1080,7 +1112,7 @@ class QuestionAnswer(Versionable):
|
||||
answer = models.TextField()
|
||||
|
||||
|
||||
class OrderPosition(models.Model):
|
||||
class OrderPosition(Versionable):
|
||||
"""
|
||||
An OrderPosition is one line of an order, representing one ordered items
|
||||
of a specified type (or variation).
|
||||
@@ -1116,7 +1148,7 @@ class OrderPosition(models.Model):
|
||||
verbose_name_plural = _("Order positions")
|
||||
|
||||
|
||||
class CartPosition(models.Model):
|
||||
class CartPosition(Versionable):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -35,7 +35,7 @@ class VariationDict(dict):
|
||||
unique among one item.
|
||||
"""
|
||||
def order_key(i):
|
||||
i[0]
|
||||
return i[0]
|
||||
return ",".join((
|
||||
str(v[1].pk) for v in sorted(self.relevant_items(), key=order_key)
|
||||
))
|
||||
|
||||
39
src/pretixpresale/static/pretixpresale/less/event.less
Normal file
39
src/pretixpresale/static/pretixpresale/less/event.less
Normal file
@@ -0,0 +1,39 @@
|
||||
.product-row {
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid @table-border-color;
|
||||
|
||||
&.headline, &.simple {
|
||||
border-top: 2px solid @table-border-color;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: 2px solid @table-border-color;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-item-count {
|
||||
text-align: center;
|
||||
}
|
||||
.availability-box {
|
||||
text-align: center;
|
||||
|
||||
&.gone {
|
||||
color: @alert-danger-text;
|
||||
}
|
||||
&.unavailable {
|
||||
color: @alert-warning-text;
|
||||
}
|
||||
}
|
||||
.price {
|
||||
text-align: center;
|
||||
}
|
||||
.price small,
|
||||
.availability-box small {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
.checkout-button-row {
|
||||
padding: 15px 0;
|
||||
}
|
||||
@@ -2,9 +2,4 @@
|
||||
@import "../../../../pretixbase/static/fontawesome/less/font-awesome.less";
|
||||
@fa-font-path: "../../fontawesome/fonts";
|
||||
|
||||
.input-item-count {
|
||||
text-align: center;
|
||||
}
|
||||
.availabilitybox.available {
|
||||
text-align: center;
|
||||
}
|
||||
@import "event.less";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{% load i18n %}
|
||||
{% if avail == 30 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box gone">
|
||||
<strong>{% trans "SOLD OUT" %}</strong>
|
||||
</div>
|
||||
{% elif avail < 30 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box unavailable">
|
||||
<strong>{% trans "Unavailable" %}</strong><br />
|
||||
<small>
|
||||
{% trans "This item is currently unavailable but might become available again." %}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,40 +1,75 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% for tup in items_by_category %}
|
||||
<h3>{{ tup.0.name }}</h3>
|
||||
{% for item in tup.1 %}
|
||||
{% if item.has_variations %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-8">
|
||||
<strong>{{ item.name }}</strong>
|
||||
<p>{{ item.short_description }}</p>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% for var in item.available_variations %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-8">
|
||||
{{ var }}
|
||||
</div>
|
||||
<div class="col-md-1 availability-box abailable">
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0">
|
||||
<section>
|
||||
<h3>{{ tup.0.name }}</h3>
|
||||
{% for item in tup.1 %}
|
||||
{% if item.has_variations %}
|
||||
<div class="row-fluid product-row headline">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-8">
|
||||
<strong>{{ item.name }}</strong>
|
||||
<p>{{ item.short_description }}</p>
|
||||
{% for var in item.available_variations %}
|
||||
<div class="row-fluid product-row variation">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
{{ var }}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{{ event.currency }} {{ var.price|floatformat:2 }}
|
||||
{% if item.tax_rate %}
|
||||
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||
incl. {{ rate }}% taxes
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if var.cached_availability == 0 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0">
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with avail=var.cached_availability %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="row-fluid product-row simple">
|
||||
<div class="col-md-8 col-xs-12">
|
||||
<strong>{{ item.name }}</strong>
|
||||
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6 price">
|
||||
{{ event.currency }} {{ item.price|floatformat:2 }}
|
||||
{% if item.tax_rate %}
|
||||
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
|
||||
incl. {{ rate }}% taxes
|
||||
{% endblocktrans %}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if item.cached_availability == 0 %}
|
||||
<div class="col-md-2 col-xs-6 availability-box available">
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0">
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "pretixpresale/event/fragment_availability.html" with avail=item.cached_availability %}
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="col-md-1 availability-box abailable">
|
||||
<input type="number" class="form-control input-item-count" placeholder="0" min="0">
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
<div class="row-fluid checkout-button-row">
|
||||
<div class="col-md-4 col-md-offset-8">
|
||||
<button class="btn btn-block btn-primary btn-lg">
|
||||
<i class="fa fa-shopping-cart"></i> {% trans "Proceed with checkout" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -16,15 +16,25 @@ class EventIndex(EventViewMixin, TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Fetch all items
|
||||
items = self.request.event.items.all().select_related(
|
||||
'category',
|
||||
'category', # for re-grouping
|
||||
).prefetch_related(
|
||||
'properties', 'variations__values__prop', # for .get_all_available_variations()
|
||||
'quotas', 'variations__quotas' # for .availability()
|
||||
).annotate(quotac=Count('quotas')).filter(
|
||||
quotac__gt=0
|
||||
).order_by('category__position', 'category_id', 'name')
|
||||
|
||||
for item in items:
|
||||
item.available_variations = item.get_all_available_variations()
|
||||
item.available_variations = sorted(item.get_all_available_variations(),
|
||||
key=lambda vd: vd.ordered_values())
|
||||
item.has_variations = (len(item.available_variations) != 1
|
||||
or not item.available_variations[0].empty())
|
||||
if not item.has_variations:
|
||||
item.cached_availability = item.availability()
|
||||
item.price = item.available_variations[0]['price']
|
||||
else:
|
||||
for var in item.available_variations:
|
||||
var.cached_availability = var['variation'].availability()
|
||||
|
||||
# Regroup those by category
|
||||
context['items_by_category'] = sorted([
|
||||
|
||||
Reference in New Issue
Block a user