Test Failed
Push — main ( 5f90c7...7c5752 )
by Jochen
04:45
created

byceps/blueprints/admin/shop/article/views.py (1 issue)

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 1
    sequence_service as article_sequence_service,
21
    service as article_service,
22
)
23
from .....services.shop.article.transfer.models import (
24 1
    ArticleType,
25
    get_article_type_label,
26
)
27
from .....services.shop.order import (
28 1
    action_registry_service,
29
    action_service,
30
    ordered_articles_service,
31
    service as order_service,
32
)
33
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.framework.blueprint import create_blueprint
41 1
from .....util.framework.flash import flash_error, flash_success
42 1
from .....util.framework.templating import templated
43 1
from .....util.templatefilters import local_tz_to_utc, utc_to_local_tz
44 1
from .....util.views import permission_required, redirect_to, respond_no_content
45 1
46
from .forms import (
47 1
    ArticleCreateForm,
48
    ArticleUpdateForm,
49
    ArticleAttachmentCreateForm,
50
    ArticleNumberSequenceCreateForm,
51
    RegisterBadgeAwardingActionForm,
52
    RegisterTicketBundlesCreationActionForm,
53
    RegisterTicketsCreationActionForm,
54
)
55
56
57
blueprint = create_blueprint('shop_article_admin', __name__)
58 1
59
60
TAX_RATE_DISPLAY_FACTOR = Decimal('100')
61 1
62
63
@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 1
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
@blueprint.get('/<uuid:article_id>')
86 1
@permission_required('shop_article.view')
87 1
@templated
88 1
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
@blueprint.get('/<uuid:article_id>/ordered')
119 1
@permission_required('shop_article.view')
120 1
@templated
121 1
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
@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 1
    """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
@blueprint.post('/for_shop/<shop_id>')
198 1
@permission_required('shop_article.create')
199 1
def create(shop_id):
200
    """Create an article."""
201
    shop = _get_shop_or_404(shop_id)
202
203
    form = ArticleCreateForm(request.form)
204
    if not form.validate():
205
        return create_form(shop_id, form)
206
207
    article_number_sequences = (
208
        article_sequence_service.get_article_number_sequences_for_shop(shop.id)
209
    )
210
    if not article_number_sequences:
211
        flash_error(
212
            gettext('No article number sequences are defined for this shop.')
213
        )
214
        return create_form(shop_id, form)
215
216
    form.set_article_number_sequence_choices(article_number_sequences)
217
    if not form.validate():
218
        return create_form(shop_id, form)
219
220
    article_number_sequence_id = form.article_number_sequence_id.data
221
    if not article_number_sequence_id:
222
        flash_error(gettext('No valid article number sequence was specified.'))
223
        return create_form(shop_id, form)
224
225
    article_number_sequence = (
226
        article_sequence_service.get_article_number_sequence(
227
            article_number_sequence_id
228
        )
229
    )
230
    if article_number_sequence.shop_id != shop.id:
231
        flash_error(gettext('No valid article number sequence was specified.'))
232
        return create_form(shop_id, form)
233
234
    try:
235
        item_number = article_sequence_service.generate_article_number(
236
            article_number_sequence.id
237
        )
238
    except article_sequence_service.ArticleNumberGenerationFailed as e:
239
        abort(500, e.message)
240
241
    type_ = ArticleType[form.type_.data]
242
    description = form.description.data.strip()
243
    price = form.price.data
244
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
245
    total_quantity = form.total_quantity.data
246
    quantity = total_quantity
247
    max_quantity_per_order = form.max_quantity_per_order.data
248
    processing_required = type_ == ArticleType.physical
249
250
    article = article_service.create_article(
251
        shop.id,
252
        item_number,
253
        type_,
254
        description,
255
        price,
256
        tax_rate,
257
        total_quantity,
258
        max_quantity_per_order,
259
        processing_required,
260
    )
261
262
    flash_success(
263
        gettext(
264
            'Article "%(item_number)s" has been created.',
265
            item_number=article.item_number,
266
        )
267
    )
268
    return redirect_to('.view', article_id=article.id)
269
270
271
# -------------------------------------------------------------------- #
272
# update
273
274
275
@blueprint.get('/<uuid:article_id>/update')
276 1
@permission_required('shop_article.update')
277 1
@templated
278 1
def update_form(article_id, erroneous_form=None):
279 1
    """Show form to update an article."""
280
    article = _get_article_or_404(article_id)
281
282
    shop = shop_service.get_shop(article.shop_id)
283
284
    brand = brand_service.get_brand(shop.brand_id)
285
286
    if article.available_from:
287
        article.available_from = utc_to_local_tz(article.available_from)
288
    if article.available_until:
289
        article.available_until = utc_to_local_tz(article.available_until)
290
291
    form = (
292
        erroneous_form
293
        if erroneous_form
294
        else ArticleUpdateForm(
295
            obj=article,
296
            available_from_date=article.available_from.date()
297
            if article.available_from
298
            else None,
299
            available_from_time=article.available_from.time()
300
            if article.available_from
301
            else None,
302
            available_until_date=article.available_until.date()
303
            if article.available_until
304
            else None,
305
            available_until_time=article.available_until.time()
306
            if article.available_until
307
            else None,
308
        )
309
    )
310
    form.tax_rate.data = article.tax_rate * TAX_RATE_DISPLAY_FACTOR
311
312
    return {
313
        'article': article,
314
        'shop': shop,
315
        'brand': brand,
316
        'form': form,
317
    }
318
319
320
@blueprint.post('/<uuid:article_id>')
321 1
@permission_required('shop_article.update')
322 1
def update(article_id):
323
    """Update an article."""
324
    article = _get_article_or_404(article_id)
325
326
    form = ArticleUpdateForm(request.form)
327
    if not form.validate():
328
        return update_form(article_id, form)
329
330
    description = form.description.data.strip()
331
    price = form.price.data
332
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
333
    if form.available_from_date.data and form.available_from_time.data:
334
        available_from = local_tz_to_utc(
335
            datetime.combine(
336
                form.available_from_date.data, form.available_from_time.data
337
            )
338
        )
339
    else:
340
        available_from = None
341
    if form.available_until_date.data and form.available_until_time.data:
342
        available_until = local_tz_to_utc(
343
            datetime.combine(
344
                form.available_until_date.data, form.available_until_time.data
345
            )
346
        )
347
    else:
348
        available_until = None
349
    total_quantity = form.total_quantity.data
350
    max_quantity_per_order = form.max_quantity_per_order.data
351
    not_directly_orderable = form.not_directly_orderable.data
352
    separate_order_required = form.separate_order_required.data
353
354
    article = article_service.update_article(
355
        article.id,
356
        description,
357
        price,
358
        tax_rate,
359
        available_from,
360
        available_until,
361
        total_quantity,
362
        max_quantity_per_order,
363
        not_directly_orderable,
364
        separate_order_required,
365
    )
366
367
    flash_success(
368
        gettext(
369
            'Article "%(description)s" has been updated.',
370
            description=article.description,
371
        )
372
    )
373
    return redirect_to('.view', article_id=article.id)
374
375
376
# -------------------------------------------------------------------- #
377
# article attachments
378
379
380
@blueprint.get('/<uuid:article_id>/attachments/create')
381 1
@permission_required('shop_article.update')
382 1
@templated
383 1
def attachment_create_form(article_id, erroneous_form=None):
384 1
    """Show form to attach an article to another article."""
385
    article = _get_article_or_404(article_id)
386
387
    shop = shop_service.get_shop(article.shop_id)
388
389
    brand = brand_service.get_brand(shop.brand_id)
390
391
    attachable_articles = article_service.get_attachable_articles(article.id)
392
393
    form = (
394
        erroneous_form
395
        if erroneous_form
396
        else ArticleAttachmentCreateForm(quantity=0)
397
    )
398
    form.set_article_to_attach_choices(attachable_articles)
399
400
    return {
401
        'article': article,
402
        'shop': shop,
403
        'brand': brand,
404
        'form': form,
405
    }
406
407
408
@blueprint.post('/<uuid:article_id>/attachments')
409 1
@permission_required('shop_article.update')
410 1
def attachment_create(article_id):
411
    """Attach an article to another article."""
412
    article = _get_article_or_404(article_id)
413
414
    attachable_articles = article_service.get_attachable_articles(article.id)
415
416
    form = ArticleAttachmentCreateForm(request.form)
417
    form.set_article_to_attach_choices(attachable_articles)
418
419
    if not form.validate():
420
        return attachment_create_form(article_id, form)
421
422
    article_to_attach_id = form.article_to_attach_id.data
423
    article_to_attach = article_service.get_article(article_to_attach_id)
424
    quantity = form.quantity.data
425
426
    article_service.attach_article(
427
        article_to_attach.item_number, quantity, article.item_number
428
    )
429
430
    flash_success(
431
        gettext(
432
            'Article "%(article_to_attach_item_number)s" has been attached %(quantity)s times to article "%(article_item_number)s".',
433
            article_to_attach_item_number=article_to_attach.item_number,
434
            quantity=quantity,
435
            article_item_number=article.item_number,
436
        )
437
    )
438
    return redirect_to('.view', article_id=article.id)
439
440
441
@blueprint.delete('/attachments/<uuid:article_id>')
442 1
@permission_required('shop_article.update')
443 1
@respond_no_content
444 1
def attachment_remove(article_id):
445
    """Remove the attachment link from one article to another."""
446
    attached_article = article_service.find_attached_article(article_id)
447
448
    if attached_article is None:
449
        abort(404)
450
451
    article = attached_article.article
452
    attached_to_article = attached_article.attached_to_article
453
454
    article_service.unattach_article(attached_article.id)
455
456
    flash_success(
457
        gettext(
458
            'Article "%(article_item_number)s" is no longer attached to article "%(attached_to_article_item_number)s".',
459
            article_item_number=article.item_number,
460
            attached_to_article_item_number=attached_to_article.item_number,
461
        )
462
    )
463
464
465
# -------------------------------------------------------------------- #
466
# actions
467
468
469
@blueprint.get('/<uuid:article_id>/actions/badge_awarding/create')
470 1
@permission_required('shop_article.update')
471 1
@templated
472 1
def action_create_form_for_badge_awarding(article_id, erroneous_form=None):
473 1
    """Show form to register a badge awarding action for the article."""
474
    article = _get_article_or_404(article_id)
475
476
    shop = shop_service.get_shop(article.shop_id)
477
    brand = brand_service.get_brand(shop.brand_id)
478
479
    badges = badge_service.get_all_badges()
480
481
    form = (
482
        erroneous_form if erroneous_form else RegisterBadgeAwardingActionForm()
483
    )
484
    form.set_badge_choices(badges)
485
486
    return {
487
        'article': article,
488
        'shop': shop,
489
        'brand': brand,
490
        'form': form,
491
    }
492
493
494
@blueprint.post('/<uuid:article_id>/actions/badge_awarding')
495 1
@permission_required('shop_article.update')
496 1
def action_create_for_badge_awarding(article_id):
497
    """Register a badge awarding action for the article."""
498
    article = _get_article_or_404(article_id)
499
500
    badges = badge_service.get_all_badges()
501
502
    form = RegisterBadgeAwardingActionForm(request.form)
503
    form.set_badge_choices(badges)
504
505
    if not form.validate():
506
        return action_create_form_for_badge_awarding(article_id, form)
507
508
    badge_id = form.badge_id.data
509
    badge = badge_service.get_badge(badge_id)
510
511
    action_registry_service.register_badge_awarding(
512
        article.item_number, badge.id
513
    )
514
515
    flash_success(gettext('Action has been added.'))
516
517
    return redirect_to('.view', article_id=article.id)
518
519
520
@blueprint.get('/<uuid:article_id>/actions/tickets_creation/create')
521 1
@permission_required('shop_article.update')
522 1
@templated
523 1
def action_create_form_for_tickets_creation(article_id, erroneous_form=None):
524 1
    """Show form to register a tickets creation action for the article."""
525
    article = _get_article_or_404(article_id)
526
527
    shop = shop_service.get_shop(article.shop_id)
528
    brand = brand_service.get_brand(shop.brand_id)
529
530
    form = (
531
        erroneous_form
532
        if erroneous_form
533
        else RegisterTicketsCreationActionForm()
534
    )
535
    form.set_category_choices(_get_categories_with_parties(brand.id))
536
537
    return {
538
        'article': article,
539
        'shop': shop,
540
        'brand': brand,
541
        'form': form,
542
    }
543
544
545 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/tickets_creation')
546 1
@permission_required('shop_article.update')
547 1
def action_create_for_tickets_creation(article_id):
548
    """Register a tickets creation action for the article."""
549
    article = _get_article_or_404(article_id)
550
551
    shop = shop_service.get_shop(article.shop_id)
552
    brand = brand_service.get_brand(shop.brand_id)
553
554
    form = RegisterTicketsCreationActionForm(request.form)
555
    form.set_category_choices(_get_categories_with_parties(brand.id))
556
557
    if not form.validate():
558
        return action_create_form_for_tickets_creation(article_id, form)
559
560
    category_id = form.category_id.data
561
    category = ticket_category_service.find_category(category_id)
562
    if category is None:
563
        raise ValueError(f'Unknown category ID "{category_id}"')
564
565
    action_registry_service.register_tickets_creation(
566
        article.item_number, category.id
567
    )
568
569
    flash_success(gettext('Action has been added.'))
570
571
    return redirect_to('.view', article_id=article.id)
572
573
574
@blueprint.get('/<uuid:article_id>/actions/ticket_bundles_creation/create')
575 1
@permission_required('shop_article.update')
576 1
@templated
577 1
def action_create_form_for_ticket_bundles_creation(
578 1
    article_id, erroneous_form=None
579
):
580
    """Show form to register a ticket bundles creation action for the article."""
581
    article = _get_article_or_404(article_id)
582
583
    shop = shop_service.get_shop(article.shop_id)
584
    brand = brand_service.get_brand(shop.brand_id)
585
586
    form = (
587
        erroneous_form
588
        if erroneous_form
589
        else RegisterTicketBundlesCreationActionForm()
590
    )
591
    form.set_category_choices(_get_categories_with_parties(brand.id))
592
593
    return {
594
        'article': article,
595
        'shop': shop,
596
        'brand': brand,
597
        'form': form,
598
    }
599
600
601 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/ticket_bundles_creation')
602 1
@permission_required('shop_article.update')
603 1
def action_create_for_ticket_bundles_creation(article_id):
604
    """Register a ticket bundles creation action for the article."""
605
    article = _get_article_or_404(article_id)
606
607
    shop = shop_service.get_shop(article.shop_id)
608
    brand = brand_service.get_brand(shop.brand_id)
609
610
    form = RegisterTicketBundlesCreationActionForm(request.form)
611
    form.set_category_choices(_get_categories_with_parties(brand.id))
612
613
    if not form.validate():
614
        return action_create_form_for_ticket_bundles_creation(article_id, form)
615
616
    category_id = form.category_id.data
617
    category = ticket_category_service.find_category(category_id)
618
    if category is None:
619
        raise ValueError(f'Unknown category ID "{category_id}"')
620
621
    ticket_quantity = form.ticket_quantity.data
622
623
    action_registry_service.register_ticket_bundles_creation(
624
        article.item_number, category.id, ticket_quantity
625
    )
626
627
    flash_success(gettext('Action has been added.'))
628
629
    return redirect_to('.view', article_id=article.id)
630
631
632
def _get_categories_with_parties(
633 1
    brand_id: BrandID,
634
) -> set[tuple[TicketCategory, Party]]:
635
    return {
636
        (category, party)
637
        for party in party_service.get_active_parties(brand_id)
638
        for category in ticket_category_service.get_categories_for_party(
639
            party.id
640
        )
641
    }
642
643
644
@blueprint.delete('/actions/<uuid:action_id>')
645 1
@permission_required('shop_article.update')
646 1
@respond_no_content
647 1
def action_remove(action_id):
648
    """Remove the action from the article."""
649
    action = action_service.find_action(action_id)
650
651
    if action is None:
652
        abort(404)
653
654
    action_service.delete_action(action.id)
655
656
    flash_success(gettext('Action has been removed.'))
657
658
659
# -------------------------------------------------------------------- #
660
# article number sequences
661
662
663
@blueprint.get('/number_sequences/for_shop/<shop_id>/create')
664 1
@permission_required('shop_article.create')
665 1
@templated
666 1
def create_number_sequence_form(shop_id, erroneous_form=None):
667 1
    """Show form to create an article number sequence."""
668
    shop = _get_shop_or_404(shop_id)
669
670
    brand = brand_service.get_brand(shop.brand_id)
671
672
    form = (
673
        erroneous_form if erroneous_form else ArticleNumberSequenceCreateForm()
674
    )
675
676
    return {
677
        'shop': shop,
678
        'brand': brand,
679
        'form': form,
680
    }
681
682
683 View Code Duplication
@blueprint.post('/number_sequences/for_shop/<shop_id>')
1 ignored issue
show
This code seems to be duplicated in your project.
Loading history...
684 1
@permission_required('shop_article.create')
685 1
def create_number_sequence(shop_id):
686
    """Create an article number sequence."""
687
    shop = _get_shop_or_404(shop_id)
688
689
    form = ArticleNumberSequenceCreateForm(request.form)
690
    if not form.validate():
691
        return create_number_sequence_form(shop_id, form)
692
693
    prefix = form.prefix.data.strip()
694
695
    sequence_id = article_sequence_service.create_article_number_sequence(
696
        shop.id, prefix
697
    )
698
    if sequence_id is None:
699
        flash_error(
700
            gettext(
701
                'Article number sequence could not be created. '
702
                'Is prefix "%(prefix)s" already defined?',
703
                prefix=prefix,
704
            )
705
        )
706
        return create_number_sequence_form(shop.id, form)
707
708
    flash_success(
709
        gettext(
710
            'Article number sequence with prefix "%(prefix)s" has been created.',
711
            prefix=prefix,
712
        )
713
    )
714
    return redirect_to('.index_for_shop', shop_id=shop.id)
715
716
717
# -------------------------------------------------------------------- #
718
# helpers
719
720
721
def _get_shop_or_404(shop_id):
722 1
    shop = shop_service.find_shop(shop_id)
723
724
    if shop is None:
725
        abort(404)
726
727
    return shop
728
729
730
def _get_article_or_404(article_id):
731 1
    article = article_service.find_db_article(article_id)
732
733
    if article is None:
734
        abort(404)
735
736
    return article
737