aoiro 0.0.3__py3-none-any.whl → 0.1.0__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/account.yml ADDED
@@ -0,0 +1,68 @@
1
+ 資産: #貸方
2
+ 資産:
3
+ [
4
+ 現金,
5
+ 当座預金,
6
+ 定期預金,
7
+ その他の預金,
8
+ 受取手形,
9
+ 売掛金,
10
+ 有価証券,
11
+ 棚卸資産,
12
+ 前払金,
13
+ 貸付金,
14
+ 建物,
15
+ 建物附属設備,
16
+ 機械装置,
17
+ 車両運搬具,
18
+ 工具器具備品,
19
+ 土地,
20
+ ]
21
+ 事業主貸: [事業主貸]
22
+ 負債:
23
+ 負債: [支払手形, 買掛金, 借入金, 未払金, 前受金, 預り金]
24
+ 貸倒引当金: [貸倒引当金]
25
+ 事業主借: [事業主借]
26
+ 純資産:
27
+ 純資産: [元入金]
28
+ 収益: #貸方
29
+ 売上: [売上, 雑収入]
30
+ 売上原価: [仕入返品, 期末商品棚卸高]
31
+ 各種引当金・準備金等: [貸倒引当金繰戻]
32
+ 追加: [家事消費]
33
+ 費用: #借方
34
+ 売上: [売上返品]
35
+ 売上原価: [仕入, 期首商品棚卸高]
36
+ 経費:
37
+ [
38
+ 租税公課,
39
+ 荷造運費,
40
+ 水道光熱費,
41
+ 旅費交通費,
42
+ 通信費,
43
+ 広告宣伝費,
44
+ 接待交際費,
45
+ 損害保険料,
46
+ 修繕費,
47
+ 消耗品費,
48
+ 減価償却費,
49
+ 福利厚生費,
50
+ 給料賃金,
51
+ 外注工賃,
52
+ 利子割引料,
53
+ 地代家賃,
54
+ 貸倒金,
55
+ 雑費,
56
+ ]
57
+ 各種引当金・準備金等: [専従者給与, 貸倒引当金]
58
+ 追加:
59
+ [
60
+ 法定福利費,
61
+ 税理士・弁護士報酬,
62
+ 支払手数料,
63
+ 新聞図書費,
64
+ 車両費,
65
+ 研修費,
66
+ 会議費,
67
+ 諸会費,
68
+ ]
aoiro/cli.py CHANGED
@@ -1,12 +1,102 @@
1
- import typer
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+
4
+ # from rich import print
5
+ import attrs
6
+ import cyclopts
7
+ import networkx as nx
8
+ import pandas as pd
9
+ from account_codes_jp import (
10
+ get_account_type_factory,
11
+ get_blue_return_accounts,
12
+ get_node_from_label,
13
+ )
14
+ from networkx.readwrite.text import generate_network_text
2
15
  from rich import print
3
16
 
4
- from .main import add
17
+ from ._ledger import (
18
+ generalledger_to_multiledger,
19
+ multiledger_to_ledger,
20
+ )
21
+ from ._multidimensional import multidimensional_ledger_to_ledger
22
+ from ._sheets import get_sheets
23
+ from .reader._expenses import ledger_from_expenses
24
+ from .reader._io import read_general_ledger
25
+ from .reader._sales import ledger_from_sales
26
+
27
+ app = cyclopts.App(name="aoiro")
28
+
29
+
30
+ @app.default
31
+ def metrics(path: Path, year: int | None = None, drop: bool = True) -> None:
32
+ """
33
+ Calculate metrics needed for tax declaration.
34
+
35
+ Parameters
36
+ ----------
37
+ path : Path
38
+ The path to the directory containing CSV files.
39
+ year : int | None, optional
40
+ The year to calculate, by default None.
41
+ If None, the previous year would be used.
42
+ drop : bool, optional
43
+ Whether to drop unused accounts, by default True.
44
+
45
+ """
46
+ if year is None:
47
+ year = datetime.now().year - 1
48
+
49
+ def patch_G(G: nx.DiGraph) -> nx.DiGraph:
50
+ G.add_node(-1, label="為替差益")
51
+ G.add_node(-2, label="為替差損")
52
+ G.add_edge(next(n for n, d in G.nodes(data=True) if d["label"] == "売上"), -1)
53
+ G.add_edge(
54
+ next(n for n, d in G.nodes(data=True) if d["label"] == "経費追加"), -2
55
+ )
56
+ return G
57
+
58
+ G = get_blue_return_accounts(patch_G)
59
+
60
+ gledger_vec = (
61
+ list(ledger_from_sales(path, G))
62
+ + list(ledger_from_expenses(path))
63
+ + list(read_general_ledger(path))
64
+ )
65
+ f = get_account_type_factory(G)
5
66
 
