byceps.blueprints.admin.shop.article.views   F
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 916
Duplicated Lines 26.75 %

Test Coverage

Coverage 29.29%

Importance

Changes 0
Metric Value
wmc 66
eloc 599
dl 245
loc 916
ccs 111
cts 379
cp 0.2929
rs 3.12
c 0
b 0
f 0

27 Functions

Rating   Name   Duplication   Size   Complexity  
A attachment_create_form() 0 25 2
A _assemble_datetime_utc() 0 6 3
A update() 0 46 2
A view_orders() 32 32 1
A _get_article_or_404() 0 7 2
A create_ticket_form() 32 32 2
A action_create_form_for_badge_awarding() 0 22 2
A action_create_form_for_tickets_creation() 0 22 2
A action_create_form_for_ticket_bundles_creation() 0 24 2
A view_purchases() 34 34 1
F create() 0 125 11
B view() 0 42 5
A attachment_create() 0 29 2
A create_number_sequence() 32 32 3
A update_form() 0 30 4
A action_create_for_badge_awarding() 0 22 2
A _get_article_type_or_400() 0 5 2
A _get_shop_or_404() 0 7 2
A create_number_sequence_form() 0 17 2
A _get_active_article_number_sequences_for_shop() 0 7 1
A attachment_remove() 0 20 2
A action_remove() 0 13 2
A create_ticket_bundle_form() 32 32 2
A action_create_for_tickets_creation() 25 25 2
A action_create_for_ticket_bundles_creation() 27 27 2
A create_form() 31 31 2
A index_for_shop() 0 42 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like byceps.blueprints.admin.shop.article.views often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
byceps.blueprints.admin.shop.article.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2014-2023 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from __future__ import annotations
10
11 1
import dataclasses
12 1
from datetime import date, datetime, time
13 1
from decimal import Decimal
14
15 1
from flask import abort, request
16 1
from flask_babel import gettext, to_user_timezone, to_utc
17 1
from moneyed import Money
18
19 1
from byceps.services.brand import brand_service
20 1
from byceps.services.party import party_service
21 1
from byceps.services.shop.article import (
22
    article_sequence_service,
23
    article_service,
24
)
25 1
from byceps.services.shop.article.models import (
26
    Article,
27
    ArticleNumberSequence,
28
    ArticleType,
29
    get_article_type_label,
30
)
31 1
from byceps.services.shop.order import (
32
    order_action_registry_service,
33
    order_action_service,
34
    ordered_articles_service,
35
)
36 1
from byceps.services.shop.order.models.order import Order, PaymentState
37 1
from byceps.services.shop.shop import shop_service
38 1
from byceps.services.shop.shop.models import ShopID
39 1
from byceps.services.ticketing import ticket_category_service
40 1
from byceps.services.user_badge import user_badge_service
41 1
from byceps.util.framework.blueprint import create_blueprint
42 1
from byceps.util.framework.flash import flash_error, flash_success
43 1
from byceps.util.framework.templating import templated
44 1
from byceps.util.views import (
45
    permission_required,
46
    redirect_to,
47
    respond_no_content,
48
)
49
50 1
from .forms import (
51
    ArticleAttachmentCreateForm,
52
    ArticleCreateForm,
53
    ArticleNumberSequenceCreateForm,
54
    ArticleUpdateForm,
55
    RegisterBadgeAwardingActionForm,
56
    RegisterTicketBundlesCreationActionForm,
57
    RegisterTicketsCreationActionForm,
58
    TicketArticleCreateForm,
59
    TicketBundleArticleCreateForm,
60
)
61
62
63 1
blueprint = create_blueprint('shop_article_admin', __name__)
64
65
66 1
TAX_RATE_DISPLAY_FACTOR = Decimal('100')
67
68
69 1
@blueprint.get('/for_shop/<shop_id>', defaults={'page': 1})
70 1
@blueprint.get('/for_shop/<shop_id>/pages/<int:page>')
71 1
@permission_required('shop_article.view')
72 1
@templated
73 1
def index_for_shop(shop_id, page):
74
    """List articles for that shop."""
