eetlijst-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eetlijst/__init__.py +3 -0
- eetlijst/eetlijst.py +37 -0
- eetlijst/exceptions.py +2 -0
- eetlijst/generated/__init__.py +1153 -0
- eetlijst/generated/all_attendances.py +13 -0
- eetlijst/generated/all_events.py +13 -0
- eetlijst/generated/all_expenses.py +13 -0
- eetlijst/generated/all_groups.py +27 -0
- eetlijst/generated/all_settlements.py +13 -0
- eetlijst/generated/all_users_in_group.py +20 -0
- eetlijst/generated/app_status.py +16 -0
- eetlijst/generated/async_base_client.py +393 -0
- eetlijst/generated/automatic_events.py +18 -0
- eetlijst/generated/base_model.py +28 -0
- eetlijst/generated/client.py +1748 -0
- eetlijst/generated/create_expense.py +15 -0
- eetlijst/generated/create_group.py +15 -0
- eetlijst/generated/create_list_item.py +15 -0
- eetlijst/generated/create_many_list_items.py +20 -0
- eetlijst/generated/create_settlement.py +15 -0
- eetlijst/generated/enums.py +555 -0
- eetlijst/generated/exceptions.py +83 -0
- eetlijst/generated/fragments.py +237 -0
- eetlijst/generated/get_attendance.py +17 -0
- eetlijst/generated/get_event.py +15 -0
- eetlijst/generated/get_group.py +20 -0
- eetlijst/generated/get_user.py +13 -0
- eetlijst/generated/get_user_in_group.py +20 -0
- eetlijst/generated/group_total_expense.py +45 -0
- eetlijst/generated/input_types.py +4702 -0
- eetlijst/generated/join_group.py +17 -0
- eetlijst/generated/list_items.py +13 -0
- eetlijst/generated/remove_account.py +16 -0
- eetlijst/generated/settle_unsettled_expenses.py +21 -0
- eetlijst/generated/settlement_expenses.py +13 -0
- eetlijst/generated/update_attendance.py +17 -0
- eetlijst/generated/update_event.py +15 -0
- eetlijst/generated/update_expense.py +20 -0
- eetlijst/generated/update_expense_distribution.py +23 -0
- eetlijst/generated/update_group.py +15 -0
- eetlijst/generated/update_list_item.py +15 -0
- eetlijst/generated/update_many_attendance.py +22 -0
- eetlijst/generated/update_user.py +15 -0
- eetlijst/generated/update_user_in_group.py +17 -0
- eetlijst/generated/update_users_in_group.py +25 -0
- eetlijst/services/__init__.py +9 -0
- eetlijst/services/app/__init__.py +1 -0
- eetlijst/services/app/app.py +20 -0
- eetlijst/services/app/transformers.py +10 -0
- eetlijst/services/event_attendance/__init__.py +1 -0
- eetlijst/services/event_attendance/event_attendance.py +94 -0
- eetlijst/services/event_attendance/transformers.py +58 -0
- eetlijst/services/events/__init__.py +1 -0
- eetlijst/services/events/events.py +148 -0
- eetlijst/services/events/transformers.py +67 -0
- eetlijst/services/expenses/__init__.py +1 -0
- eetlijst/services/expenses/expenses.py +98 -0
- eetlijst/services/expenses/transformers.py +73 -0
- eetlijst/services/expenses/utils.py +31 -0
- eetlijst/services/group_list/__init__.py +1 -0
- eetlijst/services/group_list/group_list.py +95 -0
- eetlijst/services/group_list/transformers.py +38 -0
- eetlijst/services/group_users/__init__.py +1 -0
- eetlijst/services/group_users/group_users.py +130 -0
- eetlijst/services/group_users/transformers.py +84 -0
- eetlijst/services/groups/__init__.py +1 -0
- eetlijst/services/groups/groups.py +84 -0
- eetlijst/services/groups/transformers.py +140 -0
- eetlijst/services/settlements/__init__.py +1 -0
- eetlijst/services/settlements/settlements.py +225 -0
- eetlijst/services/settlements/transformers.py +95 -0
- eetlijst/services/settlements/utils.py +84 -0
- eetlijst/services/users/__init__.py +1 -0
- eetlijst/services/users/transformers.py +103 -0
- eetlijst/services/users/users.py +44 -0
- eetlijst/utils/__init__.py +0 -0
- eetlijst/utils/datetime.py +25 -0
- eetlijst_python-0.0.1.dist-info/METADATA +80 -0
- eetlijst_python-0.0.1.dist-info/RECORD +81 -0
- eetlijst_python-0.0.1.dist-info/WHEEL +5 -0
- eetlijst_python-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
3
|
+
|
|
4
|
+
from eetlijst.generated import GraphQlClient, order_by
|
|
5
|
+
from eetlijst.generated.input_types import (
|
|
6
|
+
String_comparison_exp,
|
|
7
|
+
eetschema_group_order_by,
|
|
8
|
+
eetschema_group_set_input,
|
|
9
|
+
eetschema_users_in_group_bool_exp,
|
|
10
|
+
eetschema_users_in_group_order_by,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from eetlijst.services.group_list import GroupList
|
|
14
|
+
from eetlijst.services.groups.transformers import (
|
|
15
|
+
GroupResult,
|
|
16
|
+
transform_all_groups,
|
|
17
|
+
transform_create_group,
|
|
18
|
+
transform_get_group,
|
|
19
|
+
transform_update_group,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from eetlijst.services.group_users import GroupUsers
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Groups:
|
|
28
|
+
_client: GraphQlClient
|
|
29
|
+
users: GroupUsers
|
|
30
|
+
list: GroupList
|
|
31
|
+
|
|
32
|
+
async def get(
|
|
33
|
+
self,
|
|
34
|
+
group_id: str,
|
|
35
|
+
include_users: bool = False,
|
|
36
|
+
) -> GroupResult:
|
|
37
|
+
result = await self._client.get_group(group_id=group_id)
|
|
38
|
+
return transform_get_group(result, include_users=include_users)
|
|
39
|
+
|
|
40
|
+
async def all(
|
|
41
|
+
self,
|
|
42
|
+
user_id: str,
|
|
43
|
+
include_users: bool = False,
|
|
44
|
+
include_inactive_users: bool = False,
|
|
45
|
+
where: Optional[eetschema_users_in_group_bool_exp] = None,
|
|
46
|
+
order: Optional[List[eetschema_users_in_group_order_by]] = None,
|
|
47
|
+
limit: Optional[int] = None,
|
|
48
|
+
) -> List[GroupResult]:
|
|
49
|
+
where_data = (
|
|
50
|
+
where.model_copy(update={"user_id": String_comparison_exp(_eq=user_id)})
|
|
51
|
+
if where is not None
|
|
52
|
+
else eetschema_users_in_group_bool_exp(
|
|
53
|
+
user_id=String_comparison_exp(_eq=user_id)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
order_data = order or [
|
|
58
|
+
eetschema_users_in_group_order_by(
|
|
59
|
+
group=eetschema_group_order_by(created_at=order_by.desc)
|
|
60
|
+
)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
result = await self._client.all_groups(
|
|
64
|
+
where=where_data,
|
|
65
|
+
order=order_data,
|
|
66
|
+
limit=limit,
|
|
67
|
+
include_users=include_users,
|
|
68
|
+
include_inactive_users=include_inactive_users,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return transform_all_groups(result, include_users=include_users)
|
|
72
|
+
|
|
73
|
+
async def create(self, name: str, user_id: str) -> GroupResult:
|
|
74
|
+
result = await self._client.create_group(name=name, user_id=user_id)
|
|
75
|
+
return transform_create_group(result)
|
|
76
|
+
|
|
77
|
+
async def update(
|
|
78
|
+
self, group_id: str, data: eetschema_group_set_input
|
|
79
|
+
) -> GroupResult:
|
|
80
|
+
result = await self._client.update_group(
|
|
81
|
+
group_id=group_id,
|
|
82
|
+
set_=data,
|
|
83
|
+
)
|
|
84
|
+
return transform_update_group(result)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from eetlijst.generated.all_groups import AllGroups
|
|
7
|
+
from eetlijst.generated.create_group import CreateGroup
|
|
8
|
+
from eetlijst.generated.fragments import GroupFields, UserInGroupFields
|
|
9
|
+
from eetlijst.generated.get_group import GetGroup
|
|
10
|
+
from eetlijst.generated.update_group import UpdateGroup
|
|
11
|
+
|
|
12
|
+
from eetlijst.exceptions import EetlijstException
|
|
13
|
+
|
|
14
|
+
from eetlijst.services.users.transformers import UserResult, transform_user
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GroupSummaryResult(BaseModel):
|
|
18
|
+
payed_total: float
|
|
19
|
+
user_id: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserInGroupResult(BaseModel):
|
|
23
|
+
order: Optional[int] = None
|
|
24
|
+
start_holliday: Optional[datetime] = None
|
|
25
|
+
end_holliday: Optional[datetime] = None
|
|
26
|
+
monday: Optional[bool] = None
|
|
27
|
+
tuesday: Optional[bool] = None
|
|
28
|
+
wednesday: Optional[bool] = None
|
|
29
|
+
thursday: Optional[bool] = None
|
|
30
|
+
friday: Optional[bool] = None
|
|
31
|
+
saturday: Optional[bool] = None
|
|
32
|
+
sunday: Optional[bool] = None
|
|
33
|
+
user: Optional[UserResult] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GroupResult(BaseModel):
|
|
37
|
+
id: str
|
|
38
|
+
name: str
|
|
39
|
+
default_close_time: Optional[datetime]
|
|
40
|
+
created_at: datetime
|
|
41
|
+
created_at_eetlijst: Optional[datetime]
|
|
42
|
+
statistics_start_date: Optional[datetime]
|
|
43
|
+
statistics_end_date: Optional[datetime]
|
|
44
|
+
invite_uuid: str
|
|
45
|
+
invite_open: bool
|
|
46
|
+
description: Optional[str]
|
|
47
|
+
summary: list[GroupSummaryResult]
|
|
48
|
+
users: list[UserInGroupResult] = []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def transform_group(
|
|
52
|
+
group: Optional[GroupFields],
|
|
53
|
+
users: Optional[list[UserInGroupResult]] = None,
|
|
54
|
+
) -> GroupResult:
|
|
55
|
+
if not group:
|
|
56
|
+
raise EetlijstException("Group not found")
|
|
57
|
+
|
|
58
|
+
filtered_summary = [
|
|
59
|
+
GroupSummaryResult(
|
|
60
|
+
payed_total=float(entry.payed_total),
|
|
61
|
+
user_id=entry.user_id,
|
|
62
|
+
)
|
|
63
|
+
for entry in (group.summary or [])
|
|
64
|
+
if entry and entry.payed_total is not None and entry.user_id is not None
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
group_data = group.model_dump(exclude={"summary"})
|
|
68
|
+
return GroupResult(
|
|
69
|
+
**group_data,
|
|
70
|
+
summary=filtered_summary,
|
|
71
|
+
users=users or [],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def transform_user_in_group(
|
|
76
|
+
user_in_group: Optional[UserInGroupFields],
|
|
77
|
+
) -> UserInGroupResult:
|
|
78
|
+
if not user_in_group:
|
|
79
|
+
raise EetlijstException("User in group not found")
|
|
80
|
+
|
|
81
|
+
user_data = transform_user(user_in_group.user) if user_in_group.user else None
|
|
82
|
+
data = user_in_group.model_dump(exclude={"user"})
|
|
83
|
+
|
|
84
|
+
return UserInGroupResult(
|
|
85
|
+
**data,
|
|
86
|
+
user=user_data,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def transform_create_group(result: CreateGroup) -> GroupResult:
|
|
91
|
+
if not result.group:
|
|
92
|
+
raise EetlijstException("Failed to create group")
|
|
93
|
+
|
|
94
|
+
return transform_group(result.group)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def transform_update_group(result: UpdateGroup) -> GroupResult:
|
|
98
|
+
if not result.group:
|
|
99
|
+
raise EetlijstException("Failed to update group")
|
|
100
|
+
|
|
101
|
+
return transform_group(result.group)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transform_all_groups(
|
|
105
|
+
result: AllGroups,
|
|
106
|
+
include_users: bool = False,
|
|
107
|
+
) -> list[GroupResult]:
|
|
108
|
+
transformed_groups: list[GroupResult] = []
|
|
109
|
+
for user_in_group in result.eetschema_users_in_group:
|
|
110
|
+
if not user_in_group.group:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
users = (
|
|
114
|
+
[transform_user_in_group(u) for u in user_in_group.group.users_in_groups]
|
|
115
|
+
if include_users and user_in_group.group.users_in_groups
|
|
116
|
+
else []
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
transformed_groups.append(transform_group(user_in_group.group, users=users))
|
|
120
|
+
|
|
121
|
+
return transformed_groups
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def transform_get_group(
|
|
125
|
+
result: GetGroup,
|
|
126
|
+
include_users: bool = False,
|
|
127
|
+
) -> GroupResult:
|
|
128
|
+
if not result.eetschema_group_by_pk:
|
|
129
|
+
raise EetlijstException("Failed to get group")
|
|
130
|
+
|
|
131
|
+
users = (
|
|
132
|
+
[
|
|
133
|
+
transform_user_in_group(u)
|
|
134
|
+
for u in result.eetschema_group_by_pk.users_in_groups
|
|
135
|
+
]
|
|
136
|
+
if include_users and result.eetschema_group_by_pk.users_in_groups
|
|
137
|
+
else []
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return transform_group(result.eetschema_group_by_pk, users=users)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .settlements import Settlements
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import List, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from eetlijst.generated import GraphQlClient, order_by
|
|
7
|
+
from eetlijst.generated.fragments import ExpenseFields
|
|
8
|
+
from eetlijst.generated.input_types import (
|
|
9
|
+
Boolean_comparison_exp,
|
|
10
|
+
eetschema_expense_bool_exp,
|
|
11
|
+
eetschema_expense_distribution_insert_input,
|
|
12
|
+
eetschema_expense_order_by,
|
|
13
|
+
eetschema_expense_set_input,
|
|
14
|
+
eetschema_settlements_bool_exp,
|
|
15
|
+
eetschema_settlements_order_by,
|
|
16
|
+
uuid_comparison_exp,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from eetlijst.services.expenses.transformers import (
|
|
20
|
+
transform_create_expense,
|
|
21
|
+
transform_update_expense,
|
|
22
|
+
)
|
|
23
|
+
from eetlijst.services.expenses.utils import calculate_balances_from_expenses
|
|
24
|
+
from eetlijst.services.settlements.transformers import (
|
|
25
|
+
SettlementResult,
|
|
26
|
+
transform_create_settlement,
|
|
27
|
+
transform_settle_unsettled_expenses,
|
|
28
|
+
transform_settlement,
|
|
29
|
+
transform_settlement_expenses,
|
|
30
|
+
)
|
|
31
|
+
from eetlijst.services.settlements.utils import (
|
|
32
|
+
CalculatedAdjustmentExpense,
|
|
33
|
+
calculate_adjustment_expenses,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SettleResult(TypedDict):
|
|
38
|
+
id: str
|
|
39
|
+
expenses: list[ExpenseFields]
|
|
40
|
+
adjustments: list[ExpenseFields]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Settlements:
|
|
45
|
+
_client: GraphQlClient
|
|
46
|
+
|
|
47
|
+
async def get(self, settlement_id: str) -> Optional[SettlementResult]:
|
|
48
|
+
result = await self._client.all_settlements(
|
|
49
|
+
where=eetschema_settlements_bool_exp(
|
|
50
|
+
id=uuid_comparison_exp(_eq=settlement_id)
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return transform_settlement(result.eetschema_settlements[0])
|
|
55
|
+
|
|
56
|
+
async def all(
|
|
57
|
+
self,
|
|
58
|
+
group_id: str,
|
|
59
|
+
where: Optional[eetschema_settlements_bool_exp] = None,
|
|
60
|
+
order: Optional[list[eetschema_settlements_order_by]] = None,
|
|
61
|
+
limit: Optional[int] = None,
|
|
62
|
+
) -> list[SettlementResult]:
|
|
63
|
+
group_filter = uuid_comparison_exp(_eq=group_id)
|
|
64
|
+
where_data = (
|
|
65
|
+
where.model_copy(update={"group_id": group_filter})
|
|
66
|
+
if where is not None
|
|
67
|
+
else eetschema_settlements_bool_exp(group_id=group_filter)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
order_data = order or [eetschema_settlements_order_by(created_at=order_by.desc)]
|
|
71
|
+
|
|
72
|
+
result = await self._client.all_settlements(
|
|
73
|
+
where=where_data,
|
|
74
|
+
order=order_data,
|
|
75
|
+
limit=limit,
|
|
76
|
+
)
|
|
77
|
+
return [transform_settlement(s) for s in result.eetschema_settlements]
|
|
78
|
+
|
|
79
|
+
async def create(self, group_id: str) -> SettlementResult:
|
|
80
|
+
result = await self._client.create_settlement(group_id=group_id)
|
|
81
|
+
return transform_create_settlement(result)
|
|
82
|
+
|
|
83
|
+
async def settle(
|
|
84
|
+
self,
|
|
85
|
+
group_id: str,
|
|
86
|
+
settlement_id: Optional[str] = None,
|
|
87
|
+
do_not_create_adjustment_expenses: bool = False,
|
|
88
|
+
select: Optional[eetschema_expense_bool_exp] = None,
|
|
89
|
+
) -> SettleResult:
|
|
90
|
+
if settlement_id is None:
|
|
91
|
+
settlement = await self.create(group_id=group_id)
|
|
92
|
+
settlement_id = settlement.id
|
|
93
|
+
|
|
94
|
+
group_filter = uuid_comparison_exp(_eq=group_id)
|
|
95
|
+
settled_filter = uuid_comparison_exp(_is_null=True)
|
|
96
|
+
|
|
97
|
+
if select is not None:
|
|
98
|
+
where_data = select.model_copy(
|
|
99
|
+
update={
|
|
100
|
+
"group_id": group_filter,
|
|
101
|
+
"settled_id": settled_filter,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
where_data = eetschema_expense_bool_exp(
|
|
106
|
+
group_id=group_filter,
|
|
107
|
+
settled_id=settled_filter,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
result = await self._client.settle_unsettled_expenses(
|
|
111
|
+
settlement_id=settlement_id,
|
|
112
|
+
where=where_data,
|
|
113
|
+
)
|
|
114
|
+
settled = transform_settle_unsettled_expenses(result)
|
|
115
|
+
|
|
116
|
+
inserted_adjustments: List[ExpenseFields] = []
|
|
117
|
+
if not do_not_create_adjustment_expenses:
|
|
118
|
+
balances = calculate_balances_from_expenses(settled.expenses)
|
|
119
|
+
adjustments = calculate_adjustment_expenses(balances)
|
|
120
|
+
|
|
121
|
+
now = datetime.now(timezone.utc)
|
|
122
|
+
description = f"Vereffening {now.strftime('%d-%m-%Y')}"
|
|
123
|
+
|
|
124
|
+
async def create_and_update_adjustment(
|
|
125
|
+
adjustment: CalculatedAdjustmentExpense,
|
|
126
|
+
) -> ExpenseFields:
|
|
127
|
+
distribution_data = [
|
|
128
|
+
eetschema_expense_distribution_insert_input(
|
|
129
|
+
user_id=d["user"].id,
|
|
130
|
+
payed_amount=d["payed_amount"],
|
|
131
|
+
count=d["count"],
|
|
132
|
+
)
|
|
133
|
+
for d in adjustment["expense_distributions"]
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
created_raw = await self._client.create_expense(
|
|
137
|
+
group_id=group_id,
|
|
138
|
+
payed_by=adjustment["payed_by"].id,
|
|
139
|
+
payed_amount=adjustment["payed_amount"],
|
|
140
|
+
payed_at=now,
|
|
141
|
+
description=description,
|
|
142
|
+
settlement_expense_id=settlement_id,
|
|
143
|
+
data=distribution_data,
|
|
144
|
+
)
|
|
145
|
+
created_expense = transform_create_expense(created_raw)
|
|
146
|
+
|
|
147
|
+
updated_raw = await self._client.update_expense(
|
|
148
|
+
expense_id=created_expense.id,
|
|
149
|
+
set_=eetschema_expense_set_input(settled_id=settlement_id),
|
|
150
|
+
)
|
|
151
|
+
return transform_update_expense(updated_raw)
|
|
152
|
+
|
|
153
|
+
inserted_adjustments = list(
|
|
154
|
+
await asyncio.gather(
|
|
155
|
+
*(create_and_update_adjustment(adj) for adj in adjustments)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"id": settlement_id,
|
|
161
|
+
"expenses": settled.expenses,
|
|
162
|
+
"adjustments": inserted_adjustments,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async def expenses(
|
|
166
|
+
self,
|
|
167
|
+
settlement_id: str,
|
|
168
|
+
where: Optional[eetschema_expense_bool_exp] = None,
|
|
169
|
+
order: Optional[list[eetschema_expense_order_by]] = None,
|
|
170
|
+
) -> list[ExpenseFields]:
|
|
171
|
+
return await self._expenses(
|
|
172
|
+
settlement_id=settlement_id,
|
|
173
|
+
settlement_expense_id_not_null=False,
|
|
174
|
+
where=where,
|
|
175
|
+
order=order,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def adjustments(
|
|
179
|
+
self,
|
|
180
|
+
settlement_id: str,
|
|
181
|
+
where: Optional[eetschema_expense_bool_exp] = None,
|
|
182
|
+
order: Optional[list[eetschema_expense_order_by]] = None,
|
|
183
|
+
) -> list[ExpenseFields]:
|
|
184
|
+
return await self._expenses(
|
|
185
|
+
settlement_id=settlement_id,
|
|
186
|
+
settlement_expense_id_not_null=True,
|
|
187
|
+
where=where,
|
|
188
|
+
order=order,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def _expenses(
|
|
192
|
+
self,
|
|
193
|
+
settlement_id: str,
|
|
194
|
+
settlement_expense_id_not_null: bool,
|
|
195
|
+
where: Optional[eetschema_expense_bool_exp] = None,
|
|
196
|
+
order: Optional[list[eetschema_expense_order_by]] = None,
|
|
197
|
+
) -> list[ExpenseFields]:
|
|
198
|
+
deleted_filter = Boolean_comparison_exp(_eq=False)
|
|
199
|
+
settled_filter = uuid_comparison_exp(_eq=settlement_id)
|
|
200
|
+
settlement_expense_filter = uuid_comparison_exp(
|
|
201
|
+
_is_null=not settlement_expense_id_not_null
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if where is not None:
|
|
205
|
+
where_data = where.model_copy(
|
|
206
|
+
update={
|
|
207
|
+
"deleted": deleted_filter,
|
|
208
|
+
"settled_id": settled_filter,
|
|
209
|
+
"settlement_expense_id": settlement_expense_filter,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
where_data = eetschema_expense_bool_exp(
|
|
214
|
+
deleted=deleted_filter,
|
|
215
|
+
settled_id=settled_filter,
|
|
216
|
+
settlement_expense_id=settlement_expense_filter,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
order_data = order or [eetschema_expense_order_by(created_at=order_by.desc)]
|
|
220
|
+
|
|
221
|
+
result = await self._client.settlement_expenses(
|
|
222
|
+
where=where_data,
|
|
223
|
+
order=order_data,
|
|
224
|
+
)
|
|
225
|
+
return transform_settlement_expenses(result)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
from eetlijst.generated.create_settlement import CreateSettlement
|
|
7
|
+
from eetlijst.generated.fragments import (
|
|
8
|
+
ExpenseFields,
|
|
9
|
+
SettlementFields,
|
|
10
|
+
SettlementFieldsCreatedBy,
|
|
11
|
+
)
|
|
12
|
+
from eetlijst.generated.settle_unsettled_expenses import SettleUnsettledExpenses
|
|
13
|
+
from eetlijst.generated.settlement_expenses import SettlementExpenses
|
|
14
|
+
|
|
15
|
+
from eetlijst.exceptions import EetlijstException
|
|
16
|
+
|
|
17
|
+
from eetlijst.services.expenses.transformers import transform_expense
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettlementResult(BaseModel):
|
|
21
|
+
model_config = ConfigDict(frozen=True)
|
|
22
|
+
|
|
23
|
+
id: str
|
|
24
|
+
group_id: str
|
|
25
|
+
created_at: datetime
|
|
26
|
+
updated_at: datetime
|
|
27
|
+
created_by: SettlementFieldsCreatedBy
|
|
28
|
+
expenses_total: Optional[int] = None
|
|
29
|
+
adjustments_total: Optional[int] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SettleUnsettledExpensesResult(BaseModel):
|
|
33
|
+
model_config = ConfigDict(frozen=True)
|
|
34
|
+
|
|
35
|
+
number_expenses: int
|
|
36
|
+
expenses: list[ExpenseFields]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def transform_settlement(settlement: Optional[SettlementFields]) -> SettlementResult:
|
|
40
|
+
if not settlement:
|
|
41
|
+
raise EetlijstException("Settlement not found")
|
|
42
|
+
|
|
43
|
+
expenses_total: Optional[int] = None
|
|
44
|
+
if (
|
|
45
|
+
settlement.expenses_total
|
|
46
|
+
and settlement.expenses_total.aggregate
|
|
47
|
+
and settlement.expenses_total.aggregate.sum
|
|
48
|
+
):
|
|
49
|
+
expenses_total = settlement.expenses_total.aggregate.sum.payed_amount
|
|
50
|
+
|
|
51
|
+
adjustments_total: Optional[int] = None
|
|
52
|
+
if (
|
|
53
|
+
settlement.adjustments_total
|
|
54
|
+
and settlement.adjustments_total.aggregate
|
|
55
|
+
and settlement.adjustments_total.aggregate.sum
|
|
56
|
+
):
|
|
57
|
+
adjustments_total = settlement.adjustments_total.aggregate.sum.payed_amount
|
|
58
|
+
|
|
59
|
+
data = {
|
|
60
|
+
**settlement.model_dump(),
|
|
61
|
+
"expenses_total": expenses_total,
|
|
62
|
+
"adjustments_total": adjustments_total,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return SettlementResult(**data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def transform_create_settlement(result: CreateSettlement) -> SettlementResult:
|
|
69
|
+
if not result.settlement:
|
|
70
|
+
raise EetlijstException("Failed to create settlement")
|
|
71
|
+
|
|
72
|
+
return transform_settlement(result.settlement)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def transform_settle_unsettled_expenses(
|
|
76
|
+
result: SettleUnsettledExpenses,
|
|
77
|
+
) -> SettleUnsettledExpensesResult:
|
|
78
|
+
if not result.update_eetschema_expense:
|
|
79
|
+
raise EetlijstException("Failed to settle expenses")
|
|
80
|
+
|
|
81
|
+
expenses = [
|
|
82
|
+
transform_expense(expense)
|
|
83
|
+
for expense in result.update_eetschema_expense.expenses
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
data = {
|
|
87
|
+
**result.update_eetschema_expense.model_dump(),
|
|
88
|
+
"expenses": expenses,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return SettleUnsettledExpensesResult(**data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def transform_settlement_expenses(result: SettlementExpenses):
|
|
95
|
+
return [transform_expense(expense) for expense in result.eetschema_expense]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
from eetlijst.generated.fragments import ExpenseFieldsExpenseDistributionsUser
|
|
4
|
+
|
|
5
|
+
from eetlijst.services.expenses.utils import UserBalance
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AdjustmentExpenseDistribution(TypedDict):
|
|
9
|
+
user: ExpenseFieldsExpenseDistributionsUser
|
|
10
|
+
count: int
|
|
11
|
+
payed_amount: int
|
|
12
|
+
user_id: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CalculatedAdjustmentExpense(TypedDict):
|
|
16
|
+
payed_by: ExpenseFieldsExpenseDistributionsUser
|
|
17
|
+
payed_amount: int
|
|
18
|
+
expense_distributions: list[AdjustmentExpenseDistribution]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def calculate_adjustment_expenses(
|
|
22
|
+
balances: list[UserBalance],
|
|
23
|
+
) -> list[CalculatedAdjustmentExpense]:
|
|
24
|
+
creditors: list[UserBalance] = sorted(
|
|
25
|
+
[
|
|
26
|
+
UserBalance(user=b["user"], balance=b["balance"])
|
|
27
|
+
for b in balances
|
|
28
|
+
if b["balance"] > 0
|
|
29
|
+
],
|
|
30
|
+
key=lambda b: b["balance"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
debtors: list[UserBalance] = sorted(
|
|
34
|
+
[
|
|
35
|
+
UserBalance(user=b["user"], balance=b["balance"])
|
|
36
|
+
for b in balances
|
|
37
|
+
if b["balance"] < 0
|
|
38
|
+
],
|
|
39
|
+
key=lambda b: abs(b["balance"]),
|
|
40
|
+
reverse=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
results: list[CalculatedAdjustmentExpense] = []
|
|
44
|
+
|
|
45
|
+
for creditor in creditors:
|
|
46
|
+
distributions: list[AdjustmentExpenseDistribution] = []
|
|
47
|
+
|
|
48
|
+
for debtor in debtors:
|
|
49
|
+
amount = min(creditor["balance"], abs(debtor["balance"]))
|
|
50
|
+
if amount <= 0:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
distributions.append(
|
|
54
|
+
{
|
|
55
|
+
"user": debtor["user"],
|
|
56
|
+
"payed_amount": amount,
|
|
57
|
+
"user_id": debtor["user"].id,
|
|
58
|
+
"count": 0,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
creditor["balance"] -= amount
|
|
63
|
+
debtor["balance"] += amount
|
|
64
|
+
|
|
65
|
+
total = -sum(entry["payed_amount"] for entry in distributions)
|
|
66
|
+
|
|
67
|
+
distributions.append(
|
|
68
|
+
{
|
|
69
|
+
"user": creditor["user"],
|
|
70
|
+
"payed_amount": total,
|
|
71
|
+
"user_id": creditor["user"].id,
|
|
72
|
+
"count": 0,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
results.append(
|
|
77
|
+
{
|
|
78
|
+
"payed_by": creditor["user"],
|
|
79
|
+
"payed_amount": total,
|
|
80
|
+
"expense_distributions": distributions,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .users import Users
|