Passed
Push — main ( 57d628...c66ff9 )
by Jochen
04:13
created

_find_line_items()   A

Complexity

Conditions 2

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.6796

Importance

Changes 0
Metric Value
cc 2
eloc 21
nop 2
dl 0
loc 28
ccs 1
cts 8
cp 0.125
crap 4.6796
rs 9.376
c 0
b 0
f 0
1
"""
2
byceps.services.shop.shipping.service
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 collections import Counter, defaultdict
11 1
from dataclasses import dataclass
12 1
from typing import Iterator, Sequence
13
14 1
from ..article.dbmodels.article import Article as DbArticle
15
16 1
from ....database import db
17
18 1
from ..article.transfer.models import ArticleNumber
19 1
from ..order.dbmodels.line_item import LineItem as DbLineItem
20 1
from ..order.dbmodels.order import Order as DbOrder
21 1
from ..order.transfer.models import PaymentState
22 1
from ..shop.transfer.models import ShopID
23
24 1
from .transfer.models import ArticleToShip
25
26
27 1
def get_articles_to_ship(shop_id: ShopID) -> Sequence[ArticleToShip]:
28
    """Return articles that need, or likely need, to be shipped soon."""
29
    relevant_order_payment_states = {
30
        PaymentState.open,
31
        PaymentState.paid,
32
    }
33
34
    line_item_quantities = list(
35
        _find_line_items(shop_id, relevant_order_payment_states)
36
    )
37
38
    article_numbers = {item.article_number for item in line_item_quantities}
39
    article_descriptions = _get_article_descriptions(article_numbers)
40
41
    articles_to_ship = list(
42
        _aggregate_ordered_article_quantites(
43
            line_item_quantities, article_descriptions
44
        )
45
    )
46
47
    articles_to_ship.sort(key=lambda a: a.article_number)
48
49
    return articles_to_ship
50
51
52 1
@dataclass(frozen=True)
53
class LineItemQuantity:
54 1
    article_number: ArticleNumber
55 1
    payment_state: PaymentState
56 1
    quantity: int
57
58
59 1
def _find_line_items(
60
    shop_id: ShopID, payment_states: set[PaymentState]
61
) -> Iterator[LineItemQuantity]:
62
    """Return article quantities for the given payment states."""
63
    payment_state_names = {ps.name for ps in payment_states}
64
65
    common_query = DbLineItem.query \
66
        .join(DbOrder) \
67
        .filter(DbOrder.shop_id == shop_id) \
68
        .options(db.joinedload(DbLineItem.order)) \
69
        .filter(DbLineItem.shipping_required == True)
70
71
    definitive_line_items = common_query \
72
        .filter(DbOrder._payment_state == PaymentState.paid.name) \
73
        .filter(DbOrder.shipped_at == None) \
74
        .all()
75
76
    potential_line_items = common_query \
77
        .filter(DbOrder._payment_state == PaymentState.open.name) \
78
        .all()
79
80
    line_items = definitive_line_items + potential_line_items
81
82
    for item in line_items:
83
        yield LineItemQuantity(
84
            item.article_number,
85
            item.order.payment_state,
86
            item.quantity
87
        )
88
89
90 1
def _aggregate_ordered_article_quantites(
91
    line_item_quantities: Sequence[LineItemQuantity],
92
    article_descriptions: dict[ArticleNumber, str],
93
) -> Iterator[ArticleToShip]:
94
    """Aggregate article quantities per payment state."""
95
    d: defaultdict[ArticleNumber, Counter] = defaultdict(Counter)
96
97
    for item in line_item_quantities:
98
        d[item.article_number][item.payment_state] += item.quantity
99
100
    for article_number, counter in d.items():
101
        description = article_descriptions[article_number]
102
        quantity_paid = counter[PaymentState.paid]
103
        quantity_open = counter[PaymentState.open]
104
105
        yield ArticleToShip(
106
            article_number,
107
            description,
108
            quantity_paid,
109
            quantity_open,
110
            quantity_total=quantity_paid + quantity_open,
111
        )
112
113
114 1
def _get_article_descriptions(
115
    article_numbers: set[ArticleNumber],
116
) -> dict[ArticleNumber, str]:
117
    """Look up description texts of the specified articles."""
118
    if not article_numbers:
119
        return {}
120
121
    articles = DbArticle.query \
122
        .options(db.load_only('item_number', 'description')) \
123
        .filter(DbArticle.item_number.in_(article_numbers)) \
124
        .all()
125
126
    return {a.item_number: a.description for a in articles}
127