75
    shop = _get_shop_or_404(shop_id)
76
77
    brand = brand_service.get_brand(shop.brand_id)
78
79
    per_page = request.args.get('per_page', type=int, default=15)
80
81
    search_term = request.args.get('search_term', default='').strip()
82
83
    articles = article_service.get_articles_for_shop_paginated(
84
        shop.id,
85
        page,
86
        per_page,
87
        search_term=search_term,
88
    )
89
90
    # Inherit order of enum members.
91
    article_type_labels_by_type = {
92
        type_: get_article_type_label(type_) for type_ in ArticleType
93
    }
94
95
    totals_by_article_number = {
96
        article.item_number: ordered_articles_service.count_ordered_articles(
97
            article.id
98
        )
99
        for article in articles.items
100
    }
101
102
    return {
103
        'shop': shop,
104
        'brand': brand,
105
        'articles': articles,
106
        'article_type_labels_by_type': article_type_labels_by_type,
107
        'totals_by_article_number': totals_by_article_number,
108
        'PaymentState': PaymentState,
109
        'per_page': per_page,
110
        'search_term': search_term,
111
    }
112
113
114 1
@blueprint.get('/<uuid:article_id>')
115 1
@permission_required('shop_article.view')
116 1
@templated
117 1
def view(article_id):
118
    """Show a single article."""
119
    article = article_service.find_article_with_details(article_id)
120
    if article is None:
121
        abort(404)
122
123
    shop = shop_service.get_shop(article.shop_id)
124
125
    brand = brand_service.get_brand(shop.brand_id)
126
127
    type_label = get_article_type_label(article.type_)
128
129
    if article.type_ in (ArticleType.ticket, ArticleType.ticket_bundle):
130
        ticket_category = ticket_category_service.find_category(
131
            article.type_params['ticket_category_id']
132
        )
133
        if ticket_category is not None:
134
            ticket_party = party_service.get_party(ticket_category.party_id)
135
        else:
136
            ticket_party = None
137
    else:
138
        ticket_party = None
139
        ticket_category = None
140
141
    totals = ordered_articles_service.count_ordered_articles(article.id)
142
143
    actions = order_action_service.get_actions_for_article(article.id)
144
    actions.sort(key=lambda a: a.payment_state.name, reverse=True)
145
146
    return {
147
        'article': article,
148
        'shop': shop,
149
        'brand': brand,
150
        'type_label': type_label,
151
        'ticket_category': ticket_category,
152
        'ticket_party': ticket_party,
153
        'totals': totals,
154
        'PaymentState': PaymentState,
155
        'actions': actions,
156
    }
157
158
159 1 View Code Duplication
@blueprint.get('/<uuid:article_id>/orders')
160 1
@permission_required('shop_article.view')
161 1
@templated
162 1
def view_orders(article_id):
163
    """List the orders for this article, and the corresponding quantities."""
164
    article = _get_article_or_404(article_id)
165
166
    shop = shop_service.get_shop(article.shop_id)
167
    brand = brand_service.get_brand(shop.brand_id)
168
169
    orders = ordered_articles_service.get_orders_including_article(article.id)
170
171
    def transform(order: Order) -> tuple[Order, int]:
172
        quantity = sum(
173
            line_item.quantity
174
            for line_item in order.line_items
175
            if line_item.article_id == article.id
176
        )
177
178
        return order, quantity
179
180
    orders_with_quantities = list(map(transform, orders))
181
182
    quantity_total = sum(quantity for _, quantity in orders_with_quantities)
183
184
    return {
185
        'article': article,
186
        'shop': shop,
187
        'brand': brand,
188
        'quantity_total': quantity_total,
189
        'orders_with_quantities': orders_with_quantities,
190
        'now': datetime.utcnow(),
191
    }
192
193
194 1 View Code Duplication
@blueprint.get('/<uuid:article_id>/purchases')
195 1
@permission_required('shop_article.view')
196 1
@templated
197 1
def view_purchases(article_id):
198
    """List the purchases for this article, and the corresponding quantities."""
