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