aoiro 0.0.3__py3-none-any.whl → 0.1.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.
- aoiro/__init__.py +52 -1
- aoiro/__main__.py +1 -1
- aoiro/_ledger.py +298 -0
- aoiro/_multidimensional.py +186 -0
- aoiro/_sheets.py +123 -0
- aoiro/account.yml +68 -0
- aoiro/cli.py +97 -7
- aoiro/reader/__init__.py +12 -0
- aoiro/reader/_expenses.py +51 -0
- aoiro/reader/_io.py +177 -0
- aoiro/reader/_sales.py +170 -0
- {aoiro-0.0.3.dist-info → aoiro-0.1.1.dist-info}/LICENSE +243 -257
- aoiro-0.1.1.dist-info/METADATA +268 -0
- aoiro-0.1.1.dist-info/RECORD +17 -0
- aoiro/main.py +0 -3
- aoiro-0.0.3.dist-info/METADATA +0 -107
- aoiro-0.0.3.dist-info/RECORD +0 -10
- {aoiro-0.0.3.dist-info → aoiro-0.1.1.dist-info}/WHEEL +0 -0
- {aoiro-0.0.3.dist-info → aoiro-0.1.1.dist-info}/entry_points.txt +0 -0
aoiro/__init__.py
CHANGED
@@ -1 +1,52 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.1.1"
|
2
|
+
from ._ledger import (
|
3
|
+
GeneralLedgerLine,
|
4
|
+
GeneralLedgerLineImpl,
|
5
|
+
LedgerElement,
|
6
|
+
LedgerElementImpl,
|
7
|
+
LedgerLine,
|
8
|
+
LedgerLineImpl,
|
9
|
+
MultiLedgerLine,
|
10
|
+
MultiLedgerLineImpl,
|
11
|
+
generalledger_line_to_multiledger_line,
|
12
|
+
generalledger_to_multiledger,
|
13
|
+
multiledger_line_to_generalledger_line,
|
14
|
+
multiledger_line_to_ledger_line,
|
15
|
+
multiledger_to_generalledger,
|
16
|
+
multiledger_to_ledger,
|
17
|
+
)
|
18
|
+
from ._multidimensional import multidimensional_ledger_to_ledger
|
19
|
+
from ._sheets import get_sheets
|
20
|
+
from .reader import (
|
21
|
+
ledger_from_expenses,
|
22
|
+
ledger_from_sales,
|
23
|
+
read_all_csvs,
|
24
|
+
read_general_ledger,
|
25
|
+
read_simple_csvs,
|
26
|
+
withholding_tax,
|
27
|
+
)
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
"GeneralLedgerLine",
|
31
|
+
"GeneralLedgerLineImpl",
|
32
|
+
"LedgerElement",
|
33
|
+
"LedgerElementImpl",
|
34
|
+
"LedgerLine",
|
35
|
+
"LedgerLineImpl",
|
36
|
+
"MultiLedgerLine",
|
37
|
+
"MultiLedgerLineImpl",
|
38
|
+
"generalledger_line_to_multiledger_line",
|
39
|
+
"generalledger_to_multiledger",
|
40
|
+
"get_sheets",
|
41
|
+
"ledger_from_expenses",
|
42
|
+
"ledger_from_sales",
|
43
|
+
"multidimensional_ledger_to_ledger",
|
44
|
+
"multiledger_line_to_generalledger_line",
|
45
|
+
"multiledger_line_to_ledger_line",
|
46
|
+
"multiledger_to_generalledger",
|
47
|
+
"multiledger_to_ledger",
|
48
|
+
"read_all_csvs",
|
49
|
+
"read_general_ledger",
|
50
|
+
"read_simple_csvs",
|
51
|
+
"withholding_tax",
|
52
|
+
]
|
aoiro/__main__.py
CHANGED
aoiro/_ledger.py
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
from collections.abc import Sequence
|
2
|
+
from decimal import Decimal
|
3
|
+
from itertools import chain
|
4
|
+
from typing import Any, Callable, Literal, Protocol, TypeVar
|
5
|
+
|
6
|
+
import attrs
|
7
|
+
import pandas as pd
|
8
|
+
from account_codes_jp._common import SUNDRY
|
9
|
+
|
10
|
+
AccountSundry = Literal["諸口"]
|
11
|
+
Account = TypeVar("Account", bound=str)
|
12
|
+
Currency = TypeVar("Currency", bound=str)
|
13
|
+
|
14
|
+
|
15
|
+
class _LedgerLineBase(Protocol):
|
16
|
+
date: pd.Timestamp
|
17
|
+
"""The date when the transaction occurred."""
|
18
|
+
|
19
|
+
|
20
|
+
class LedgerLine(_LedgerLineBase, Protocol[Account, Currency]):
|
21
|
+
amount: Decimal
|
22
|
+
"""The amount. Must be non-negative."""
|
23
|
+
currency: Currency
|
24
|
+
"""The currency."""
|
25
|
+
debit_account: Account
|
26
|
+
"""The account written on the debit side."""
|
27
|
+
credit_account: Account
|
28
|
+
"""The account written on the credit side."""
|
29
|
+
|
30
|
+
|
31
|
+
class LedgerElement(Protocol[Account, Currency]):
|
32
|
+
account: Account
|
33
|
+
"""The account."""
|
34
|
+
amount: Decimal
|
35
|
+
"""The amount."""
|
36
|
+
currency: Currency
|
37
|
+
"""The currency."""
|
38
|
+
|
39
|
+
|
40
|
+
class MultiLedgerLine(_LedgerLineBase, Protocol[Account, Currency]):
|
41
|
+
debit: Sequence[LedgerElement[Account, Currency]]
|
42
|
+
"""The accounts and amounts on the debit side.
|
43
|
+
Each amount needs to be non-negative."""
|
44
|
+
credit: Sequence[LedgerElement[Account, Currency]]
|
45
|
+
"""The accounts and amounts on the credit side.
|
46
|
+
Each amount needs to be non-negative."""
|
47
|
+
|
48
|
+
|
49
|
+
class GeneralLedgerLine(_LedgerLineBase, Protocol[Account, Currency]):
|
50
|
+
values: Sequence[LedgerElement[Account, Currency]]
|
51
|
+
"""The accounts and amounts. Amounts does not need to be non-negative."""
|
52
|
+
|
53
|
+
|
54
|
+
@attrs.frozen(kw_only=True)
|
55
|
+
class LedgerLineImpl(LedgerLine[Account, Currency]):
|
56
|
+
date: pd.Timestamp
|
57
|
+
amount: Decimal = attrs.field()
|
58
|
+
currency: Currency
|
59
|
+
debit_account: Account
|
60
|
+
credit_account: Account
|
61
|
+
|
62
|
+
def __repr__(self) -> str:
|
63
|
+
return (
|
64
|
+
f"{self.date} {self.amount} {self.currency} "
|
65
|
+
f"{self.debit_account} / {self.credit_account}"
|
66
|
+
)
|
67
|
+
|
68
|
+
@amount.validator
|
69
|
+
def _validate_amount(self, attribute: Any, value: Decimal) -> None:
|
70
|
+
if value < Decimal(0):
|
71
|
+
raise ValueError("amount must be non-negative")
|
72
|
+
|
73
|
+
|
74
|
+
@attrs.frozen(kw_only=True)
|
75
|
+
class LedgerElementImpl(LedgerElement[Account, Currency]):
|
76
|
+
account: Account
|
77
|
+
amount: Decimal
|
78
|
+
currency: Currency
|
79
|
+
|
80
|
+
def __repr__(self) -> str:
|
81
|
+
return f"{self.account} {self.amount} {self.currency}"
|
82
|
+
|
83
|
+
|
84
|
+
@attrs.frozen(kw_only=True, auto_detect=True)
|
85
|
+
class MultiLedgerLineImpl(MultiLedgerLine[Account, Currency]):
|
86
|
+
date: pd.Timestamp
|
87
|
+
debit: Sequence[LedgerElement[Account, Currency]] = attrs.field()
|
88
|
+
credit: Sequence[LedgerElement[Account, Currency]] = attrs.field()
|
89
|
+
|
90
|
+
def __repr__(self) -> str:
|
91
|
+
date = pd.Series([self.date], name="date")
|
92
|
+
debit = pd.DataFrame(
|
93
|
+
self.debit, columns=["debit_account", "amount", "currency"]
|
94
|
+
)
|
95
|
+
credit = pd.DataFrame(
|
96
|
+
self.credit, columns=["credit_account", "amount", "currency"]
|
97
|
+
)
|
98
|
+
return (
|
99
|
+
pd.concat([date, debit, credit], axis=1)
|
100
|
+
.replace({pd.NaT: ""})
|
101
|
+
.fillna("")
|
102
|
+
.to_string(index=False, header=False)
|
103
|
+
)
|
104
|
+
|
105
|
+
@debit.validator
|
106
|
+
def _validate_debit(
|
107
|
+
self, attribute: Any, value: Sequence[LedgerElement[Account, Currency]]
|
108
|
+
) -> None:
|
109
|
+
if any(el.amount < Decimal(0) for el in value):
|
110
|
+
raise ValueError("amount must be non-negative")
|
111
|
+
|
112
|
+
@credit.validator
|
113
|
+
def _validate_credit(
|
114
|
+
self, attribute: Any, value: Sequence[LedgerElement[Account, Currency]]
|
115
|
+
) -> None:
|
116
|
+
if any(el.amount < Decimal(0) for el in value):
|
117
|
+
raise ValueError("amount must be non-negative")
|
118
|
+
|
119
|
+
|
120
|
+
@attrs.frozen(kw_only=True, auto_detect=True)
|
121
|
+
class GeneralLedgerLineImpl(GeneralLedgerLine[Account, Currency]):
|
122
|
+
date: pd.Timestamp
|
123
|
+
values: Sequence[LedgerElement[Account, Currency]]
|
124
|
+
|
125
|
+
|
126
|
+
def generalledger_line_to_multiledger_line(
|
127
|
+
line: GeneralLedgerLine[Account, Currency], is_debit: Callable[[Account], bool], /
|
128
|
+
) -> MultiLedgerLine[Account, Currency]:
|
129
|
+
"""
|
130
|
+
Convert a GeneralLedgerLine to a MultiLedgerLine.
|
131
|
+
|
132
|
+
Parameters
|
133
|
+
----------
|
134
|
+
line : GeneralLedgerLine[Account, Currency]
|
135
|
+
The GeneralLedgerLine to convert.
|
136
|
+
is_debit : Callable[[Account], bool]
|
137
|
+
Whether the account is a debit account.
|
138
|
+
|
139
|
+
Returns
|
140
|
+
-------
|
141
|
+
MultiLedgerLine[Account, Currency]
|
142
|
+
The converted MultiLedgerLine.
|
143
|
+
|
144
|
+
"""
|
145
|
+
debit = []
|
146
|
+
credit = []
|
147
|
+
for el in line.values:
|
148
|
+
if is_debit(el.account) == (el.amount > 0):
|
149
|
+
debit.append(
|
150
|
+
LedgerElementImpl(
|
151
|
+
account=el.account, amount=abs(el.amount), currency=el.currency
|
152
|
+
)
|
153
|
+
)
|
154
|
+
else:
|
155
|
+
credit.append(
|
156
|
+
LedgerElementImpl(
|
157
|
+
account=el.account, amount=abs(el.amount), currency=el.currency
|
158
|
+
)
|
159
|
+
)
|
160
|
+
return MultiLedgerLineImpl(date=line.date, debit=debit, credit=credit)
|
161
|
+
|
162
|
+
|
163
|
+
def multiledger_line_to_generalledger_line(
|
164
|
+
line: MultiLedgerLine[Account, Currency], /
|
165
|
+
) -> GeneralLedgerLine[Account, Currency]:
|
166
|
+
"""
|
167
|
+
Convert a MultiLedgerLine to a GeneralLedgerLine.
|
168
|
+
|
169
|
+
Parameters
|
170
|
+
----------
|
171
|
+
line : MultiLedgerLine[Account, Currency]
|
172
|
+
The MultiLedgerLine to convert.
|
173
|
+
|
174
|
+
Returns
|
175
|
+
-------
|
176
|
+
GeneralLedgerLine[Account, Currency]
|
177
|
+
The converted GeneralLedgerLine.
|
178
|
+
|
179
|
+
"""
|
180
|
+
return GeneralLedgerLineImpl(
|
181
|
+
date=line.date,
|
182
|
+
values=[*line.debit, *line.credit],
|
183
|
+
)
|
184
|
+
|
185
|
+
|
186
|
+
def multiledger_line_to_ledger_line(
|
187
|
+
line: MultiLedgerLine[Account, Currency], /
|
188
|
+
) -> Sequence[LedgerLine[Account | AccountSundry, Currency]]:
|
189
|
+
"""
|
190
|
+
Convert a MultiLedgerLine to a list of LedgerLine.
|
191
|
+
|
192
|
+
Parameters
|
193
|
+
----------
|
194
|
+
line : MultiLedgerLine[Account, Currency]
|
195
|
+
The MultiLedgerLine to convert.
|
196
|
+
|
197
|
+
Returns
|
198
|
+
-------
|
199
|
+
Sequence[LedgerLine[Account | AccountSundry, Currency]]
|
200
|
+
The converted LedgerLines.
|
201
|
+
|
202
|
+
"""
|
203
|
+
if (
|
204
|
+
len(line.debit) == len(line.credit) == 1
|
205
|
+
and line.debit[0].amount == line.credit[0].amount
|
206
|
+
and line.debit[0].currency == line.credit[0].currency
|
207
|
+
):
|
208
|
+
return [
|
209
|
+
LedgerLineImpl(
|
210
|
+
date=line.date,
|
211
|
+
amount=line.debit[0].amount,
|
212
|
+
currency=line.debit[0].currency,
|
213
|
+
debit_account=line.debit[0].account,
|
214
|
+
credit_account=line.credit[0].account,
|
215
|
+
)
|
216
|
+
]
|
217
|
+
return [
|
218
|
+
LedgerLineImpl(
|
219
|
+
date=line.date,
|
220
|
+
amount=el.amount,
|
221
|
+
currency=el.currency,
|
222
|
+
debit_account=el.account,
|
223
|
+
credit_account=SUNDRY,
|
224
|
+
)
|
225
|
+
for el in line.debit
|
226
|
+
] + [
|
227
|
+
LedgerLineImpl(
|
228
|
+
date=line.date,
|
229
|
+
amount=el.amount,
|
230
|
+
currency=el.currency,
|
231
|
+
debit_account=SUNDRY,
|
232
|
+
credit_account=el.account,
|
233
|
+
)
|
234
|
+
for el in line.credit
|
235
|
+
]
|
236
|
+
|
237
|
+
|
238
|
+
def generalledger_to_multiledger(
|
239
|
+
lines: Sequence[GeneralLedgerLine[Account, Currency]],
|
240
|
+
is_debit: Callable[[Account], bool],
|
241
|
+
) -> Sequence[MultiLedgerLine[Account, Currency]]:
|
242
|
+
"""
|
243
|
+
Convert a GeneralLedger to a MultiLedger.
|
244
|
+
|
245
|
+
Parameters
|
246
|
+
----------
|
247
|
+
lines : Sequence[GeneralLedgerLine[Account, Currency]]
|
248
|
+
The GeneralLedger to convert.
|
249
|
+
is_debit : Callable[[Account], bool]
|
250
|
+
Whether the account is a debit account.
|
251
|
+
|
252
|
+
Returns
|
253
|
+
-------
|
254
|
+
Sequence[MultiLedgerLine[Account, Currency]]
|
255
|
+
The converted MultiLedger.
|
256
|
+
|
257
|
+
"""
|
258
|
+
return [generalledger_line_to_multiledger_line(line, is_debit) for line in lines]
|
259
|
+
|
260
|
+
|
261
|
+
def multiledger_to_generalledger(
|
262
|
+
lines: Sequence[MultiLedgerLine[Account, Currency]],
|
263
|
+
) -> Sequence[GeneralLedgerLine[Account, Currency]]:
|
264
|
+
"""
|
265
|
+
Convert a MultiLedger to a GeneralLedger.
|
266
|
+
|
267
|
+
Parameters
|
268
|
+
----------
|
269
|
+
lines : Sequence[MultiLedgerLine[Account, Currency]]
|
270
|
+
The MultiLedger to convert.
|
271
|
+
|
272
|
+
Returns
|
273
|
+
-------
|
274
|
+
Sequence[GeneralLedgerLine[Account, Currency]]
|
275
|
+
The converted GeneralLedger.
|
276
|
+
|
277
|
+
"""
|
278
|
+
return [multiledger_line_to_generalledger_line(line) for line in lines]
|
279
|
+
|
280
|
+
|
281
|
+
def multiledger_to_ledger(
|
282
|
+
lines: Sequence[MultiLedgerLine[Account, Currency]],
|
283
|
+
) -> Sequence[LedgerLine[Account | AccountSundry, Currency]]:
|
284
|
+
"""
|
285
|
+
Convert a MultiLedger to a Ledger.
|
286
|
+
|
287
|
+
Parameters
|
288
|
+
----------
|
289
|
+
lines : Sequence[MultiLedgerLine[Account, Currency]]
|
290
|
+
The MultiLedger to convert.
|
291
|
+
|
292
|
+
Returns
|
293
|
+
-------
|
294
|
+
Sequence[LedgerLine[Account | AccountSundry, Currency]]
|
295
|
+
The converted Ledger.
|
296
|
+
|
297
|
+
"""
|
298
|
+
return list(chain(*[multiledger_line_to_ledger_line(line) for line in lines]))
|
@@ -0,0 +1,186 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from collections.abc import Iterable, Mapping, Sequence
|
3
|
+
from decimal import ROUND_DOWN, Decimal, localcontext
|
4
|
+
from pathlib import Path
|
5
|
+
from sys import platform
|
6
|
+
from typing import Callable, Literal
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
import pandas as pd
|
10
|
+
import requests
|
11
|
+
|
12
|
+
from ._ledger import (
|
13
|
+
Account,
|
14
|
+
Currency,
|
15
|
+
GeneralLedgerLine,
|
16
|
+
GeneralLedgerLineImpl,
|
17
|
+
LedgerElement,
|
18
|
+
LedgerElementImpl,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
def _get_currency(
|
23
|
+
lines: Sequence[GeneralLedgerLine[Account, Currency]],
|
24
|
+
) -> Sequence[Currency]:
|
25
|
+
return np.unique([x.currency for line in lines for x in line.values])
|
26
|
+
|
27
|
+
|
28
|
+
def get_prices(
|
29
|
+
currency: Iterable[Currency],
|
30
|
+
) -> Mapping[Currency, "pd.Series[Decimal]"]:
|
31
|
+
"""
|
32
|
+
Get prices of the currency indexed by date (not datetime).
|
33
|
+
|
34
|
+
Interpolates missing dates by forward fill.
|
35
|
+
|
36
|
+
Parameters
|
37
|
+
----------
|
38
|
+
currency : Iterable[Currency]
|
39
|
+
The currency to get prices.
|
40
|
+
|
41
|
+
Returns
|
42
|
+
-------
|
43
|
+
Mapping[Currency, pd.Series[Decimal]]
|
44
|
+
The prices, indexed by date.
|
45
|
+
|
46
|
+
"""
|
47
|
+
URL = "https://www.mizuhobank.co.jp/market/quote.csv"
|
48
|
+
path_dir = Path("~/.cache/aoiro").expanduser()
|
49
|
+
path_dir.mkdir(exist_ok=True, parents=True)
|
50
|
+
path = path_dir / "quote.csv"
|
51
|
+
if not path.exists():
|
52
|
+
with path.open("w", encoding="utf-8") as f:
|
53
|
+
r = requests.get(URL, timeout=5)
|
54
|
+
r.encoding = "Shift_JIS"
|
55
|
+
f.write(r.text)
|
56
|
+
df = pd.read_csv(
|
57
|
+
path,
|
58
|
+
index_col=0,
|
59
|
+
skiprows=3 if platform == "win32" else 2,
|
60
|
+
na_values=["*****"],
|
61
|
+
parse_dates=True,
|
62
|
+
)
|
63
|
+
# fill missing dates
|
64
|
+
df = df.reindex(pd.date_range(df.index[0], df.index[-1]), method="ffill")
|
65
|
+
return df
|
66
|
+
|
67
|
+
|
68
|
+
def multidimensional_ledger_to_ledger(
|
69
|
+
lines: Sequence[GeneralLedgerLine[Account, Currency]],
|
70
|
+
is_debit: Callable[[Account], bool],
|
71
|
+
prices: Mapping[Currency, "pd.Series[Decimal]"] = {},
|
72
|
+
) -> Sequence[
|
73
|
+
GeneralLedgerLineImpl[Account | Literal["為替差益", "為替差損"], Literal[""]]
|
74
|
+
]:
|
75
|
+
"""
|
76
|
+
Convert multidimensional ledger to ledger.
|
77
|
+
|
78
|
+
The multidimensional ledger's any account must have
|
79
|
+
positive amount.
|
80
|
+
At the BOY, the previous B/S sheet
|
81
|
+
must be added as a general ledger line, for example,
|
82
|
+
|
83
|
+
>>> lines.append(
|
84
|
+
>>> GeneralLedgerLineImpl(
|
85
|
+
>>> date=datetime(2024, 1, 1),
|
86
|
+
>>> values=[
|
87
|
+
>>> ("売掛金", 1000, ""),
|
88
|
+
>>> ("元入金", 1000, "")
|
89
|
+
>>> ]
|
90
|
+
>>> )
|
91
|
+
>>> )
|
92
|
+
|
93
|
+
Returns
|
94
|
+
-------
|
95
|
+
GeneralLedgerLineImpl[Account | Literal["為替差益", "為替差損"], Literal[""]]
|
96
|
+
The ledger lines.
|
97
|
+
|
98
|
+
"""
|
99
|
+
# get prices, use prices_
|
100
|
+
lines = sorted(lines, key=lambda x: x.date)
|
101
|
+
prices_ = dict(prices)
|
102
|
+
del prices
|
103
|
+
prices_.update(get_prices(set(_get_currency(lines)) - set(prices_.keys())))
|
104
|
+
|
105
|
+
# balance of (account, currency) represented by tuple (price, amount)
|
106
|
+
balance: dict[Account, dict[Currency, list[tuple[Decimal, Decimal]]]] = defaultdict(
|
107
|
+
lambda: defaultdict(list)
|
108
|
+
)
|
109
|
+
|
110
|
+
# the new lines
|
111
|
+
lines_new = []
|
112
|
+
for line in lines:
|
113
|
+
# profit of the old line
|
114
|
+
profit = Decimal(0)
|
115
|
+
|
116
|
+
# the values of the new line
|
117
|
+
values: list[
|
118
|
+
LedgerElement[Account | Literal["為替差益", "為替差損"], Literal[""]]
|
119
|
+
] = []
|
120
|
+
|
121
|
+
# iterate over the values of the old line
|
122
|
+
for el in line.values:
|
123
|
+
amount_left = el.amount
|
124
|
+
# skip if the currency is empty (default, exchange rate = 1)
|
125
|
+
if el.currency == "":
|
126
|
+
values.append(el) # type: ignore
|
127
|
+
continue
|
128
|
+
|
129
|
+
# meaning of "quote": https://docs.ccxt.com/#/?id=market-structure
|
130
|
+
price_current = Decimal(str(prices_[el.currency][line.date]))
|
131
|
+
amount_in_quote = Decimal(0)
|
132
|
+
if amount_left < 0:
|
133
|
+
while amount_left < 0:
|
134
|
+
# the maximum amount to subtract from the first element
|
135
|
+
# of the balance[account][currency]
|
136
|
+
amount_substract = max(
|
137
|
+
-amount_left, balance[el.account][el.currency][0][1]
|
138
|
+
)
|
139
|
+
amount_in_quote -= (
|
140
|
+
amount_substract * balance[el.account][el.currency][0][0]
|
141
|
+
)
|
142
|
+
amount_left += amount_substract
|
143
|
+
|
144
|
+
# subtract the amount from the balance
|
145
|
+
balance[el.account][el.currency][0] = (
|
146
|
+
balance[el.account][el.currency][0][0],
|
147
|
+
balance[el.account][el.currency][0][1] - amount_substract,
|
148
|
+
)
|
149
|
+
# remove if the amount is zero
|
150
|
+
if balance[el.account][el.currency][0][1] == 0:
|
151
|
+
balance[el.account][el.currency].pop(0)
|
152
|
+
else:
|
153
|
+
balance[el.account][el.currency].append((price_current, amount_left))
|
154
|
+
amount_in_quote = amount_left * price_current
|
155
|
+
|
156
|
+
# round the amount_in_quote
|
157
|
+
with localcontext() as ctx:
|
158
|
+
ctx.rounding = ROUND_DOWN
|
159
|
+
amount_in_quote = round(amount_in_quote, 0)
|
160
|
+
|
161
|
+
# append to the values of the new line
|
162
|
+
values.append(
|
163
|
+
LedgerElementImpl(
|
164
|
+
account=el.account, amount=amount_in_quote, currency=""
|
165
|
+
)
|
166
|
+
)
|
167
|
+
|
168
|
+
# add to the profit
|
169
|
+
if is_debit(el.account) is True:
|
170
|
+
profit += amount_in_quote
|
171
|
+
else:
|
172
|
+
profit -= amount_in_quote
|
173
|
+
|
174
|
+
# append only if profit is not zero
|
175
|
+
if profit != 0:
|
176
|
+
values.append(
|
177
|
+
LedgerElementImpl(
|
178
|
+
account="為替差益" if profit > 0 else "為替差損",
|
179
|
+
amount=abs(profit),
|
180
|
+
currency="",
|
181
|
+
)
|
182
|
+
)
|
183
|
+
|
184
|
+
# append the new line
|
185
|
+
lines_new.append(GeneralLedgerLineImpl(date=line.date, values=values))
|
186
|
+
return lines_new
|
aoiro/_sheets.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
from collections.abc import Sequence
|
2
|
+
from itertools import groupby
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import networkx as nx
|
6
|
+
|
7
|
+
from ._ledger import Account, Currency, GeneralLedgerLine
|
8
|
+
|
9
|
+
|
10
|
+
def get_sheets(
|
11
|
+
lines: Sequence[GeneralLedgerLine[Account, Currency]],
|
12
|
+
G: nx.DiGraph,
|
13
|
+
*,
|
14
|
+
drop: bool = True,
|
15
|
+
) -> nx.DiGraph:
|
16
|
+
"""
|
17
|
+
Get the blue return accounts as a graph.
|
18
|
+
|
19
|
+
Returns
|
20
|
+
-------
|
21
|
+
nx.DiGraph
|
22
|
+
Tree representation of the blue return account list.
|
23
|
+
Has the following attributes:
|
24
|
+
|
25
|
+
sum: dict[Currency, Decimal]
|
26
|
+
The sum of the children for each currency,
|
27
|
+
with alternating signs for each AccountType.
|
28
|
+
|
29
|
+
sum_natural: dict[Currency, Decimal]
|
30
|
+
The sum of the children for each currency.
|
31
|
+
For accounts with AccountType well-defined, the sum is not
|
32
|
+
altered.
|
33
|
+
For accounts with AccountType None, the value is the same as sum.
|
34
|
+
|
35
|
+
"""
|
36
|
+
G = G.copy()
|
37
|
+
values = [value for line in lines for value in line.values]
|
38
|
+
grouped = {
|
39
|
+
k: list(v)
|
40
|
+
for k, v in groupby(
|
41
|
+
sorted(values, key=lambda x: (x.account, x.currency)),
|
42
|
+
key=lambda x: (x.account, x.currency),
|
43
|
+
)
|
44
|
+
}
|
45
|
+
grouped_nested = {
|
46
|
+
k: dict(v) for k, v in groupby(grouped.items(), key=lambda x: x[0][0])
|
47
|
+
}
|
48
|
+
|
49
|
+
# Check that all accounts are in G
|
50
|
+
all_accounts = set(grouped_nested.keys())
|
51
|
+
all_accounts_G = {d["label"] for n, d in G.nodes(data=True) if not d["abstract"]}
|
52
|
+
if all_accounts - all_accounts_G:
|
53
|
+
raise ValueError(f"{all_accounts - all_accounts_G} not in G")
|
54
|
+
|
55
|
+
# non-abstract accounts
|
56
|
+
met_accounts: set[Any] = set()
|
57
|
+
for n in reversed(list(nx.topological_sort(G))):
|
58
|
+
d = G.nodes[n]
|
59
|
+
successors = list(G.successors(n))
|
60
|
+
add_current = (not d["abstract"]) and d["label"] in (
|
61
|
+
all_accounts - met_accounts
|
62
|
+
)
|
63
|
+
if successors or add_current:
|
64
|
+
G.nodes[n]["sum"] = _dict_sum(
|
65
|
+
[G.nodes[child]["sum"] for child in successors]
|
66
|
+
+ (
|
67
|
+
[
|
68
|
+
{
|
69
|
+
currency: sum(el.amount for el in values)
|
70
|
+
* (1 if d["account_type"].debit else -1)
|
71
|
+
for (_, currency), values in grouped_nested[
|
72
|
+
d["label"]
|
73
|
+
].items()
|
74
|
+
}
|
75
|
+
]
|
76
|
+
if add_current
|
77
|
+
else []
|
78
|
+
)
|
79
|
+
)
|
80
|
+
if add_current:
|
81
|
+
met_accounts.add(d["label"])
|
82
|
+
else:
|
83
|
+
if drop:
|
84
|
+
G.remove_node(n)
|
85
|
+
else:
|
86
|
+
G.nodes[n]["sum"] = {}
|
87
|
+
|
88
|
+
# natural sum
|
89
|
+
for n, d in G.nodes(data=True):
|
90
|
+
account_type = d["account_type"]
|
91
|
+
if account_type is not None:
|
92
|
+
G.nodes[n]["sum_natural"] = {
|
93
|
+
k: v * (1 if account_type.debit else -1) for k, v in d["sum"].items()
|
94
|
+
}
|
95
|
+
else:
|
96
|
+
G.nodes[n]["sum_natural"] = d["sum"]
|
97
|
+
return G
|
98
|
+
|
99
|
+
|
100
|
+
def _dict_sum(
|
101
|
+
ds: Sequence[dict[Any, Any]],
|
102
|
+
/,
|
103
|
+
) -> dict[Any, Any]:
|
104
|
+
"""
|
105
|
+
Sum dictionaries.
|
106
|
+
|
107
|
+
Return a dictionary which,
|
108
|
+
for any key in any of the dictionaries,
|
109
|
+
contains the sum of the values of that key in all dictionaries
|
110
|
+
where the key is present.
|
111
|
+
|
112
|
+
Parameters
|
113
|
+
----------
|
114
|
+
ds : Sequence[dict[Any, Any]]
|
115
|
+
The dictionaries to sum.
|
116
|
+
|
117
|
+
Returns
|
118
|
+
-------
|
119
|
+
dict[Any, Any]
|
120
|
+
The sum of the dictionaries.
|
121
|
+
|
122
|
+
"""
|
123
|
+
return {k: sum([d.get(k, 0) for d in ds]) for k in set().union(*ds)}
|