6
- app = typer.Typer()
67
+ def is_debit(x: str) -> bool:
68
+ v = getattr(f(x), "debit", None)
69
+ if v is None:
70
+ raise ValueError(f"Account {x} not recognized")
71
+ return v
7
72
 
73
+ gledger = multidimensional_ledger_to_ledger(gledger_vec, is_debit=is_debit)
74
+ ledger = multiledger_to_ledger(
75
+ generalledger_to_multiledger(gledger, is_debit=is_debit)
76
+ )
77
+ ledger_now = [line for line in ledger if line.date.year == year]
78
+ with pd.option_context("display.max_rows", None, "display.max_columns", None):
79
+ print(
80
+ pd.DataFrame([attrs.asdict(line) for line in ledger_now]) # type: ignore
81
+ .set_index("date")
82
+ .sort_index(axis=0)
83
+ )
84
+ gledger_now = [line for line in gledger if line.date.year == year]
85
+ G = get_sheets(gledger_now, G, drop=drop)
86
+ G_print = G.copy()
87
+ for n, d in G_print.nodes(data=True):
88
+ G_print.nodes[n]["label"] = f"{d['label']}/{d['sum_natural'].get('', 0)}"
89
+ for line in generate_network_text(G_print, with_labels=True):
90
+ print(line)
8
91
 