199
    article = _get_article_or_404(article_id)
200
201
    shop = shop_service.get_shop(article.shop_id)
202
    brand = brand_service.get_brand(shop.brand_id)
203
204
    orders = ordered_articles_service.get_orders_including_article(
205
        article.id, only_payment_state=PaymentState.paid
206
    )
207
208
    def transform(order: Order) -> tuple[Order, int]:
209
        quantity = sum(
210
            line_item.quantity
211
            for line_item in order.line_items
212
            if line_item.article_id == article.id
213
        )
214
215
        return order, quantity
216
217
    orders_with_quantities = list(map(transform, orders))
218
219
    quantity_total = sum(quantity for _, quantity in orders_with_quantities)
220
221
    return {
222
        'article': article,
223
        'shop': shop,
224
        'brand': brand,
225
        'quantity_total': quantity_total,
226
        'orders_with_quantities': orders_with_quantities,
227
        'now': datetime.utcnow(),
228
    }
229
230
231
# -------------------------------------------------------------------- #
232
# create
233
234
235 1 View Code Duplication
@blueprint.get('/for_shop/<shop_id>/create/<type>')
236 1
@permission_required('shop_article.create')
237 1
@templated
238 1
def create_form(shop_id, type, erroneous_form=None):
239
    """Show form to create an article."""
240
    shop = _get_shop_or_404(shop_id)
241
    type_ = _get_article_type_or_400(type)
242
243
    brand = brand_service.get_brand(shop.brand_id)
244
245
    article_number_sequences = _get_active_article_number_sequences_for_shop(
246
        shop.id
247
    )
248
    article_number_sequence_available = bool(article_number_sequences)
249
250
    form = (
251
        erroneous_form
252
        if erroneous_form
253
        else ArticleCreateForm(
254
            price_amount=Decimal('0.0'), tax_rate=Decimal('19.0')
255
        )
256
    )
257
    form.set_article_number_sequence_choices(article_number_sequences)
258
259
    return {
260
        'shop': shop,
261
        'brand': brand,
262
        'article_type_name': type_.name,
263
        'article_type_label': get_article_type_label(type_),
264
        'article_number_sequence_available': article_number_sequence_available,
265
        'form': form,
266
    }
267
268
269 1 View Code Duplication
@blueprint.get('/for_shop/<shop_id>/create/ticket')
270 1
@permission_required('shop_article.create')
271 1
@templated
272 1
def create_ticket_form(shop_id, erroneous_form=None):
273
    """Show form to create a ticket article."""
274
    shop = _get_shop_or_404(shop_id)
275
    type_ = ArticleType.ticket
276
277
    brand = brand_service.get_brand(shop.brand_id)
278
279
    article_number_sequences = _get_active_article_number_sequences_for_shop(
280
        shop.id
281
    )
282
    article_number_sequence_available = bool(article_number_sequences)
283
284
    form = (
285
        erroneous_form
286
        if erroneous_form
287
        else TicketArticleCreateForm(
288
            price_amount=Decimal('0.0'), tax_rate=Decimal('19.0')
289
        )
290
    )
291
    form.set_article_number_sequence_choices(article_number_sequences)
292
    form.set_ticket_category_choices(brand.id)
293
294
    return {
295
        'shop': shop,
296
        'brand': brand,
297
        'article_type_name': type_.name,
298
        'article_type_label': get_article_type_label(type_),
299
        'article_number_sequence_available': article_number_sequence_available,
300
        'form': form,
301
    }
302
303
304 1 View Code Duplication
@blueprint.get('/for_shop/<shop_id>/create/ticket_bundle')
305 1
@permission_required('shop_article.create')
306 1
@templated
307 1
def create_ticket_bundle_form(shop_id, erroneous_form=None):
308
    """Show form to create a ticket bundle article."""
309
    shop = _get_shop_or_404(shop_id)
310
    type_ = ArticleType.ticket_bundle
