beancount-helper 0.1.0__tar.gz
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.
- beancount_helper-0.1.0/.gitignore +12 -0
- beancount_helper-0.1.0/.python-version +1 -0
- beancount_helper-0.1.0/LICENSE +21 -0
- beancount_helper-0.1.0/PKG-INFO +219 -0
- beancount_helper-0.1.0/README.md +207 -0
- beancount_helper-0.1.0/pyproject.toml +24 -0
- beancount_helper-0.1.0/rules.example.txt +19 -0
- beancount_helper-0.1.0/src/__init__.py +0 -0
- beancount_helper-0.1.0/src/beancount/__init__.py +0 -0
- beancount_helper-0.1.0/src/beancount/core.py +88 -0
- beancount_helper-0.1.0/src/beancount_helper/__init__.py +0 -0
- beancount_helper-0.1.0/src/beancount_helper/cli.py +243 -0
- beancount_helper-0.1.0/src/bill_resolver/__init__.py +0 -0
- beancount_helper-0.1.0/src/bill_resolver/ccb.py +49 -0
- beancount_helper-0.1.0/src/bill_resolver/wechat.py +51 -0
- beancount_helper-0.1.0/src/transformer/__init__.py +0 -0
- beancount_helper-0.1.0/src/transformer/rule.py +114 -0
- beancount_helper-0.1.0/test/__init__.py +0 -0
- beancount_helper-0.1.0/test/test_beancount_core.py +162 -0
- beancount_helper-0.1.0/test/test_rule_matcher.py +222 -0
- beancount_helper-0.1.0/uv.lock +360 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FishCat233
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beancount-helper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert bank statement Excel files to Beancount format with a rule-based account matching engine.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
9
|
+
Requires-Dist: pandas>=3.0.3
|
|
10
|
+
Requires-Dist: xlrd>=2.0.2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# beancount-helper
|
|
14
|
+
|
|
15
|
+
Convert bank statement Excel files into [Beancount](https://beancount.github.io/) transactions with a rule-based account matching engine.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
2025-01-15 * "Coffee Shop" "Latte"
|
|
19
|
+
Expenses:Food:Coffee 25.00 CNY
|
|
20
|
+
Assets:CCB
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Supported banks
|
|
24
|
+
|
|
25
|
+
| Parser | Source |
|
|
26
|
+
|--------|--------|
|
|
27
|
+
| `ccb` | China Construction Bank (建设银行) statement `.xls` |
|
|
28
|
+
| `wechat` | WeChat Pay (微信支付) statement `.xlsx` |
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install beancount-helper
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/your-org/beancount-helper.git
|
|
40
|
+
cd beancount-helper
|
|
41
|
+
uv sync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
**Step 1** — Export your statement from the bank app and save it locally.
|
|
47
|
+
|
|
48
|
+
**Step 2** — (Optional) Create a rules file:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
# rules.txt
|
|
52
|
+
p=opposite:eq:Coffee Shop;;direct=out=>Expenses:Food:Coffee
|
|
53
|
+
direct=in=>Income:Misc
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Step 3** — Run:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
beancount-helper ccb.xls -p ccb -r rules.txt -o ledger.beancount
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's it. Every record falls into a Beancount account — either matched by your rules, or placed in `Equity:Opening-Balances` as a fallback.
|
|
63
|
+
|
|
64
|
+
## CLI reference
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
beancount-helper INPUT -p PARSER [-r RULES] [-o OUTPUT] [--asset ASSET] [--default-account ACCOUNT]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Argument | Required | Default | Description |
|
|
71
|
+
|----------|----------|---------|-------------|
|
|
72
|
+
| `INPUT` | yes | — | Path to the bank statement file (`.xls` / `.xlsx`). |
|
|
73
|
+
| `-p`, `--parser` | yes | — | `ccb` or `wechat`. |
|
|
74
|
+
| `-r`, `--rules` | no | — | Path to a rules file. Without it, all records use the default account. |
|
|
75
|
+
| `-o`, `--output` | no | `output.beancount` | Output file path. |
|
|
76
|
+
| `--asset` | no | `Assets:CCB` / `Assets:WeChat` | Asset account for the source of funds. |
|
|
77
|
+
| `--default-account` | no | `Equity:Opening-Balances` | Fallback account when no rule matches. |
|
|
78
|
+
|
|
79
|
+
### Examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Bare minimum — everything goes to Equity:Opening-Balances
|
|
83
|
+
beancount-helper ccb.xls -p ccb
|
|
84
|
+
|
|
85
|
+
# With rules
|
|
86
|
+
beancount-helper wechat.xlsx -p wechat -r rules.txt
|
|
87
|
+
|
|
88
|
+
# Custom accounts
|
|
89
|
+
beancount-helper ccb.xls -p ccb -r rules.txt \
|
|
90
|
+
--asset Assets:CCB:Savings \
|
|
91
|
+
--default-account Expenses:Pending
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Rule system
|
|
95
|
+
|
|
96
|
+
Rules tell the tool which Beancount account a bill should be posted to. Each rule has this shape:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
condition1;;condition2;;...=>TargetAccount
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Conditions are AND-ed: a bill must satisfy **all** of them for the rule to fire. Rules are tried in order; the first match wins.
|
|
103
|
+
|
|
104
|
+
### Condition reference
|
|
105
|
+
|
|
106
|
+
#### Direction (`direct` / `direction`)
|
|
107
|
+
|
|
108
|
+
Matches whether money flows in or out.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
direct=in # income
|
|
112
|
+
direct=out # expense
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Property (`p` / `property`)
|
|
116
|
+
|
|
117
|
+
Matches a field on the bill record.
|
|
118
|
+
|
|
119
|
+
| Method | Syntax | Example |
|
|
120
|
+
|--------|--------|---------|
|
|
121
|
+
| exact | `p=field:eq:value` | `p=opposite:eq:Coffee Shop` |
|
|
122
|
+
| contains | `p=field:in:substring` | `p=note:in:lunch` |
|
|
123
|
+
| regex | `p=field:re:pattern` | `p=product:re:^ATMS$` |
|
|
124
|
+
|
|
125
|
+
Available fields: `opposite`, `note`, `product` (wechat only).
|
|
126
|
+
|
|
127
|
+
#### Amount comparisons
|
|
128
|
+
|
|
129
|
+
| Operator | Meaning |
|
|
130
|
+
|----------|---------|
|
|
131
|
+
| `lt=N` | amount < N |
|
|
132
|
+
| `le=N` | amount ≤ N |
|
|
133
|
+
| `gt=N` | amount > N |
|
|
134
|
+
| `ge=N` | amount ≥ N |
|
|
135
|
+
| `eq=N` | amount = N |
|
|
136
|
+
|
|
137
|
+
Negative amounts are expenses, positive are income (from the bank's perspective).
|
|
138
|
+
|
|
139
|
+
### Combining conditions
|
|
140
|
+
|
|
141
|
+
Separate conditions with `;;`:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
# Small lunch expenses from a specific vendor
|
|
145
|
+
direct=out;;p=opposite:in:Restaurant;;lt=50.0=>Expenses:Food:Lunch
|
|
146
|
+
|
|
147
|
+
# Any income
|
|
148
|
+
direct=in=>Income:Misc
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Rule file format
|
|
152
|
+
|
|
153
|
+
One rule per line. Blank lines and `#`-comments are ignored.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
# Food & drinks
|
|
157
|
+
p=opposite:eq:Starbucks=>Expenses:Food:Coffee
|
|
158
|
+
p=opposite:in:Pizza;;lt=80.0=>Expenses:Food:Takeout
|
|
159
|
+
|
|
160
|
+
# Transport
|
|
161
|
+
p=opposite:eq:DiDi=>Expenses:Transport:Ride
|
|
162
|
+
p=note:re:metro|subway=>Expenses:Transport:Transit
|
|
163
|
+
|
|
164
|
+
# Income
|
|
165
|
+
direct=in;;gt=5000.0=>Income:Salary
|
|
166
|
+
direct=in=>Income:Misc
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## How it works
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Bank Excel (.xls/.xlsx)
|
|
173
|
+
│
|
|
174
|
+
▼
|
|
175
|
+
bill_resolver ← parses bank-specific formats into Bill objects
|
|
176
|
+
│
|
|
177
|
+
▼
|
|
178
|
+
transformer.rule ← matches each Bill against the rule set
|
|
179
|
+
│
|
|
180
|
+
▼
|
|
181
|
+
beancount.core ← assembles BeanCount transactions, dumps to text
|
|
182
|
+
│
|
|
183
|
+
▼
|
|
184
|
+
output.beancount
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Project structure
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
beancount-helper/
|
|
191
|
+
├── main.py CLI entry point
|
|
192
|
+
├── rules.example.txt Sample rule set
|
|
193
|
+
├── src/
|
|
194
|
+
│ ├── beancount/core.py BeanCount model + serialization
|
|
195
|
+
│ ├── bill_resolver/
|
|
196
|
+
│ │ ├── ccb.py CCB Excel parser
|
|
197
|
+
│ │ └── wechat.py WeChat Excel parser
|
|
198
|
+
│ └── transformer/rule.py Rule matching engine
|
|
199
|
+
└── test/
|
|
200
|
+
├── test_beancount_core.py
|
|
201
|
+
└── rule_matcher.py
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Install dev dependencies
|
|
208
|
+
uv sync
|
|
209
|
+
|
|
210
|
+
# Run tests
|
|
211
|
+
uv run pytest
|
|
212
|
+
|
|
213
|
+
# Lint
|
|
214
|
+
uv run ruff check .
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# beancount-helper
|
|
2
|
+
|
|
3
|
+
Convert bank statement Excel files into [Beancount](https://beancount.github.io/) transactions with a rule-based account matching engine.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
2025-01-15 * "Coffee Shop" "Latte"
|
|
7
|
+
Expenses:Food:Coffee 25.00 CNY
|
|
8
|
+
Assets:CCB
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Supported banks
|
|
12
|
+
|
|
13
|
+
| Parser | Source |
|
|
14
|
+
|--------|--------|
|
|
15
|
+
| `ccb` | China Construction Bank (建设银行) statement `.xls` |
|
|
16
|
+
| `wechat` | WeChat Pay (微信支付) statement `.xlsx` |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install beancount-helper
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/your-org/beancount-helper.git
|
|
28
|
+
cd beancount-helper
|
|
29
|
+
uv sync
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
**Step 1** — Export your statement from the bank app and save it locally.
|
|
35
|
+
|
|
36
|
+
**Step 2** — (Optional) Create a rules file:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# rules.txt
|
|
40
|
+
p=opposite:eq:Coffee Shop;;direct=out=>Expenses:Food:Coffee
|
|
41
|
+
direct=in=>Income:Misc
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Step 3** — Run:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
beancount-helper ccb.xls -p ccb -r rules.txt -o ledger.beancount
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it. Every record falls into a Beancount account — either matched by your rules, or placed in `Equity:Opening-Balances` as a fallback.
|
|
51
|
+
|
|
52
|
+
## CLI reference
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
beancount-helper INPUT -p PARSER [-r RULES] [-o OUTPUT] [--asset ASSET] [--default-account ACCOUNT]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Argument | Required | Default | Description |
|
|
59
|
+
|----------|----------|---------|-------------|
|
|
60
|
+
| `INPUT` | yes | — | Path to the bank statement file (`.xls` / `.xlsx`). |
|
|
61
|
+
| `-p`, `--parser` | yes | — | `ccb` or `wechat`. |
|
|
62
|
+
| `-r`, `--rules` | no | — | Path to a rules file. Without it, all records use the default account. |
|
|
63
|
+
| `-o`, `--output` | no | `output.beancount` | Output file path. |
|
|
64
|
+
| `--asset` | no | `Assets:CCB` / `Assets:WeChat` | Asset account for the source of funds. |
|
|
65
|
+
| `--default-account` | no | `Equity:Opening-Balances` | Fallback account when no rule matches. |
|
|
66
|
+
|
|
67
|
+
### Examples
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Bare minimum — everything goes to Equity:Opening-Balances
|
|
71
|
+
beancount-helper ccb.xls -p ccb
|
|
72
|
+
|
|
73
|
+
# With rules
|
|
74
|
+
beancount-helper wechat.xlsx -p wechat -r rules.txt
|
|
75
|
+
|
|
76
|
+
# Custom accounts
|
|
77
|
+
beancount-helper ccb.xls -p ccb -r rules.txt \
|
|
78
|
+
--asset Assets:CCB:Savings \
|
|
79
|
+
--default-account Expenses:Pending
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Rule system
|
|
83
|
+
|
|
84
|
+
Rules tell the tool which Beancount account a bill should be posted to. Each rule has this shape:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
condition1;;condition2;;...=>TargetAccount
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Conditions are AND-ed: a bill must satisfy **all** of them for the rule to fire. Rules are tried in order; the first match wins.
|
|
91
|
+
|
|
92
|
+
### Condition reference
|
|
93
|
+
|
|
94
|
+
#### Direction (`direct` / `direction`)
|
|
95
|
+
|
|
96
|
+
Matches whether money flows in or out.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
direct=in # income
|
|
100
|
+
direct=out # expense
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Property (`p` / `property`)
|
|
104
|
+
|
|
105
|
+
Matches a field on the bill record.
|
|
106
|
+
|
|
107
|
+
| Method | Syntax | Example |
|
|
108
|
+
|--------|--------|---------|
|
|
109
|
+
| exact | `p=field:eq:value` | `p=opposite:eq:Coffee Shop` |
|
|
110
|
+
| contains | `p=field:in:substring` | `p=note:in:lunch` |
|
|
111
|
+
| regex | `p=field:re:pattern` | `p=product:re:^ATMS$` |
|
|
112
|
+
|
|
113
|
+
Available fields: `opposite`, `note`, `product` (wechat only).
|
|
114
|
+
|
|
115
|
+
#### Amount comparisons
|
|
116
|
+
|
|
117
|
+
| Operator | Meaning |
|
|
118
|
+
|----------|---------|
|
|
119
|
+
| `lt=N` | amount < N |
|
|
120
|
+
| `le=N` | amount ≤ N |
|
|
121
|
+
| `gt=N` | amount > N |
|
|
122
|
+
| `ge=N` | amount ≥ N |
|
|
123
|
+
| `eq=N` | amount = N |
|
|
124
|
+
|
|
125
|
+
Negative amounts are expenses, positive are income (from the bank's perspective).
|
|
126
|
+
|
|
127
|
+
### Combining conditions
|
|
128
|
+
|
|
129
|
+
Separate conditions with `;;`:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
# Small lunch expenses from a specific vendor
|
|
133
|
+
direct=out;;p=opposite:in:Restaurant;;lt=50.0=>Expenses:Food:Lunch
|
|
134
|
+
|
|
135
|
+
# Any income
|
|
136
|
+
direct=in=>Income:Misc
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Rule file format
|
|
140
|
+
|
|
141
|
+
One rule per line. Blank lines and `#`-comments are ignored.
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
# Food & drinks
|
|
145
|
+
p=opposite:eq:Starbucks=>Expenses:Food:Coffee
|
|
146
|
+
p=opposite:in:Pizza;;lt=80.0=>Expenses:Food:Takeout
|
|
147
|
+
|
|
148
|
+
# Transport
|
|
149
|
+
p=opposite:eq:DiDi=>Expenses:Transport:Ride
|
|
150
|
+
p=note:re:metro|subway=>Expenses:Transport:Transit
|
|
151
|
+
|
|
152
|
+
# Income
|
|
153
|
+
direct=in;;gt=5000.0=>Income:Salary
|
|
154
|
+
direct=in=>Income:Misc
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## How it works
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Bank Excel (.xls/.xlsx)
|
|
161
|
+
│
|
|
162
|
+
▼
|
|
163
|
+
bill_resolver ← parses bank-specific formats into Bill objects
|
|
164
|
+
│
|
|
165
|
+
▼
|
|
166
|
+
transformer.rule ← matches each Bill against the rule set
|
|
167
|
+
│
|
|
168
|
+
▼
|
|
169
|
+
beancount.core ← assembles BeanCount transactions, dumps to text
|
|
170
|
+
│
|
|
171
|
+
▼
|
|
172
|
+
output.beancount
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Project structure
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
beancount-helper/
|
|
179
|
+
├── main.py CLI entry point
|
|
180
|
+
├── rules.example.txt Sample rule set
|
|
181
|
+
├── src/
|
|
182
|
+
│ ├── beancount/core.py BeanCount model + serialization
|
|
183
|
+
│ ├── bill_resolver/
|
|
184
|
+
│ │ ├── ccb.py CCB Excel parser
|
|
185
|
+
│ │ └── wechat.py WeChat Excel parser
|
|
186
|
+
│ └── transformer/rule.py Rule matching engine
|
|
187
|
+
└── test/
|
|
188
|
+
├── test_beancount_core.py
|
|
189
|
+
└── rule_matcher.py
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Install dev dependencies
|
|
196
|
+
uv sync
|
|
197
|
+
|
|
198
|
+
# Run tests
|
|
199
|
+
uv run pytest
|
|
200
|
+
|
|
201
|
+
# Lint
|
|
202
|
+
uv run ruff check .
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "beancount-helper"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Convert bank statement Excel files to Beancount format with a rule-based account matching engine."
|
|
5
|
+
license = "MIT"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = ["openpyxl>=3.1.5", "pandas>=3.0.3", "xlrd>=2.0.2"]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["hatchling"]
|
|
12
|
+
build-backend = "hatchling.build"
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["src/beancount", "src/bill_resolver", "src/transformer", "src/beancount_helper"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
beancount-helper = "beancount_helper.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
pythonpath = ["src"]
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = ["coverage>=7.15.2", "pytest>=9.1.1", "ruff>=0.15.22"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# beancount-helper 规则集
|
|
2
|
+
# 格式: 条件段1;;条件段2;;...=>目标账户
|
|
3
|
+
# 条件类型:
|
|
4
|
+
# direct=in|out 收支方向
|
|
5
|
+
# p=字段:eq|in|re:值 属性匹配(精确/包含/正则)
|
|
6
|
+
# lt|le|gt|ge|eq=数值 金额比较
|
|
7
|
+
#
|
|
8
|
+
# 示例:
|
|
9
|
+
# p=opposite:eq:一车一人=>Expenses:Trip:共享车:一车一人
|
|
10
|
+
# p=opposite:in:餐饮;;lt=50.0=>Expenses:Life:餐饮
|
|
11
|
+
# direct=in=>Income:Salary
|
|
12
|
+
|
|
13
|
+
p=opposite:eq:一车一人=>Expenses:Trip:共享车:一车一人
|
|
14
|
+
p=opposite:eq:享坐车出行=>Expenses:Trip:共享车:摆渡车
|
|
15
|
+
p=opposite:eq:灵川夜不将就餐饮店(个体工商户)=>Expenses:Life:餐饮
|
|
16
|
+
p=opposite:eq:灵川县这些年小炒店=>Expenses:Life:餐饮
|
|
17
|
+
p=opposite:eq:灵川县张兰食苑餐馆=>Expenses:Life:餐饮
|
|
18
|
+
p=opposite:eq:家乐轩餐厅丨灵川县=>Expenses:Life:餐饮
|
|
19
|
+
p=opposite:in:甘晋宇;;lt=20.0;;direct=in=>Expenses:Life:餐饮
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
_HEADER_PATTERN = re.compile(r'^(\d{4}-\d{2}-\d{2}) \* "(.*?)" "(.*?)"$')
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_beancounts(string: str) -> list["BeanCount"]:
|
|
8
|
+
# 去除空行
|
|
9
|
+
string = "\n".join([line.rstrip() for line in string.split("\n")])
|
|
10
|
+
|
|
11
|
+
blocks = [b.strip() for b in string.strip().split("\n\n") if b.strip()]
|
|
12
|
+
results: list["BeanCount"] = []
|
|
13
|
+
|
|
14
|
+
for block in blocks:
|
|
15
|
+
results.append(resolve_beancount(block))
|
|
16
|
+
|
|
17
|
+
return results
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_beancount(string: str) -> "BeanCount":
|
|
21
|
+
lines = [line.strip() for line in string.split("\n") if line.strip()]
|
|
22
|
+
if len(lines) < 3:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Beancount transaction block must be 3 lines for resolving, but {len(lines)} lines got from: {string}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# resolve header
|
|
28
|
+
match = _HEADER_PATTERN.match(lines[0])
|
|
29
|
+
if not match:
|
|
30
|
+
raise ValueError(f"Can't resolve beancount block: {string}")
|
|
31
|
+
|
|
32
|
+
date, receiver, digest = match.groups()
|
|
33
|
+
date = datetime.strptime(date, "%Y-%m-%d")
|
|
34
|
+
|
|
35
|
+
# resolve information
|
|
36
|
+
in_account, amount, currency_type = lines[1].split(" ")
|
|
37
|
+
out_account = lines[2]
|
|
38
|
+
|
|
39
|
+
return BeanCount(
|
|
40
|
+
datetime=date,
|
|
41
|
+
receiver=receiver,
|
|
42
|
+
digest=digest,
|
|
43
|
+
in_account=in_account,
|
|
44
|
+
out_account=out_account,
|
|
45
|
+
amount=float(amount),
|
|
46
|
+
currency_type=currency_type,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dump_beancount(beancount: "BeanCount") -> str:
|
|
51
|
+
return beancount.to_str()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def dump_beancounts(beancounts: list["BeanCount"]) -> str:
|
|
55
|
+
return "\n\n".join([b.to_str() for b in beancounts])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BeanCount:
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
datetime: datetime,
|
|
62
|
+
receiver: str,
|
|
63
|
+
digest: str,
|
|
64
|
+
in_account: str,
|
|
65
|
+
out_account: str,
|
|
66
|
+
amount: float,
|
|
67
|
+
currency_type: str,
|
|
68
|
+
):
|
|
69
|
+
self.datetime = datetime
|
|
70
|
+
self.receiver = receiver
|
|
71
|
+
self.digest = digest
|
|
72
|
+
self.in_account = in_account
|
|
73
|
+
self.out_account = out_account
|
|
74
|
+
self.amount = amount
|
|
75
|
+
self.currency_type = currency_type
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
date_str = self.datetime.strftime("%Y-%m-%d")
|
|
79
|
+
return (
|
|
80
|
+
f'{date_str} * "{self.receiver}" "{self.digest}"\n'
|
|
81
|
+
f" {self.in_account} {self.amount} {self.currency_type}\n"
|
|
82
|
+
f" {self.out_account}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __repr__(self) -> str:
|
|
86
|
+
return f'BeanCount(date={self.datetime.strftime("%Y-%m-%d")}, {self.in_account}->{self.out_account}({self.amount}), receiver: {self.receiver}({self.digest}))'
|
|
87
|
+
|
|
88
|
+
to_str = __str__
|
|
File without changes
|