Passed
Push — main ( de173d...744b84 )
by Jochen
04:10
created

byceps/blueprints/admin/shop/article/views.py (4 issues)

1
"""
2
byceps.blueprints.admin.shop.article.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2021 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from __future__ import annotations
10 1
from datetime import datetime
11 1
from decimal import Decimal
12
13 1
from flask import abort, request
14 1
from flask_babel import gettext
15
16 1
from .....services.brand import service as brand_service
17 1
from .....services.party import service as party_service
18 1
from .....services.party.transfer.models import Party
19 1
from .....services.shop.article import (
20
    sequence_service as article_sequence_service,
21
    service as article_service,
22
)
23 1
from .....services.shop.order import (
24
    action_registry_service,
25
    action_service,
26
    ordered_articles_service,
27
    service as order_service,
28
)
29 1
from .....services.shop.order.transfer.models import PaymentState
30 1
from .....services.shop.shop import service as shop_service
31 1
from .....services.ticketing import category_service as ticket_category_service
32 1
from .....services.ticketing.transfer.models import TicketCategory
33 1
from .....services.user import service as user_service
34 1
from .....services.user_badge import badge_service
35 1
from .....typing import BrandID
36 1
from .....util.authorization import register_permission_enum
37 1
from .....util.framework.blueprint import create_blueprint
38 1
from .....util.framework.flash import flash_error, flash_success
39 1
from .....util.framework.templating import templated
40 1
from .....util.templatefilters import local_tz_to_utc, utc_to_local_tz
41 1
from .....util.views import permission_required, redirect_to, respond_no_content
42
43 1
from .authorization import ShopArticlePermission
44 1
from .forms import (
45
    ArticleCreateForm,
46
    ArticleUpdateForm,
47
    ArticleAttachmentCreateForm,
48
    ArticleNumberSequenceCreateForm,
49
    RegisterBadgeAwardingActionForm,
50
    RegisterTicketBundlesCreationActionForm,
51
    RegisterTicketsCreationActionForm,
52
)
53
54
55 1
blueprint = create_blueprint('shop_article_admin', __name__)
56
57
58 1
register_permission_enum(ShopArticlePermission)
59
60
61 1
TAX_RATE_DISPLAY_FACTOR = Decimal('100')
62
63
64 1
@blueprint.get('/for_shop/<shop_id>', defaults={'page': 1})
65 1
@blueprint.get('/for_shop/<shop_id>/pages/<int:page>')
66 1
@permission_required(ShopArticlePermission.view)
67 1
@templated
68
def index_for_shop(shop_id, page):
69
    """List articles for that shop."""
70
    shop = _get_shop_or_404(shop_id)
71
72
    brand = brand_service.get_brand(shop.brand_id)
73
74
    per_page = request.args.get('per_page', type=int, default=15)
75
    articles = article_service.get_articles_for_shop_paginated(
76
        shop.id, page, per_page
77
    )
78
79
    return {
80
        'shop': shop,
81
        'brand': brand,
82
        'articles': articles,
83
    }
84
85
86 1
@blueprint.get('/<uuid:article_id>')
87 1
@permission_required(ShopArticlePermission.view)
88 1
@templated
89
def view(article_id):
90
    """Show a single article."""
91
    article = article_service.find_article_with_details(article_id)
92
    if article is None:
93
        abort(404)
94
95
    shop = shop_service.get_shop(article.shop_id)
96
97
    brand = brand_service.get_brand(shop.brand_id)
98
99
    totals = ordered_articles_service.count_ordered_articles(
100
        article.item_number
101
    )
102
103
    actions = action_service.get_actions_for_article(article.item_number)
104
    actions.sort(key=lambda a: a.payment_state.name, reverse=True)
105
106
    return {
107
        'article': article,
108
        'shop': shop,
109
        'brand': brand,
110
        'totals': totals,
111
        'PaymentState': PaymentState,
112
        'actions': actions,
113
    }
114
115
116 1
@blueprint.get('/<uuid:article_id>/ordered')
117 1
@permission_required(ShopArticlePermission.view)
118 1
@templated
119
def view_ordered(article_id):
120
    """List the people that have ordered this article, and the