311
312
    brand = brand_service.get_brand(shop.brand_id)
313
314
    article_number_sequences = _get_active_article_number_sequences_for_shop(
315
        shop.id
316
    )
317
    article_number_sequence_available = bool(article_number_sequences)
318
319
    form = (
320
        erroneous_form
321
        if erroneous_form
322
        else TicketBundleArticleCreateForm(
323
            price_amount=Decimal('0.0'), tax_rate=Decimal('19.0')
324
        )
325
    )
326
    form.set_article_number_sequence_choices(article_number_sequences)
327
    form.set_ticket_category_choices(brand.id)
328
329
    return {
330
        'shop': shop,
331
        'brand': brand,
332
        'article_type_name': type_.name,
333
        'article_type_label': get_article_type_label(type_),
334
        'article_number_sequence_available': article_number_sequence_available,
335
        'form': form,
336
    }
337
338
339 1
@blueprint.post('/for_shop/<shop_id>/<type>')
340 1
@permission_required('shop_article.create')
341 1
def create(shop_id, type):
342
    """Create an article."""
343
    shop = _get_shop_or_404(shop_id)
344
    type_ = _get_article_type_or_400(type)
345
346
    if type_ == ArticleType.ticket:
347
        form = TicketArticleCreateForm(request.form)
348
    elif type_ == ArticleType.ticket_bundle:
349
        form = TicketBundleArticleCreateForm(request.form)
350
    else:
351
        form = ArticleCreateForm(request.form)
352
353
    article_number_sequences = _get_active_article_number_sequences_for_shop(
354
        shop.id
355
    )
356
    if not article_number_sequences:
357
        flash_error(
358
            gettext('No article number sequences are defined for this shop.')
359
        )
360
        return create_form(shop_id, type_, form)
361
362
    form.set_article_number_sequence_choices(article_number_sequences)
363
    if type_ in (ArticleType.ticket, ArticleType.ticket_bundle):
364
        form.set_ticket_category_choices(shop.brand_id)
365
366
    if not form.validate():
367
        return create_form(shop_id, type_, form)
368
369
    article_number_sequence_id = form.article_number_sequence_id.data
370
    if not article_number_sequence_id:
371
        flash_error(gettext('No valid article number sequence was specified.'))
372
        return create_form(shop_id, type_, form)
373
374
    article_number_sequence = (
375
        article_sequence_service.get_article_number_sequence(
376
            article_number_sequence_id
377
        )
378
    )
379
    if article_number_sequence.shop_id != shop.id:
380
        flash_error(gettext('No valid article number sequence was specified.'))
381
        return create_form(shop_id, type_, form)
382
383
    article_number_generation_result = (
384
        article_sequence_service.generate_article_number(
385
            article_number_sequence.id
386
        )
387
    )
388
    if article_number_generation_result.is_err():
389
        abort(500, article_number_generation_result.unwrap_err())
390
391
    item_number = article_number_generation_result.unwrap()
392
393
    description = form.description.data.strip()
394
    price = Money(form.price_amount.data, shop.currency)
395
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
396
    available_from_utc = _assemble_datetime_utc(
397
        form.available_from_date.data, form.available_from_time.data
398
    )
399
    available_until_utc = _assemble_datetime_utc(
400
        form.available_until_date.data, form.available_until_time.data
401
    )
402
    total_quantity = form.total_quantity.data
403
    max_quantity_per_order = form.max_quantity_per_order.data
404
    not_directly_orderable = form.not_directly_orderable.data
405
    separate_order_required = form.separate_order_required.data
406
407
    if type_ == ArticleType.ticket:
408
        article = article_service.create_ticket_article(
409
            shop.id,
410
            item_number,
411
            description,
412
            price,
413
            tax_rate,
414
            total_quantity,
415
            max_quantity_per_order,
416
            form.ticket_category_id.data,
417
            available_from=available_from_utc,
418
            available_until=available_until_utc,
419
            not_directly_orderable=not_directly_orderable,
420
            separate_order_required=separate_order_required,
421
        )
