First steps on item availability

This commit is contained in:
Raphael Michel
2015-02-12 00:08:29 +01:00
parent f6bafd1f5e
commit 01b34f42cf
8 changed files with 257 additions and 55 deletions

View 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,
),
]

View File

@@ -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

View File

@@ -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)
))

View 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;
}

View File

@@ -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";

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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([