9
- @app.command()
10
- def main(n1: int, n2: int) -> None:
11
- """Add the arguments and print the result."""
12
- print(add(n1, n2))
92
+ # sales per month
93
+ print("Sales per month")
94
+ for month in range(1, 13):
95
+ G_month = get_sheets(
96
+ [line for line in gledger_now if line.date.month == month], G, drop=False
97
+ )
98
+ sales_deeper_node = get_node_from_label(
99
+ G, "売上", lambda x: not G.nodes[x]["abstract"]
100
+ )
101
+ sales_deeper = G_month.nodes[sales_deeper_node]["sum_natural"].get("", 0)
102
+ print(f"{month}: {sales_deeper}")
@@ -0,0 +1,12 @@
1
+ from ._expenses import ledger_from_expenses
2
+ from ._io import read_all_csvs, read_general_ledger, read_simple_csvs
3
+ from ._sales import ledger_from_sales, withholding_tax
4
+
5
+ __all__ = [
6
+ "ledger_from_expenses",
7
+ "ledger_from_sales",
8
+ "read_all_csvs",
9
+ "read_general_ledger",
10
+ "read_simple_csvs",
11
+ "withholding_tax",
12
+ ]
@@ -0,0 +1,51 @@
1
+ from collections.abc import Sequence
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from .._ledger import GeneralLedgerLineImpl, LedgerElementImpl
6
+ from ._io import read_simple_csvs
7
+
8
+
9
+ def ledger_from_expenses(
10
+ path: Path,
11
+ ) -> Sequence[GeneralLedgerLineImpl[Any, Any]]:
12
+ """
13
+ Generate ledger from expenses.
14
+
15
+ The CSV files are assumed to have columns
16
+ ["勘定科目"].
17
+ The relative path of the CSV file would be used as "取引先".
18
+
19
+ Parameters
20
+ ----------
21
+ path : Path
22
+ The path to the directory containing CSV files.
23
+
24
+ Returns
25
+ -------
26
+ Sequence[GeneralLedgerLineImpl[Any, Any]]
27
+ The ledger lines.
28
+
29
+ """
30
+ df = read_simple_csvs(path / "expenses")
31
+ if df.empty:
32
+ return []
33
+ df["取引先"] = df["path"]
34
+ res: list[GeneralLedgerLineImpl[Any, Any]] = []
35
+ for date, row in df.iterrows():
36
+ res.append(
37
+ GeneralLedgerLineImpl(
38
+ date=date,
39
+ values=[
40
+ LedgerElementImpl(
41
+ account="事業主借", amount=row["金額"], currency=row["通貨"]
42
+ ),
43
+ LedgerElementImpl(
44
+ account=row["勘定科目"],
45
+ amount=row["金額"],
46
+ currency=row["通貨"],
47
+ ),
48
+ ],
49
+ )
50
+ )
51
+ return res
aoiro/reader/_io.py ADDED
@@ -0,0 +1,177 @@
1
+ import re
2
+ import warnings
3
+ from collections.abc import Iterable
4
+ from decimal import Decimal
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pandas as pd
9
+ from dateparser import parse
10
+
11
+ from .._ledger import GeneralLedgerLineImpl, LedgerElementImpl
12
+
13
+
14
+ def read_all_csvs(path: Path, /, **kwargs: Any) -> pd.DataFrame:
15
+ """
16
+ Read all CSV files in the path.
17
+
18
+ Parameters
19
+ ----------
20
+ path : Path
21
+ The path to the directory containing CSV files.
22
+ **kwargs : Any
23
+ The keyword arguments for `pd.read_csv`.
24
+
25
+ Returns
26
+ -------
27
+ pd.DataFrame
28
+ The concatenated DataFrame with
29
+ column "path" containing the relative path of the CSV file added.
30
+
31
+ """
32
+ dfs = []
33
+ for p in path.rglob("*.csv"):
34
+ df = pd.read_csv(p, **kwargs)
35
+ df["path"] = p.relative_to(path).as_posix()
36
+ dfs.append(df)
37
+ if not dfs:
38
+ return pd.DataFrame(columns=["path"])
39
+ return pd.concat(dfs)
40
+
41
+
42
+ def parse_date(s: str) -> pd.Timestamp:
43
+ """
44
+ Parse date.
45
+
46
+ Prefer the last day of the month if the day is not provided.
47
+
48
+ Parameters
49
+ ----------
50
+ s : str
51
+ The string to parse.
52
+
53
+ Returns
54
+ -------
55
+ pd.Timestamp
56
+ The parsed date.
57
+
58
+ """
59
+ return pd.Timestamp(parse(s, settings={"PREFER_DAY_OF_MONTH": "last"}))
60
+
61
+
62
+ def parse_money(
63
+ s: str, currency: str | None = None
64
+ ) -> tuple[Decimal | None, str | None]:
65
+ """
66
+ Parse money.
67
+
68
+ Parameters
69
+ ----------
70
+ s : str
71
+ The string to parse.
72
+ currency : str | None, optional
73
+ The currency, by default None.
74
+ If provided, the currency
75
+ in the string would be ignored and replaced by this.
76
+
77
+ Returns
78
+ -------
79
+ tuple[Decimal | None, str | None]
80
+ The amount and the currency.
81
+
82
+ """
83
+ match = re.search(r"[\d.]+", s)
84
+ if match is None:
85
+ return None, None
86
+ amount = Decimal(match.group())
87
+ if currency is None:
88
+ currency = re.sub(r"\s+", "", s[: match.start()] + s[match.end() :])
89
+ return amount, currency
90
+
91
+
92
+ def read_simple_csvs(path: Path) -> pd.DataFrame:
93
+ """
94
+ Read all CSV files in the path.
95
+
96
+ The CSV files are assumed to have columns
97
+ ["発生日", "金額"].
98
+
99
+ Parameters
100
+ ----------
101
+ path : Path
102
+ The path to the directory containing CSV files.
103
+
104
+ Returns
105
+ -------
106
+ pd.DataFrame
107
+ The concatenated DataFrame with columns
108
+ ["発生日", "金額", "通貨", "path"].
109
+
110
+ """
111
+ df = read_all_csvs(path, dtype=str)
112
+ for col in ["発生日", "金額"]:
113
+ if col not in df.columns:
114
+ df[col] = None
115
+
116
+ # parse date
117
+ for k in df.columns:
118
+ if "日" not in k:
119
+ continue
120
+ df[k] = df[k].map(parse_date)
121
+
122
+ # parse money
123
+ df[["金額", "通貨"]] = pd.DataFrame(
124
+ df["金額"].map(parse_money).tolist(), index=df.index
125
+ )
126
+
127
+ # set date as index
128
+ df.set_index("発生日", inplace=True, drop=False)
129
+ return df
130
+
131
+
132
+ def read_general_ledger(path: Path) -> Iterable[GeneralLedgerLineImpl[Any, Any]]:
133
+ """
134
+ Read general ledger.
135
+
136
+ The first column is assumed to be the date.
137
+ For all n in N. the 2n-1-th column is assumed to be
138
+ the account name, and the 2n-th column
139
+ is assumed to be the amount.
140
+
141
+ Parameters
142
+ ----------
143
+ path : Path
144
+ The path to the CSV file.
145
+
146
+ Returns
147
+ -------
148
+ Iterable[GeneralLedgerLineImpl[Any, Any]]
149
+ The general ledger.
150
+
151
+ """
152
+ df = read_all_csvs(path / "general", header=None, dtype=str)
153
+ df.drop(columns="path", inplace=True)
154
+ if df.empty:
155
+ return
156
+ if len(df.columns) % 2 != 1:
157
+ raise ValueError("The number of columns should be odd.")
158
+ if len(df.columns) < 3:
159
+ raise ValueError("The number of columns should be at least 3.")
160
+ for _, row in df.iterrows():
161
+ values: list[LedgerElementImpl[Any, Any]] = []
162
+ for i in range(1, len(row), 2):
163
+ amount, currency = parse_money(row[i + 1])
164
+ if amount is None:
165
+ warnings.warn(f"Amount not found in {row[i + 1]}", stacklevel=2)
166
+ continue
167
+ values.append(
168
+ LedgerElementImpl(
169
+ account=row[i],
170
+ amount=amount,
171
+ currency=currency,
172
+ )
173
+ )
174
+ yield GeneralLedgerLineImpl(
175
+ values=values,
176
+ date=parse_date(row[0]),
177
+ )
aoiro/reader/_sales.py ADDED
@@ -0,0 +1,158 @@
1
+ from collections.abc import Sequence
2
+ from decimal import ROUND_DOWN, Decimal, localcontext
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import networkx as nx
7
+ import pandas as pd
8
+ from account_codes_jp import get_node_from_label
9
+
10
+ from .._ledger import GeneralLedgerLineImpl, LedgerElementImpl
11
+ from ._io import read_simple_csvs
12
+
13
+
14
+ def withholding_tax(amount: Decimal) -> Decimal:
15
+ """
16
+ Withholding tax calculation for most 源泉徴収が必要な報酬・料金等.
17
+
18
+ Parameters
19
+ ----------
20
+ amount : Decimal
21
+ The raw amount.
22
+
23
+ Returns
24
+ -------
25
+ Decimal
26
+ The withholding tax amount.
27
+
28
+ References
29
+ ----------
30
+ https://www.nta.go.jp/taxes/shiraberu/taxanswer/gensen/2792.htm
31
+
32
+ """
33
+ with localcontext() as ctx:
34
+ ctx.rounding = ROUND_DOWN
35
+ if amount > 1000000:
36
+ return round(
37
+ Decimal(
38
+ 1000000 * Decimal("0.1021") + (amount - 1000000) * Decimal("0.2042")
39
+ ),
40
+ 0,
41
+ )
42
+ else:
43
+ return round(amount * Decimal("0.1021"), 0)
44
+
45
+
46
+ def ledger_from_sales(
47
+ path: Path,
48
+ G: nx.DiGraph | None = None,
49
+ ) -> Sequence[GeneralLedgerLineImpl[Any, Any]]:
50
+ """
51
+ Generate ledger from sales.
52
+
53
+ The CSV files are assumed to have columns
54
+ ["発生日", "金額", "振込日", "源泉徴収", "手数料"].
55
+ If "源泉徴収" is True, the amount would be assumed by `withholding_tax()`.
56
+ If "源泉徴収" if False or NaN, the amount would be assumed as 0.
57
+ If "源泉徴収" is numeric, the amount would be assumed as 源泉徴収額.
58
+ The relative path of the CSV file would be used as "取引先".
59
+
60
+ Parameters
61
+ ----------
62
+ path : Path
63
+ The path to the directory containing CSV files.
64
+ G : nx.DiGraph | None
65
+ The graph of accounts, by default None.
66
+ If provided, each "取引先" would be added as a child node of "売上".
67
+
68
+ Returns
69
+ -------
70
+ Sequence[GneralLedgerLineImpl[Any, Any]]
71
+ The ledger lines.
72
+
73
+ Raises
74
+ ------
75
+ ValueError
76
+ If the transaction date is later than the transfer date.
77
+ ValueError
78
+ If withholding tax is included in transactions with different currencies.
79
+
80
+ """
81
+ df = read_simple_csvs(path / "sales")
82
+ if df.empty:
83
+ return []
84
+ df["取引先"] = df["path"].str.replace(".csv", "")
85
+ df["手数料"] = df["手数料"].apply(
86
+ lambda x: Decimal(x) if pd.notna(x) else Decimal(0)
87
+ )
88
+ df["源泉徴収"] = df["源泉徴収"].replace(
89
+ {"True": True, "False": False, "true": True, "false": False}
90
+ )
91
+ df.fillna({"源泉徴収": Decimal(0)}, inplace=True)
92
+
93
+ if G is not None:
94
+ for ca in ["売上", "仮払税金"]:
95
+ parent_node = get_node_from_label(
96
+ G, ca, lambda x: not G.nodes[x]["abstract"]
97
+ )
98
+ parent_node_attrs = G.nodes[parent_node]
99
+ for t in df["取引先"].unique():
100
+ t_attrs = {**parent_node_attrs, "label": f"{ca}({t})"}
101
+ t_id = f"{ca}({t})"
102
+ G.add_node(t_id, **t_attrs)
103
+ G.add_edge(parent_node, t_id)
104
+
105
+ ledger_lines: list[GeneralLedgerLineImpl[Any, Any]] = []
106
+ for date, row in df.iterrows():
107
+ ledger_lines.append(
108
+ GeneralLedgerLineImpl(
109
+ date=date,
110
+ values=[
111
+ LedgerElementImpl(
112
+ account="売掛金", amount=row["金額"], currency=row["通貨"]
113
+ ),
114
+ LedgerElementImpl(
115
+ account="売上" if G is None else f"売上({row['取引先']})",
116
+ amount=row["金額"],
117
+ currency=row["通貨"],
118
+ ),
119
+ ],
120
+ )
121
+ )
122
+ for (t, date, currency), df_ in df.groupby(["取引先", "振込日", "通貨"]):
123
+ amount = Decimal(df_["金額"].sum())
124
+ if currency == "":
125
+ withholding = withholding_tax(
126
+ df_.loc[df_["源泉徴収"] == True, "金額"].sum()
127
+ )
128
+ values = [
129
+ LedgerElementImpl(
130
+ account="事業主貸", amount=amount - withholding, currency=currency
131
+ )
132
+ ]
133
+ if withholding > 0:
134
+ values.append(
135
+ LedgerElementImpl(
136
+ account=f"仮払税金({t})", amount=withholding, currency=currency
137
+ )
138
+ )
139
+ else:
140
+ if (df_["源泉徴収"] == True).any():
141
+ raise ValueError("通貨が異なる取引に源泉徴収が含まれています。")
142
+ values = [
143
+ LedgerElementImpl(account="事業主貸", amount=amount, currency=currency)
144
+ ]
145
+ ledger_lines.append(
146
+ GeneralLedgerLineImpl(
147
+ date=date,
148
+ values=[
149
+ *values,
150
+ LedgerElementImpl(
151
+ account="売掛金", amount=-amount, currency=currency
152
+ ),
153
+ ],
154
+ )
155
+ )
156
+ if (df["発生日"] > df["振込日"]).any():
157
+ raise ValueError("発生日が振込日より後の取引があります。")
158
+ return ledger_lines