422
    elif type_ == ArticleType.ticket_bundle:
423
        article = article_service.create_ticket_bundle_article(
424
            shop.id,
425
            item_number,
426
            description,
427
            price,
428
            tax_rate,
429
            total_quantity,
430
            max_quantity_per_order,
431
            form.ticket_category_id.data,
432
            form.ticket_quantity.data,
433
            available_from=available_from_utc,
434
            available_until=available_until_utc,
435
            not_directly_orderable=not_directly_orderable,
436
            separate_order_required=separate_order_required,
437
        )
438
    else:
439
        processing_required = type_ == ArticleType.physical
440
441
        article = article_service.create_article(
442
            shop.id,
443
            item_number,
444
            type_,
445
            description,
446
            price,
447
            tax_rate,
448
            total_quantity,
449
            max_quantity_per_order,
450
            processing_required,
451
            available_from=available_from_utc,
452
            available_until=available_until_utc,
453
            not_directly_orderable=not_directly_orderable,
454
            separate_order_required=separate_order_required,
455
        )
456
457
    flash_success(
458
        gettext(
459
            'Article "%(item_number)s" has been created.',
460
            item_number=article.item_number,
461
        )
462
    )
463
    return redirect_to('.view', article_id=article.id)
464
465
466
# -------------------------------------------------------------------- #
467
# update
468
469
470 1
@blueprint.get('/<uuid:article_id>/update')
471 1
@permission_required('shop_article.update')
472 1
@templated
473 1
def update_form(article_id, erroneous_form=None):
474
    """Show form to update an article."""
475
    article = _get_article_or_404(article_id)
476
477
    shop = shop_service.get_shop(article.shop_id)
478
479
    brand = brand_service.get_brand(shop.brand_id)
480
481
    data = dataclasses.asdict(article)
482
    data['price_amount'] = article.price.amount
483
    if article.available_from:
484
        available_from_local = to_user_timezone(article.available_from)
485
        data['available_from_date'] = available_from_local.date()
486
        data['available_from_time'] = available_from_local.time()
487
    if article.available_until:
488
        available_until_local = to_user_timezone(article.available_until)
489
        data['available_until_date'] = available_until_local.date()
490
        data['available_until_time'] = available_until_local.time()
491
492
    form = erroneous_form if erroneous_form else ArticleUpdateForm(data=data)
493
    form.tax_rate.data = article.tax_rate * TAX_RATE_DISPLAY_FACTOR
494
495
    return {
496
        'article': article,
497
        'shop': shop,
498
        'brand': brand,
499
        'form': form,
500
    }
501
502
503 1
@blueprint.post('/<uuid:article_id>')
504 1
@permission_required('shop_article.update')
505 1
def update(article_id):
506
    """Update an article."""
507
    article = _get_article_or_404(article_id)
508
509
    shop = shop_service.get_shop(article.shop_id)
510
511
    form = ArticleUpdateForm(request.form)
512
    if not form.validate():
513
        return update_form(article_id, form)
514
515
    description = form.description.data.strip()
516
    price = Money(form.price_amount.data, shop.currency)
517
    tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR
518
    available_from_utc = _assemble_datetime_utc(
519
        form.available_from_date.data, form.available_from_time.data
520
    )
521
    available_until_utc = _assemble_datetime_utc(
522
        form.available_until_date.data, form.available_until_time.data
523
    )
524
    total_quantity = form.total_quantity.data
525
    max_quantity_per_order = form.max_quantity_per_order.data
526
    not_directly_orderable = form.not_directly_orderable.data
527
    separate_order_required = form.separate_order_required.data
528
529
    article = article_service.update_article(
530
        article.id,
531
        description,
532
        price,
533
        tax_rate,
534
        available_from_utc,
535
        available_until_utc,
536
        total_quantity,
537
        max_quantity_per_order,
538
        not_directly_orderable,
539
        separate_order_required,
540
    )
541
542
    flash_success(
543
        gettext(
544
            'Article "%(description)s" has been updated.',
545
            description=article.description,
546
        )
547
    )
