Skip to content

Commit aa0e686

Browse files
authored
Merge pull request #337 from bruecksen/175-genussrechtler
175 Genussrechtler, option to sell product to a specific user group
2 parents 678f4ad + 0ac0bd7 commit aa0e686

31 files changed

+750
-227
lines changed

bakeup/pages/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def get_context(self, request, *args, **kwargs):
8686
context['production_day_next'] = self.production_day
8787
current_customer_order = CustomerOrder.objects.filter(customer=customer, production_day=self.production_day).first()
8888
context['current_customer_order'] = current_customer_order
89-
production_day_products = self.production_day.production_day_products.published()
89+
production_day_products = self.production_day.production_day_products.published().available_to_user(request.user)
9090
# TODO this needs to go at one place, code duplication, very bad idea shop/views
9191
production_day_products = production_day_products.annotate(
9292
ordered_quantity=Subquery(

bakeup/shop/forms.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def clean(self):
164164

165165

166166
ProductionDayProductFormSet = modelformset_factory(
167-
ProductionDayProduct, fields=("product", "max_quantity", "is_published"), extra=1, can_delete=True, formset=BaseProductionDayProductFormSet
167+
ProductionDayProduct, fields=("product", "max_quantity", "is_published", "group"), extra=1, can_delete=True, formset=BaseProductionDayProductFormSet
168168
)
169169

170170
CustomerOrderPositionFormSet = modelformset_factory(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 3.2.12 on 2023-09-14 09:33
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('auth', '0012_alter_user_first_name_max_length'),
11+
('shop', '0030_alter_productionday_day_of_sale'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='productiondayproduct',
17+
name='group',
18+
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group', verbose_name='Group'),
19+
),
20+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 3.2.12 on 2023-09-14 12:15
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('auth', '0012_alter_user_first_name_max_length'),
11+
('shop', '0031_productiondayproduct_group'),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name='productiondayproduct',
17+
name='group',
18+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group', verbose_name='Group'),
19+
),
20+
]

bakeup/shop/models.py

+30-2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ def upcoming(self):
5959
today = timezone.now().date()
6060
return self.filter(day_of_sale__gte=today).order_by('day_of_sale')
6161

62+
def available_to_user(self, user):
63+
return self.filter(
64+
Q(production_day_products__group__isnull=True) |
65+
Q(production_day_products__group__in=[group.pk for group in user.groups.all()])
66+
)
67+
68+
def available(self):
69+
return self.filter(production_day_products__group__isnull=True)
70+
6271

6372
class ProductionDay(CommonBaseClass):
6473
day_of_sale = models.DateField(unique=True, verbose_name=_("Day of Sale"), db_index=True)
@@ -223,6 +232,15 @@ def update_order_positions_product(self, production_plan_product):
223232
positions.update(product=production_plan_product)
224233

225234
class ProductionDayProductQuerySet(models.QuerySet):
235+
def available_to_user(self, user):
236+
return self.filter(
237+
Q(group__isnull=True) |
238+
Q(group__in=[group.pk for group in user.groups.all()])
239+
)
240+
241+
def available(self):
242+
return self.filter(group__isnull=True)
243+
226244
def published(self):
227245
return self.filter(is_published=True)
228246

@@ -245,6 +263,7 @@ class ProductionDayProduct(CommonBaseClass):
245263
max_quantity = models.PositiveSmallIntegerField(blank=False, null=False, verbose_name=_("Max quantity"))
246264
production_plan = models.ForeignKey('workshop.ProductionPlan', on_delete=models.SET_NULL, blank=True, null=True)
247265
is_published = models.BooleanField(default=False, verbose_name=_("Published?"))
266+
group = models.ForeignKey('auth.Group', blank=True, null=True, verbose_name=_('Group'), on_delete=models.SET_NULL)
248267

249268
objects = ProductionDayProductQuerySet.as_manager()
250269

@@ -318,6 +337,13 @@ def calculate_max_quantity(self, exclude_customer=None):
318337
ordered_quantity = orders.aggregate(quantity_sum=Sum('quantity'))['quantity_sum'] or 0
319338
return max(self.max_quantity - ordered_quantity, 0)
320339

340+
def is_available_to_customer(self, customer):
341+
if not self.group:
342+
return True
343+
if self.group and self.group in customer.user.groups.all():
344+
return True
345+
return False
346+
321347

322348
class PointOfSale(CommonBaseClass):
323349
name = models.CharField(max_length=255)
@@ -427,7 +453,6 @@ def has_abo(self):
427453

428454
@classmethod
429455
def create_or_update_customer_order(cls, request, production_day, customer, products, point_of_sale=None):
430-
# TODO order_nr, address, should point of sale really be saved in order?
431456
# TODO check quantity with availalbe quantity
432457
with transaction.atomic():
433458
point_of_sale = point_of_sale and PointOfSale.objects.get(pk=point_of_sale) or customer.point_of_sale
@@ -441,6 +466,9 @@ def create_or_update_customer_order(cls, request, production_day, customer, prod
441466
for product, quantity in products.items():
442467
# print(product, quantity)
443468
production_day_product = ProductionDayProduct.objects.get(production_day=production_day, product=product)
469+
if not production_day_product.is_available_to_customer(customer):
470+
messages.add_message(request, messages.INFO, "Leider ist das Produkt {} nicht verfügbar.".format(product.get_display_name()))
471+
continue
444472
max_quantity = production_day_product.calculate_max_quantity(customer)
445473
if quantity > 0 and production_day_product.is_sold_out and max_quantity <= quantity:
446474
messages.add_message(request, messages.INFO, "Leider ist das Produkt {} schon ausverkauft und konnte nicht mehr bestellt werden.".format(product.get_display_name()))
@@ -478,7 +506,7 @@ def create_or_update_customer_order(cls, request, production_day, customer, prod
478506
return customer_order, created
479507

480508
def get_production_day_products_ordered_list(self):
481-
production_day_products = self.production_day.production_day_products.published()
509+
production_day_products = self.production_day.production_day_products.published().available_to_user(self.customer.user)
482510
production_day_products = production_day_products.annotate(
483511
ordered_quantity=Subquery(self.positions.filter(Q(product=OuterRef('product__pk')) | Q(product__product_template=OuterRef('product__pk')),).values("quantity"))
484512
).annotate(

bakeup/shop/templatetags/shop_tags.py

+16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ def customer_quantity(context, production_day_product):
1616
).first()
1717
return position and position.quantity or None
1818

19+
@register.simple_tag(takes_context=True)
20+
def upcoming_available_production_days(context, product):
21+
user = context['request'].user
22+
if user.is_authenticated:
23+
return product.production_days.published().upcoming().available_to_user(user)
24+
else:
25+
return product.production_days.published().upcoming().available()
26+
27+
@register.simple_tag(takes_context=True)
28+
def available_products(context, production_day):
29+
user = context['request'].user
30+
if user.is_authenticated:
31+
return production_day.production_day_products.published().upcoming().available_to_user(user)
32+
else:
33+
return production_day.production_day_products.published().upcoming().available()
34+
1935
@register.simple_tag(takes_context=True)
2036
def max_quantity(context, production_day, product):
2137
production_day_product = ProductionDayProduct.objects.get(production_day=production_day, product=product)

bakeup/shop/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ def get_context_data(self, **kwargs):
233233
if self.production_day:
234234
context['abo_product_days'] = ProductionDayProduct.get_available_abo_product_days(self.production_day, customer)
235235
context['production_day_next'] = self.production_day
236-
context['production_day_products'] = self.production_day.production_day_products.published()
237-
production_day_products = self.production_day.production_day_products.published()
236+
production_day_products = self.production_day.production_day_products.published().available_to_user(self.request.user)
237+
context['production_day_products'] = production_day_products
238238
current_customer_order = CustomerOrder.objects.filter(customer=customer, production_day=self.production_day).first()
239239
context['current_customer_order'] = current_customer_order
240240
# TODO this needs to go at one place, code duplication, very bad idea pages/models

bakeup/static/css/project.css

+9
Original file line numberDiff line numberDiff line change
@@ -13181,6 +13181,15 @@ textarea {
1318113181
z-index: 10;
1318213182
}
1318313183

13184+
.badge.top {
13185+
position: absolute;
13186+
left: 50%;
13187+
top: 0;
13188+
z-index: 10;
13189+
-webkit-transform: translateX(-50%) translateY(-50%);
13190+
transform: translateX(-50%) translateY(-50%);
13191+
}
13192+
1318413193
.badge.top-middle {
1318513194
position: absolute;
1318613195
left: 50%;

bakeup/static/css/project.min.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bakeup/static/sass/project.scss

+7
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,13 @@ textarea {
575575
top: -10px;
576576
z-index: 10;
577577
}
578+
.badge.top {
579+
position: absolute;
580+
left: 50%;
581+
top: 0;
582+
z-index: 10;
583+
transform: translateX(-50%) translateY(-50%);
584+
}
578585
.badge.top-middle {
579586
position: absolute;
580587
left: 50%;
+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{% extends "shop/base_page.html" %}
2+
{% load wagtailcore_tags %}
3+
24
{% block page_content %}
3-
{{ page.content }}
5+
{% for block in page.content %}
6+
{% include_block block %}
7+
{% endfor %}
48
{% endblock %}

bakeup/templates/shop/includes/product_card.html

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
{% customer_quantity production_day_product as customer_quantity %}
55
{% endif %}
66
<div class="card h-100 product-card" id="product-{{ production_day_product.pk }}" data-product="{{ production_day_product.product.pk }}" data-ordered-quantity="{{ customer_quantity|default:0 }}">
7+
{% if production_day_product.group %}
8+
<span class="badge bg-primary top text-uppercase fs-5">{{ production_day_product.group }}</span>
9+
{% endif %}
710
{% if production_day_product.production_plan.is_production %}
8-
<span class="badge bg-dark top-middle text-uppercase fs-5">In Produktion</span>
11+
<span class="badge bg-dark top-middle text-uppercase fs-6">In Produktion</span>
912
{% elif production_day_product.production_plan.is_produced %}
10-
<span class="badge bg-dark top-middle text-uppercase fs-5">Produziert</span>
13+
<span class="badge bg-dark top-middle text-uppercase fs-6">Produziert</span>
1114
{% elif production_day_product.is_sold_out %}
12-
<span class="badge bg-dark top-middle text-uppercase fs-5 opacity-100 shadow">Ausverkauft</span>
15+
<span class="badge bg-dark top-middle text-uppercase fs-6 opacity-100 shadow">Ausverkauft</span>
1316
{% endif %}
1417
{% if production_day_product.is_locked or production_day_product.is_sold_out%}<div class="opacity-60">{% endif %}
1518

bakeup/templates/shop/includes/product_card_overview.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ <h2 class="card-title hyphens-auto fw-bold">{{ product.get_display_name }}
3535
{% if product.production_days.upcoming %}
3636
<h4>Nächste Backtage:</h4>
3737
<div class="d-grid gap-2">
38-
{% for production_day in product.production_days.upcoming %}
38+
{% upcoming_available_production_days product as production_days %}
39+
{% for production_day in production_days %}
3940
<a href="{% url 'shop:shop-production-day' production_day=production_day.production_day.pk %}#product-{{production_day.pk}}" class="btn btn-primary"><span class="fw-bold">{{ production_day.production_day.day_of_sale|date:'d.m.Y' }}</span> ({{ production_day.calculate_max_quantity }} verfügbar)</a>
4041
{% endfor %}
4142
</div>

bakeup/templates/shop/includes/production_day_card.html

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@
33
<div class="card h-100 production-day-card">
44
<div class="position-relative">
55
<div class="row g-0">
6-
{% if production_day.production_day_products.published.with_pictures.count == 1 %}
7-
{% thumbnail production_day.production_day_products.published.first.product.image "600x450" crop="center" as im %}
6+
{% available_products production_day as products %}
7+
{% if products.with_pictures.count == 1 %}
8+
{% thumbnail products.first.product.image "600x450" crop="center" as im %}
89
<img src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" class="card-img-top img-fluid" alt="product image">
910
{% endthumbnail %}
1011
{% else %}
1112
<div id="carousel-production-day-{{ production_day.pk }}" class="carousel slide" data-bs-ride="true">
1213
<div class="carousel-indicators">
13-
{% for production_day_product in production_day.production_day_products.published.with_pictures.all %}
14+
{% for production_day_product in products.with_pictures.all %}
1415
<button type="button" data-bs-target="#carousel-production-day-{{ production_day.pk }}" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active"{% endif %} aria-current="true" aria-label="Slide {{ forloop.counter }}"></button>
1516
{% endfor %}
1617
</div>
1718
<div class="carousel-inner">
18-
{% for production_day_product in production_day.production_day_products.published.with_pictures.all %}
19+
{% for production_day_product in products.with_pictures.all %}
1920
<div class="carousel-item{% if forloop.first %} active{% endif %}">
2021
{% thumbnail production_day_product.product.image "600x450" crop="center" as im %}
2122
<img src="{{ im.url }}" width="{{ im.width }}" height="{{ im.height }}" class="card-img-top img-fluid" alt="product image">
@@ -39,7 +40,7 @@
3940
<div class="card-body pb-0">
4041
<h2 class="card-title fw-bold">{{ production_day.day_of_sale|date:'d.m.Y' }}</h2>
4142
<p>{{ production_day.description|safe|linebreaksbr|urlize }}</p>
42-
{% for product in production_day.production_day_products.published.all %}<span class="badge bg-dark me-2 mb-2 fs-6">{{ product.product.get_display_name }}</span>{% endfor %}
43+
{% for product in products %}<span class="badge bg-dark me-2 mb-2 fs-6">{{ product.product.get_display_name }}</span>{% endfor %}
4344
</div>
4445
<div class="card-footer">
4546
<div class="d-grid">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class="btn-group btn-group-sm" role="group" aria-label="Column action links">
2+
<a href="{% url 'workshop:group-update' pk=record.pk %}" class="btn btn-primary"><i class="fas fa-edit"></i></a>
3+
<a href="{% url 'workshop:group-delete' pk=record.pk %}" class="btn btn-primary"><i class="fas fa-trash"></i></a>
4+
</div>

bakeup/templates/workshop/customer_detail.html

+21-13
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,36 @@
1616

1717
{% block page_content %}
1818
<div class="row">
19-
<div class="col col-md-6">
19+
<div class="col col-md-7">
2020
<div class="card p-3">
21-
<h3>Account:</h3>
21+
<h3>{% trans "Account" %}:</h3>
2222
<div class="row">
2323
<div class="col-4 fw-bold">
24-
Email:
24+
{% trans "Email" %}:
2525
</div>
2626
<div class="col-8">
2727
<a href="mailto:{{ object.user.email }}">{{ object.user.email }}</a>
2828
</div>
2929
</div>
3030
<div class="row">
3131
<div class="col-4 fw-bold">
32-
First name:
32+
{% trans "First name" %}:
3333
</div>
3434
<div class="col-8">
3535
{{ object.user.first_name }}
3636
</div>
3737
</div>
3838
<div class="row">
3939
<div class="col-4 fw-bold">
40-
Last name:
40+
{% trans "Last name" %}:
4141
</div>
4242
<div class="col-8">
4343
{{ object.user.last_name }}
4444
</div>
4545
</div>
4646
<div class="row">
4747
<div class="col-4 fw-bold">
48-
Point of sale:
48+
{% trans "Point of sale" %}:
4949
</div>
5050
<div class="col-8">
5151
{{ object.point_of_sale }}
@@ -54,7 +54,7 @@ <h3>Account:</h3>
5454
{% if object.street %}
5555
<div class="row">
5656
<div class="col-4 fw-bold">
57-
Street:
57+
{% trans "Street" %}:
5858
</div>
5959
<div class="col-8">
6060
{{ object.street }} {{ object.street_number }}
@@ -64,7 +64,7 @@ <h3>Account:</h3>
6464
{% if object.city or object.postal_code %}
6565
<div class="row">
6666
<div class="col-4 fw-bold">
67-
City:
67+
{% trans "City" %}:
6868
</div>
6969
<div class="col-8">
7070
{{ object.postal_code }} {{ object.city }}
@@ -74,7 +74,7 @@ <h3>Account:</h3>
7474
{% if object.telephone_number %}
7575
<div class="row">
7676
<div class="col-4 fw-bold">
77-
Telephone:
77+
{% trans "Telephone" %}:
7878
</div>
7979
<div class="col-8">
8080
{{ object.telephone_number }}
@@ -89,6 +89,14 @@ <h3>Account:</h3>
8989
{% if object.user.is_active %}<i class="fas fa-check-circle"></i>{% else %}<i class="fas fa-times-circle"></i>{% endif%}
9090
</div>
9191
</div>
92+
<div class="row">
93+
<div class="col-4 fw-bold">
94+
{% trans "Groups" %}:
95+
</div>
96+
<div class="col-8">
97+
{% for group in object.user.groups.all %}<span class="badge text-bg-dark me-2">{{ group }}</span>{% empty %} - {% endfor %}
98+
</div>
99+
</div>
92100
<div class="row">
93101
<div class="col-4 fw-bold">
94102
{% trans "date joined" %}:
@@ -109,7 +117,7 @@ <h3>Account:</h3>
109117
</div>
110118
</div>
111119
<div class="row mt-4">
112-
<div class="col col-md-6">
120+
<div class="col col-md-7">
113121
<div class="card p-3">
114122
<h3>Abo:</h3>
115123
<ul class="list-unstyled">
@@ -125,17 +133,17 @@ <h3>Abo:</h3>
125133
</div>
126134
</div>
127135
<div class="row mt-4">
128-
<div class="col col-md-6">
136+
<div class="col col-md-7">
129137
<div class="card p-3">
130138
<h3>Order history:</h3>
131139
<p>Total orders: <strong>{{ object.orders.all.count }}</strong><br>
132140
Total positions: <strong>{{ object.total_ordered_positions }}</strong></p>
133141
{% for order in customer.orders.all %}
134142
<div class="row border-bottom">
135-
<div class="col-4 fw-bold">
143+
<div class="col-3 fw-bold">
136144
{{ order.production_day }}
137145
</div>
138-
<div class="col-8">
146+
<div class="col-9">
139147
{% include "workshop/includes/order_positions.html" with positions=order.positions.all %}
140148
</div>
141149
</div>

0 commit comments

Comments
 (0)