Passed
Push — main ( 5549e4...7e867c )
by Jochen
04:43
created

action_create_for_ticket_bundles_creation()   A

Complexity

Conditions 3

Size

Total Lines 29
Code Lines 19

Duplication

Lines 29
Ratio 100 %

Code Coverage

Tests 2
CRAP Score 9.1835

Importance

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