548
    return redirect_to('.view', article_id=article.id)
549
550
551
# -------------------------------------------------------------------- #
552
# article attachments
553
554
555 1
@blueprint.get('/<uuid:article_id>/attachments/create')
556 1
@permission_required('shop_article.update')
557 1
@templated
558 1
def attachment_create_form(article_id, erroneous_form=None):
559
    """Show form to attach an article to another article."""
560
    article = _get_article_or_404(article_id)
561
562
    shop = shop_service.get_shop(article.shop_id)
563
564
    brand = brand_service.get_brand(shop.brand_id)
565
566
    attachable_articles = article_service.get_attachable_articles(article.id)
567
568
    form = (
569
        erroneous_form
570
        if erroneous_form
571
        else ArticleAttachmentCreateForm(quantity=0)
572
    )
573
    form.set_article_to_attach_choices(attachable_articles)
574
575
    return {
576
        'article': article,
577
        'shop': shop,
578
        'brand': brand,
579
        'form': form,
580
    }
581
582
583 1
@blueprint.post('/<uuid:article_id>/attachments')
584 1
@permission_required('shop_article.update')
585 1
def attachment_create(article_id):
586
    """Attach an article to another article."""
587
    article = _get_article_or_404(article_id)
588
589
    attachable_articles = article_service.get_attachable_articles(article.id)
590
591
    form = ArticleAttachmentCreateForm(request.form)
592
    form.set_article_to_attach_choices(attachable_articles)
593
594
    if not form.validate():
595
        return attachment_create_form(article_id, form)
596
597
    article_to_attach_id = form.article_to_attach_id.data
598
    article_to_attach = article_service.get_article(article_to_attach_id)
599
    quantity = form.quantity.data
600
601
    article_service.attach_article(article_to_attach.id, quantity, article.id)
602
603
    flash_success(
604
        gettext(
605
            'Article "%(article_to_attach_item_number)s" has been attached %(quantity)s times to article "%(article_item_number)s".',
606
            article_to_attach_item_number=article_to_attach.item_number,
607
            quantity=quantity,
608
            article_item_number=article.item_number,
609
        )
610
    )
611
    return redirect_to('.view', article_id=article.id)
612
613
614 1
@blueprint.delete('/attachments/<uuid:article_id>')
615 1
@permission_required('shop_article.update')
616 1
@respond_no_content
617 1
def attachment_remove(article_id):
618
    """Remove the attachment link from one article to another."""
619
    attached_article = article_service.find_attached_article(article_id)
620
621
    if attached_article is None:
622
        abort(404)
623
624
    article = attached_article.article
625
    attached_to_article = attached_article.attached_to_article
626
627
    article_service.unattach_article(attached_article.id)
628
629
    flash_success(
630
        gettext(
631
            'Article "%(article_item_number)s" is no longer attached to article "%(attached_to_article_item_number)s".',
632
            article_item_number=article.item_number,
633
            attached_to_article_item_number=attached_to_article.item_number,
634
        )
635
    )
636
637
638
# -------------------------------------------------------------------- #
639
# actions
640
641
642 1
@blueprint.get('/<uuid:article_id>/actions/badge_awarding/create')
643 1
@permission_required('shop_article.update')
644 1
@templated
645 1
def action_create_form_for_badge_awarding(article_id, erroneous_form=None):
646
    """Show form to register a badge awarding action for the article."""
647
    article = _get_article_or_404(article_id)
648
649
    shop = shop_service.get_shop(article.shop_id)
650
    brand = brand_service.get_brand(shop.brand_id)
651
652
    badges = user_badge_service.get_all_badges()
653
654
    form = (
655
        erroneous_form if erroneous_form else RegisterBadgeAwardingActionForm()
656
    )
657
    form.set_badge_choices(badges)
658
659
    return {
660
        'article': article,
661
        'shop': shop,
662
        'brand': brand,
663
        'form': form,
664
    }
