Passed
Push — main ( 1909da...2ebff6 )
by Jochen
01:37
created

ArticleWithQuantity.__post_init__()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2.5

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 1
dl 0
loc 5
ccs 1
cts 2
cp 0.5
crap 2.5
rs 10
c 0
b 0
f 0
1
"""
2
byceps.services.shop.article.models
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2014-2024 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from collections.abc import Iterator
10
from dataclasses import dataclass, field
11 1
from datetime import datetime
12 1
from decimal import Decimal
13 1
from enum import Enum
14 1
from typing import NewType
15 1
from uuid import UUID
16 1
17 1
from flask_babel import lazy_gettext
18
from moneyed import Money
19 1
20 1
from byceps.services.shop.shop.models import ShopID
21
22 1
23
ArticleID = NewType('ArticleID', UUID)
24
25 1
26
ArticleNumber = NewType('ArticleNumber', str)
27
28 1
29
ArticleNumberSequenceID = NewType('ArticleNumberSequenceID', UUID)
30
31 1
32
@dataclass(frozen=True)
33
class ArticleNumberSequence:
34 1
    id: ArticleNumberSequenceID
35 1
    shop_id: ShopID
36 1
    prefix: str
37 1
    value: int
38 1
    archived: bool
39 1
40 1
41
ArticleType = Enum(
42
    'ArticleType', ['physical', 'ticket', 'ticket_bundle', 'other']
43 1
)
44
45
46
_ARTICLE_TYPE_LABELS = {
47
    ArticleType.physical: lazy_gettext('physical'),
48 1
    ArticleType.ticket: lazy_gettext('Ticket'),
49
    ArticleType.ticket_bundle: lazy_gettext('Ticket bundle'),
50
    ArticleType.other: lazy_gettext('other'),
51
}
52
53
54
def get_article_type_label(article_type: ArticleType) -> str:
55
    """Return a label for the article type."""
56 1
    return _ARTICLE_TYPE_LABELS[article_type]
57
58
59
ArticleTypeParams = dict[str, str | int]
60
61 1
62
AttachedArticleID = NewType('AttachedArticleID', UUID)
63
64 1
65
@dataclass(frozen=True)
66
class Article:
67 1
    id: ArticleID
68 1
    shop_id: ShopID
69 1
    item_number: ArticleNumber
70 1
    type_: ArticleType
71 1
    type_params: ArticleTypeParams
72 1
    name: str
73 1
    price: Money
74 1
    tax_rate: Decimal
75 1
    available_from: datetime | None
76 1
    available_until: datetime | None
77 1
    total_quantity: int
78 1
    quantity: int
79 1
    max_quantity_per_order: int
80 1
    not_directly_orderable: bool
81 1
    separate_order_required: bool
82 1
    processing_required: bool
83 1
84 1
85
@dataclass(frozen=True)
86
class ArticleAttachment:
87 1
    attached_article: Article
88 1
    attached_quantity: int
89 1
90 1
91
@dataclass(frozen=True, slots=True)
92 1
class ArticleWithQuantity:
93 1
    article: Article
94
    quantity: int
95
    amount: Money = field(init=False)
96
97
    def __post_init__(self) -> None:
98 1
        if self.quantity < 1:
99
            raise ValueError('Quantity must be a positive number.')
100
101
        object.__setattr__(self, 'amount', self.article.price * self.quantity)
102 1
103 1
104
@dataclass(frozen=True)
105
class ArticleCompilationItem:
106 1
    article: Article
107
    fixed_quantity: int | None = None
108 1
109 1
    def __post_init__(self) -> None:
110
        if (self.fixed_quantity is not None) and (self.fixed_quantity < 1):
111 1
            raise ValueError(
112 1
                'Fixed quantity, if given, must be a positive number.'
113
            )
114 1
115 1
    def has_fixed_quantity(self) -> bool:
116
        return self.fixed_quantity is not None
117 1
118 1
119
class ArticleCompilation:
120
    def __init__(self, items: list[ArticleCompilationItem]) -> None:
121
        if not items:
122
            raise ValueError('Article compilation must not be empty')
123
124
        self._items = list(items)
125
126
    def __iter__(self) -> Iterator[ArticleCompilationItem]:
127
        return iter(self._items)
128
129
130
class ArticleCompilationBuilder:
131
    def __init__(self) -> None:
132
        self._items: list[ArticleCompilationItem] = []
133
134
    def append_article(
135
        self, article: Article, *, fixed_quantity: int | None = None
136
    ) -> None:
137
        item = ArticleCompilationItem(article, fixed_quantity=fixed_quantity)
138
        self._items.append(item)
139
140
    def build(self) -> ArticleCompilation:
141
        return ArticleCompilation(self._items)
142