121
    corresponding quantities.
122
    """
123
    article = _get_article_or_404(article_id)
124
125
    shop = shop_service.get_shop(article.shop_id)
126
127
    brand = brand_service.get_brand(shop.brand_id)
128
129
    order_items = ordered_articles_service.get_order_items_for_article(
130
        article.item_number
131
    )
132
133
    quantity_total = sum(item.quantity for item in order_items)
134
135
    order_numbers = {item.order_number for item in order_items}
136
    orders = order_service.find_orders_by_order_numbers(order_numbers)
137
    orders_by_order_numbers = {order.order_number: order for order in orders}
138
139
    user_ids = {order.placed_by_id for order in orders}
140
    users = user_service.find_users(user_ids, include_avatars=True)
141
    users_by_id = {user.id: user for user in users}
142
143
    def transform(order_item):
144
        quantity = order_item.quantity
145
        order = orders_by_order_numbers[order_item.order_number]
146
        user = users_by_id[order.placed_by_id]
147
148
        return quantity, order, user
149
150
    quantities_orders_users = list(map(transform, order_items))
151
152
    return {
153
        'article': article,
154
        'shop': shop,
155
        'brand': brand,
156
        'quantity_total': quantity_total,
157
        'quantities_orders_users': quantities_orders_users,
158
        'now': datetime.utcnow(),
159
    }
160
161
162
# -------------------------------------------------------------------- #
163
# create
164
165
166 1
@blueprint.get('/for_shop/<shop_id>/create')
167 1
@permission_required(ShopArticlePermission.create)
168 1
@templated
169 1
def create_form(shop_id, erroneous_form=None):
170
    """Show form to create an article."""
171
    shop = _get_shop_or_404(shop_id)
172
173
    brand = brand_service.get_brand(shop.brand_id)
174
175
    article_number_sequences = (
176
        article_sequence_service.get_article_number_sequences_for_shop(shop.id)
177
    )
178
    article_number_sequence_available = bool(article_number_sequences)
179
180
    form = (
181
        erroneous_form
182
        if erroneous_form
183
        else ArticleCreateForm(price=Decimal('0.0'), tax_rate=Decimal('19.0'))
184
    )
185
    form.set_article_number_sequence_choices(article_number_sequences)
186
187
    return {
188
        'shop': shop,
189
        'brand': brand,
190
        'article_number_sequence_available': article_number_sequence_available,
191
        'form': form,
192
    }
193
194
195 1
@blueprint.post('/for_shop/<shop_id>')
196 1
@permission_required(ShopArticlePermission.create)
197
def create(shop_id):
198
    """Create an article."""
199
    shop = _get_shop_or_404(shop_id)
200
201
    form = ArticleCreateForm(request.form)
202
203
    article_number_sequences = (
204
        article_sequence_service.get_article_number_sequences_for_shop(shop.id)
205
    )
206
    if not article_number_sequences:
207
        flash_error(
208
            gettext('No article number sequences are defined for this shop.')
209
        )
210
        return create_form(shop_id, form)
211
212
    form.set_article_number_sequence_choices(article_number_sequences)
213
    if not form.validate():
214
        return create_form(shop_id, form)
215
216
    article_number_sequence_id = form.article_number_sequence_id.data
217
    if not article_number_sequence_id:
218
        flash_error(gettext('No valid article number sequence was specified.'))
219
        return create_form(shop_id, form)
220
221
    article_number_sequence = (
222
        article_sequence_service.get_article_number_sequence(
223
            article_number_sequence_id
224
        )
225
    )
226
    if article_number_sequence.shop_id != shop.id:
227
        flash_error(gettext('No valid article number sequence was specified.'))
228
        return create_form(shop_id, form)
229
230
    try:
231
        item_number = article_sequence_service.generate_article_number(
232
            article_number_sequence.id
233
        )
234
    except article_sequence_service.ArticleNumberGenerationFailed as e:
235
        abort(500, e.message)
236
237
    description = form.description.data.strip()
238
    price = form.price.data
239
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
240
    total_quantity = form.total_quantity.data
241
    quantity = total_quantity
242
    max_quantity_per_order = form.max_quantity_per_order.data
243
244
    article = article_service.create_article(
245
        shop.id,
246
        item_number,
247
        description,
248
        price,
249
        tax_rate,
250
        total_quantity,
251
        max_quantity_per_order,
252
    )
253
254
    flash_success(
255
        gettext(
256
            'Article "%(item_number)s" has been created.',
257
            item_number=article.item_number,
258
        )
259
    )
260
    return redirect_to('.view', article_id=article.id)
261
262
263
# -------------------------------------------------------------------- #
264
# update
265
266
267 1
@blueprint.get('/<uuid:article_id>/update')
268 1
@permission_required(ShopArticlePermission.update)
269 1
@templated
270 1
def update_form(article_id, erroneous_form=None):
271
    """Show form to update an article."""
272
    article = _get_article_or_404(article_id)
273
274
    shop = shop_service.get_shop(article.shop_id)
275
276
    brand = brand_service.get_brand(shop.brand_id)
277
278
    if article.available_from:
279
        article.available_from = utc_to_local_tz(article.available_from)
280
    if article.available_until:
281
        article.available_until = utc_to_local_tz(article.available_until)
282
283
    form = (
284
        erroneous_form
285
        if erroneous_form
286
        else ArticleUpdateForm(
287
            obj=article,
288
            available_from_date=article.available_from.date()
289
            if article.available_from
290
            else None,
291
            available_from_time=article.available_from.time()
292
            if article.available_from
293
            else None,
294
            available_until_date=article.available_until.date()
295
            if article.available_until
296
            else None,
297
            available_until_time=article.available_until.time()
298
            if article.available_until
299
            else None,
300
        )
301
    )
302
    form.tax_rate.data = article.tax_rate * TAX_RATE_DISPLAY_FACTOR
303
304
    return {
305
        'article': article,
306
        'shop': shop,
307
        'brand': brand,
308
        'form': form,
309
    }
310
311
312 1
@blueprint.post('/<uuid:article_id>')
313 1
@permission_required(ShopArticlePermission.update)
314
def update(article_id):
315
    """Update an article."""
316
    article = _get_article_or_404(article_id)
317
318
    form = ArticleUpdateForm(request.form)
319
    if not form.validate():
320
        return update_form(article_id, form)
321
322
    description = form.description.data.strip()
323
    price = form.price.data
324
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
325
    if form.available_from_date.data and form.available_from_time.data:
326
        available_from = local_tz_to_utc(
327
            datetime.combine(
328
                form.available_from_date.data, form.available_from_time.data
329
            )
330
        )
331
    else:
332
        available_from = None
333
    if form.available_until_date.data and form.available_until_time.data:
334
        available_until = local_tz_to_utc(
335
            datetime.combine(
336
                form.available_until_date.data, form.available_until_time.data
337
            )
338
        )
339
    else:
340
        available_until = None
341
    total_quantity = form.total_quantity.data
342
    max_quantity_per_order = form.max_quantity_per_order.data
343
    not_directly_orderable = form.not_directly_orderable.data
344
    requires_separate_order = form.requires_separate_order.data
345
    shipping_required = form.shipping_required.data
346
347
    article = article_service.update_article(
348
        article.id,
349
        description,
350
        price,
351
        tax_rate,
352
        available_from,
353
        available_until,
354
        total_quantity,
355
        max_quantity_per_order,
356
        not_directly_orderable,
357
        requires_separate_order,
358
        shipping_required,
359
    )
360
361
    flash_success(
362
        gettext(
363
            'Article "%(description)s" has been updated.',
364
            description=article.description,
365
        )
366
    )
367
    return redirect_to('.view', article_id=article.id)
368
369
370
# -------------------------------------------------------------------- #
371
# article attachments
372
373
374 1
@blueprint.get('/<uuid:article_id>/attachments/create')
375 1
@permission_required(ShopArticlePermission.update)
376 1
@templated
377 1
def attachment_create_form(article_id, erroneous_form=None):
378
    """Show form to attach an article to another article."""
379
    article = _get_article_or_404(article_id)
380
381
    shop = shop_service.get_shop(article.shop_id)
382
383
    brand = brand_service.get_brand(shop.brand_id)
384
385
    attachable_articles = article_service.get_attachable_articles(article.id)
386
387
    form = (
388
        erroneous_form
389
        if erroneous_form
390
        else ArticleAttachmentCreateForm(quantity=0)
391
    )
392
    form.set_article_to_attach_choices(attachable_articles)
393
394
    return {
395
        'article': article,
396
        'shop': shop,
397
        'brand': brand,
398
        'form': form,
399
    }
400
401
402 1
@blueprint.post('/<uuid:article_id>/attachments')
403 1
@permission_required(ShopArticlePermission.update)
404
def attachment_create(article_id):
405
    """Attach an article to another article."""
406
    article = _get_article_or_404(article_id)
407
408
    attachable_articles = article_service.get_attachable_articles(article.id)
409
410
    form = ArticleAttachmentCreateForm(request.form)
411
    form.set_article_to_attach_choices(attachable_articles)
412
413
    if not form.validate():
414
        return attachment_create_form(article_id, form)
415
416
    article_to_attach_id = form.article_to_attach_id.data
417
    article_to_attach = article_service.get_article(article_to_attach_id)
418
    quantity = form.quantity.data
419
420
    article_service.attach_article(
421
        article_to_attach.item_number, quantity, article.item_number
422
    )
423
424
    flash_success(
425
        gettext(
426
            'Article "%(article_to_attach_item_number)s" has been attached %(quantity)s times to article "%(article_item_number)s".',
427
            article_to_attach_item_number=article_to_attach.item_number,
428
            quantity=quantity,
429
            article_item_number=article.item_number,
430
        )
431
    )
432
    return redirect_to('.view', article_id=article.id)
433
434
435 1
@blueprint.delete('/attachments/<uuid:article_id>')
436 1
@permission_required(ShopArticlePermission.update)
437 1
@respond_no_content
438
def attachment_remove(article_id):
439
    """Remove the attachment link from one article to another."""
440
    attached_article = article_service.find_attached_article(article_id)
441
442
    if attached_article is None:
443
        abort(404)
444
445
    article = attached_article.article
446
    attached_to_article = attached_article.attached_to_article
447
448
    article_service.unattach_article(attached_article.id)
449
450
    flash_success(
451
        gettext(
452
            'Article "%(article_item_number)s" is no longer attached to article "%(attached_to_article_item_number)s".',
453
            article_item_number=article.item_number,
454
            attached_to_article_item_number=attached_to_article.item_number,
455
        )
456
    )
457
458
459
# -------------------------------------------------------------------- #
460
# actions
461
462
463 1
@blueprint.get('/<uuid:article_id>/actions/badge_awarding/create')
464 1
@permission_required(ShopArticlePermission.update)
465 1
@templated
466 1
def action_create_form_for_badge_awarding(article_id, erroneous_form=None):
467
    """Show form to register a badge awarding action for the article."""
468
    article = _get_article_or_404(article_id)
469
470
    shop = shop_service.get_shop(article.shop_id)
471
    brand = brand_service.get_brand(shop.brand_id)
472
473
    badges = badge_service.get_all_badges()
474
475
    form = (
476
        erroneous_form if erroneous_form else RegisterBadgeAwardingActionForm()
477
    )
478
    form.set_badge_choices(badges)
479
480
    return {
481
        'article': article,
482
        'shop': shop,
483
        'brand': brand,
484
        'form': form,
485
    }
486
487
488 1
@blueprint.post('/<uuid:article_id>/actions/badge_awarding')
489 1
@permission_required(ShopArticlePermission.update)
490
def action_create_for_badge_awarding(article_id):
491
    """Register a badge awarding action for the article."""
492
    article = _get_article_or_404(article_id)
493
494
    badges = badge_service.get_all_badges()
495
496
    form = RegisterBadgeAwardingActionForm(request.form)
497
    form.set_badge_choices(badges)
498
499
    if not form.validate():
500
        return action_create_form_for_badge_awarding(article_id, form)
501
502
    badge_id = form.badge_id.data
503
    badge = badge_service.get_badge(badge_id)
504
505
    action_registry_service.register_badge_awarding(
506
        article.item_number, badge.id
507
    )
508
509
    flash_success(gettext('Action has been added.'))
510
511
    return redirect_to('.view', article_id=article.id)
512
513
514 1 View Code Duplication
@blueprint.get('/<uuid:article_id>/actions/tickets_creation/create')
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
515 1
@permission_required(ShopArticlePermission.update)
516 1
@templated
517 1
def action_create_form_for_tickets_creation(article_id, erroneous_form=None):
518
    """Show form to register a tickets creation action for the article."""
519
    article = _get_article_or_404(article_id)
520
521
    shop = shop_service.get_shop(article.shop_id)
522
    brand = brand_service.get_brand(shop.brand_id)
523
524
    form = (
525
        erroneous_form
526
        if erroneous_form
527
        else RegisterTicketsCreationActionForm()
528
    )
529
    form.set_category_choices(_get_categories_with_parties(brand.id))
530
531
    return {
532
        'article': article,
533
        'shop': shop,
534
        'brand': brand,
535
        'form': form,
536
    }
537
538
539 1 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/tickets_creation')
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
540 1
@permission_required(ShopArticlePermission.update)
541
def action_create_for_tickets_creation(article_id):
542
    """Register a tickets creation action for the article."""
543
    article = _get_article_or_404(article_id)
544
545
    shop = shop_service.get_shop(article.shop_id)
546
    brand = brand_service.get_brand(shop.brand_id)
547
548
    form = RegisterTicketsCreationActionForm(request.form)
549
    form.set_category_choices(_get_categories_with_parties(brand.id))
550
551
    if not form.validate():
552
        return action_create_form_for_tickets_creation(article_id, form)
553
554
    category_id = form.category_id.data
555
    category = ticket_category_service.find_category(category_id)
556
    if category is None:
557
        raise ValueError(f'Unknown category ID "{category_id}"')
558
559
    action_registry_service.register_tickets_creation(
560
        article.item_number, category.id
561
    )
562
563
    flash_success(gettext('Action has been added.'))
564
565
    return redirect_to('.view', article_id=article.id)
566
567
568 1 View Code Duplication
@blueprint.get('/<uuid:article_id>/actions/ticket_bundles_creation/create')
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
569 1
@permission_required(ShopArticlePermission.update)
570 1
@templated
571 1
def action_create_form_for_ticket_bundles_creation(
572
    article_id, erroneous_form=None
573
):
574
    """Show form to register a ticket bundles creation action for the article."""
575
    article = _get_article_or_404(article_id)
576
577
    shop = shop_service.get_shop(article.shop_id)
578
    brand = brand_service.get_brand(shop.brand_id)
579
580
    form = (
581
        erroneous_form
582
        if erroneous_form
583
        else RegisterTicketBundlesCreationActionForm()
584
    )
585
    form.set_category_choices(_get_categories_with_parties(brand.id))
586
587
    return {
588
        'article': article,
589
        'shop': shop,
590
        'brand': brand,
591
        'form': form,
592
    }
593
594
595 1 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/ticket_bundles_creation')
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
596 1
@permission_required(ShopArticlePermission.update)
597
def action_create_for_ticket_bundles_creation(article_id):
598
    """Register a ticket bundles creation action for the article."""
599
    article = _get_article_or_404(article_id)
600
601
    shop = shop_service.get_shop(article.shop_id)
602
    brand = brand_service.get_brand(shop.brand_id)
603
604
    form = RegisterTicketBundlesCreationActionForm(request.form)
605
    form.set_category_choices(_get_categories_with_parties(brand.id))
606
607
    if not form.validate():
608
        return action_create_form_for_ticket_bundles_creation(article_id, form)
609
610
    category_id = form.category_id.data
611
    category = ticket_category_service.find_category(category_id)
612
    if category is None:
613
        raise ValueError(f'Unknown category ID "{category_id}"')
614
615
    ticket_quantity = form.ticket_quantity.data
616
617
    action_registry_service.register_ticket_bundles_creation(
618
        article.item_number, category.id, ticket_quantity
619
    )
620
621
    flash_success(gettext('Action has been added.'))
622
623
    return redirect_to('.view', article_id=article.id)
624
625
626 1
def _get_categories_with_parties(
627
    brand_id: BrandID,
628
) -> set[tuple[TicketCategory, Party]]:
629
    return {
630
        (category, party)
631
        for party in party_service.get_active_parties(brand_id)
632
        for category in ticket_category_service.get_categories_for_party(
633
            party.id
634
        )
635
    }
636
637
638 1
@blueprint.delete('/actions/<uuid:action_id>')
639 1
@permission_required(ShopArticlePermission.update)
640 1
@respond_no_content
641
def action_remove(action_id):
642
    """Remove the action from the article."""
643
    action = action_service.find_action(action_id)
644
645
    if action is None:
646
        abort(404)
647
648
    action_service.delete_action(action.id)
649
650
    flash_success(gettext('Action has been removed.'))
651
652
653
# -------------------------------------------------------------------- #
654
# article number sequences
655
656
657 1
@blueprint.get('/number_sequences/for_shop/<shop_id>/create')
658 1
@permission_required(ShopArticlePermission.create)
659 1
@templated
660 1
def create_number_sequence_form(shop_id, erroneous_form=None):
661
    """Show form to create an article number sequence."""
662
    shop = _get_shop_or_404(shop_id)
663
664
    brand = brand_service.get_brand(shop.brand_id)
665
666
    form = (
667
        erroneous_form if erroneous_form else ArticleNumberSequenceCreateForm()
668
    )
669
670
    return {
671
        'shop': shop,
672
        'brand': brand,
673
        'form': form,
674
    }
675
676
677 1
@blueprint.post('/number_sequences/for_shop/<shop_id>')
678 1
@permission_required(ShopArticlePermission.create)
679
def create_number_sequence(shop_id):
680
    """Create an article number sequence."""
681
    shop = _get_shop_or_404(shop_id)
682
683
    form = ArticleNumberSequenceCreateForm(request.form)
684
    if not form.validate():
685
        return create_number_sequence_form(shop_id, form)
686
687
    prefix = form.prefix.data.strip()
688
689
    sequence_id = article_sequence_service.create_article_number_sequence(
690
        shop.id, prefix
691
    )
692
    if sequence_id is None:
693
        flash_error(
694
            gettext(
695
                'Article number sequence could not be created. '
696
                'Is prefix "%(prefix)s" already defined?',
697
                prefix=prefix,
698
            )
699
        )
700
        return create_number_sequence_form(shop.id, form)
701
702
    flash_success(
703
        gettext(
704
            'Article number sequence with prefix "%(prefix)s" has been created.',
705
            prefix=prefix,
706
        )
707
    )
708
    return redirect_to('.index_for_shop', shop_id=shop.id)
709
710
711
# -------------------------------------------------------------------- #
712
# helpers
713
714
715 1
def _get_shop_or_404(shop_id):
716
    shop = shop_service.find_shop(shop_id)
717
718
    if shop is None:
719
        abort(404)
720
721
    return shop
722
723
724 1
def _get_article_or_404(article_id):
725
    article = article_service.find_db_article(article_id)
726
727
    if article is None:
728
        abort(404)
729
730
    return article
731