665
666
667 1
@blueprint.post('/<uuid:article_id>/actions/badge_awarding')
668 1
@permission_required('shop_article.update')
669 1
def action_create_for_badge_awarding(article_id):
670
    """Register a badge awarding action for the article."""
671
    article = _get_article_or_404(article_id)
672
673
    badges = user_badge_service.get_all_badges()
674
675
    form = RegisterBadgeAwardingActionForm(request.form)
676
    form.set_badge_choices(badges)
677
678
    if not form.validate():
679
        return action_create_form_for_badge_awarding(article_id, form)
680
681
    badge_id = form.badge_id.data
682
    badge = user_badge_service.get_badge(badge_id)
683
684
    order_action_registry_service.register_badge_awarding(article.id, badge.id)
685
686
    flash_success(gettext('Action has been added.'))
687
688
    return redirect_to('.view', article_id=article.id)
689
690
691 1
@blueprint.get('/<uuid:article_id>/actions/tickets_creation/create')
692 1
@permission_required('shop_article.update')
693 1
@templated
694 1
def action_create_form_for_tickets_creation(article_id, erroneous_form=None):
695
    """Show form to register a tickets creation action for the article."""
696
    article = _get_article_or_404(article_id)
697
698
    shop = shop_service.get_shop(article.shop_id)
699
    brand = brand_service.get_brand(shop.brand_id)
700
701
    form = (
702
        erroneous_form
703
        if erroneous_form
704
        else RegisterTicketsCreationActionForm()
705
    )
706
    form.set_category_choices(brand.id)
707
708
    return {
709
        'article': article,
710
        'shop': shop,
711
        'brand': brand,
712
        'form': form,
713
    }
714
715
716 1 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/tickets_creation')
717 1
@permission_required('shop_article.update')
718 1
def action_create_for_tickets_creation(article_id):
719
    """Register a tickets creation action for the article."""
720
    article = _get_article_or_404(article_id)
721
722
    shop = shop_service.get_shop(article.shop_id)
723
    brand = brand_service.get_brand(shop.brand_id)
724
725
    form = RegisterTicketsCreationActionForm(request.form)
726
    form.set_category_choices(brand.id)
727
728
    if not form.validate():
729
        return action_create_form_for_tickets_creation(article_id, form)
730
731
    category_id = form.category_id.data
732
    category = ticket_category_service.get_category(category_id)
733
734
    order_action_registry_service.register_tickets_creation(
735
        article.id, category.id
736
    )
737
738
    flash_success(gettext('Action has been added.'))
739
740
    return redirect_to('.view', article_id=article.id)
741
742
743 1
@blueprint.get('/<uuid:article_id>/actions/ticket_bundles_creation/create')
744 1
@permission_required('shop_article.update')
745 1
@templated
746 1
def action_create_form_for_ticket_bundles_creation(
747
    article_id, erroneous_form=None
748
):
749
    """Show form to register a ticket bundles creation action for the article."""
750
    article = _get_article_or_404(article_id)
751
752
    shop = shop_service.get_shop(article.shop_id)
753
    brand = brand_service.get_brand(shop.brand_id)
754
755
    form = (
756
        erroneous_form
757
        if erroneous_form
758
        else RegisterTicketBundlesCreationActionForm()
759
    )
760
    form.set_category_choices(brand.id)
761
762
    return {
763
        'article': article,
764
        'shop': shop,
765
        'brand': brand,
766
        'form': form,
767
    }
768
769
770 1 View Code Duplication
@blueprint.post('/<uuid:article_id>/actions/ticket_bundles_creation')
771 1
@permission_required('shop_article.update')
772 1
def action_create_for_ticket_bundles_creation(article_id):
773
    """Register a ticket bundles creation action for the article."""
774
    article = _get_article_or_404(article_id)
775
776
    shop = shop_service.get_shop(article.shop_id)
777
    brand = brand_service.get_brand(shop.brand_id)
778
779
    form = RegisterTicketBundlesCreationActionForm(request.form)
780
    form.set_category_choices(brand.id)
781
782
    if not form.validate():
783
        return action_create_form_for_ticket_bundles_creation(article_id, form)
784
785
    category_id = form.category_id.data
786
    category = ticket_category_service.get_category(category_id)
787
788
    ticket_quantity = form.ticket_quantity.data
789
790
    order_action_registry_service.register_ticket_bundles_creation(
791
        article.id, category.id, ticket_quantity
792
    )
793
794
    flash_success(gettext('Action has been added.'))
795
796
    return redirect_to('.view', article_id=article.id)
797
798
799 1
@blueprint.delete('/actions/<uuid:action_id>')
800 1
@permission_required('shop_article.update')
801 1
@respond_no_content
802 1
def action_remove(action_id):
803
    """Remove the action from the article."""
804
    action = order_action_service.find_action(action_id)
805
806
    if action is None:
807
        abort(404)
808
809
    order_action_service.delete_action(action.id)
810
811
    flash_success(gettext('Action has been removed.'))
812
813
814
# -------------------------------------------------------------------- #
815
# article number sequences
816
817
818 1
@blueprint.get('/number_sequences/for_shop/<shop_id>/create')
819 1
@permission_required('shop_article.create')
820 1
@templated
821 1
def create_number_sequence_form(shop_id, erroneous_form=None):
822
    """Show form to create an article number sequence."""
823
    shop = _get_shop_or_404(shop_id)
824
825
    brand = brand_service.get_brand(shop.brand_id)
826
827
    form = (
828
        erroneous_form if erroneous_form else ArticleNumberSequenceCreateForm()
829
    )
830
831
    return {
832
        'shop': shop,
833
        'brand': brand,
834
        'form': form,
835
    }
836
837
838 1 View Code Duplication
@blueprint.post('/number_sequences/for_shop/<shop_id>')
839 1
@permission_required('shop_article.create')
840 1
def create_number_sequence(shop_id):
841
    """Create an article number sequence."""
842
    shop = _get_shop_or_404(shop_id)
843
844
    form = ArticleNumberSequenceCreateForm(request.form)
845
    if not form.validate():
846
        return create_number_sequence_form(shop_id, form)
847
848
    prefix = form.prefix.data.strip()
849
850
    creation_result = article_sequence_service.create_article_number_sequence(
851
        shop.id, prefix
852
    )
853
    if creation_result.is_err():
854
        flash_error(
855
            gettext(
856
                'Article number sequence could not be created. '
857
                'Is prefix "%(prefix)s" already defined?',
858
                prefix=prefix,
859
            )
860
        )
861
        return create_number_sequence_form(shop.id, form)
862
863
    flash_success(
864
        gettext(
865
            'Article number sequence with prefix "%(prefix)s" has been created.',
866
            prefix=prefix,
867
        )
868
    )
869
    return redirect_to('.index_for_shop', shop_id=shop.id)
870
871
872
# -------------------------------------------------------------------- #
873
# helpers
874
875
876 1
def _get_shop_or_404(shop_id):
877
    shop = shop_service.find_shop(shop_id)
878
879
    if shop is None:
880
        abort(404)
881
882
    return shop
883
884
885 1
def _get_article_or_404(article_id) -> Article:
886
    article = article_service.find_article(article_id)
887
888
    if article is None:
889
        abort(404)
890
891
    return article
892
893
894 1
def _get_article_type_or_400(value: str) -> ArticleType:
895
    try:
896
        return ArticleType[value]
897
    except KeyError:
898
        abort(400, 'Unknown article type')
899
900
901 1
def _get_active_article_number_sequences_for_shop(
902
    shop_id: ShopID,
903
) -> list[ArticleNumberSequence]:
904
    sequences = article_sequence_service.get_article_number_sequences_for_shop(
905
        shop_id
906
    )
907
    return [sequence for sequence in sequences if not sequence.archived]
908
909
910 1
def _assemble_datetime_utc(d: date, t: time) -> datetime | None:
911
    if not d or not t:
912
        return None
913
914
    local_dt = datetime.combine(d, t)
915
    return to_utc(local_dt)
916