Metadata-Version: 2.4
Name: beancount-helper
Version: 0.2.1
Summary: Convert bank statement Excel files to Beancount format with a rule-based account matching engine.
Project-URL: Repository, https://github.com/FishCat233/beancount-helper
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: openpyxl>=3.1.5
Requires-Dist: pandas>=3.0.3
Requires-Dist: xlrd>=2.0.2
Description-Content-Type: text/markdown

# beancount-helper

[中文文档](README-zh_cn.md)

Convert bank statement Excel files into [Beancount](https://beancount.github.io/) transactions with a rule-based account matching engine.

```
2025-01-15 * "Coffee Shop" "Latte"
  Expenses:Food:Coffee  25.00 CNY
  Assets:CCB
```

## Supported banks

| Parser | Source |
|--------|--------|
| `ccb` | China Construction Bank (建设银行) statement `.xls` |
| `wechat` | WeChat Pay (微信支付) statement `.xlsx` |

## Installation

```bash
pip install beancount-helper
```

For development:

```bash
git clone https://github.com/FishCat233/beancount-helper.git
cd beancount-helper
uv sync
```

## Quick start

**Step 1** — Export your statement from the bank app and save it locally.

**Step 2** — (Optional) Create a rules file:

```
# rules.txt
p=opposite:eq:Coffee Shop;;direct=out=>Expenses:Food:Coffee
direct=in=>Income:Misc
```

**Step 3** — Run:

```bash
beancount-helper ccb.xls -p ccb -r rules.txt -o ledger.beancount
```

That's it. Every record falls into a Beancount account — either matched by your rules, or placed in `Equity:Opening-Balances` as a fallback.

## CLI reference

```
beancount-helper INPUT -p PARSER [-r RULES] [-o OUTPUT] [--asset ASSET] [--default-account ACCOUNT]
```

| Argument | Required | Default | Description |
|----------|----------|---------|-------------|
| `INPUT` | yes | — | Path to the bank statement file (`.xls` / `.xlsx`). |
| `-p`, `--parser` | yes | — | `ccb` or `wechat`. |
| `-r`, `--rules` | no | — | Path to a rules file. Can be specified multiple times to load from several files. Without it, all records use the default account. |
| `-o`, `--output` | no | `output.beancount` | Output file path. |
| `--asset` | no | `Assets:CCB` / `Assets:WeChat` | Asset account for the source of funds. |
| `--default-account` | no | `Equity:Opening-Balances` | Fallback account when no rule matches. |

### Examples

```bash
# Bare minimum — everything goes to Equity:Opening-Balances
beancount-helper ccb.xls -p ccb

# With rules
beancount-helper wechat.xlsx -p wechat -r rules.txt

# Multiple rules files
beancount-helper ccb.xls -p ccb -r food.txt -r transport.txt -r income.txt

# Custom accounts
beancount-helper ccb.xls -p ccb -r rules.txt \
  --asset Assets:CCB:Savings \
  --default-account Expenses:Pending
```

## Rule system

Rules tell the tool which Beancount account a bill should be posted to. Each rule has this shape:

```
condition1;;condition2;;...=>TargetAccount
```

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.

### Condition reference

#### Direction (`direct` / `direction`)

Matches whether money flows in or out.

```
direct=in       # income
direct=out      # expense
```

#### Property (`p` / `property`)

Matches a field on the bill record. The syntax is:

```
p=field:method:value
```

**Available fields**

| Field | Description | CCB | WeChat |
|-------|-------------|-----|--------|
| `opposite` | Counterparty name (parsed) | ✓ | ✓ |
| `raw_opposite` | Raw counterparty as in the statement | ✓ | — |
| `note` | Remarks / memo | ✓ | ✓ |
| `product` | Product description | — | ✓ |

**Methods**

| Method | Meaning | Example |
|--------|---------|---------|
| `eq` | Field equals the value exactly | `p=opposite:eq:Starbucks` |
| `in` | Field contains the substring | `p=note:in:refund` |
| `re` | Field matches the regex pattern | `p=opposite:re:^ATM\d+$` |

**Examples**

```
# Match an exact merchant name
p=opposite:eq:China Mobile=>Expenses:Utilities:Phone

# Match anything with "restaurant" in the note
p=note:in:restaurant=>Expenses:Food:Dining

# Match product descriptions starting with "JD-"
p=product:re:^JD-=>Expenses:Shopping:Online

# Combine with other conditions
p=opposite:eq:DiDi;;direct=out=>Expenses:Transport:Ride
```

#### Amount comparisons

| Operator | Meaning |
|----------|---------|
| `lt=N` | amount < N |
| `le=N` | amount ≤ N |
| `gt=N` | amount > N |
| `ge=N` | amount ≥ N |
| `eq=N` | amount = N |

Negative amounts are expenses, positive are income (from the bank's perspective).

### Combining conditions

Separate conditions with `;;`:

```
# Small lunch expenses from a specific vendor
direct=out;;p=opposite:in:Restaurant;;lt=50.0=>Expenses:Food:Lunch

# Any income
direct=in=>Income:Misc
```

### Rule file format

One rule per line. Blank lines and `#`-comments are ignored.

```
# Food & drinks
p=opposite:eq:Starbucks=>Expenses:Food:Coffee
p=opposite:in:Pizza;;lt=80.0=>Expenses:Food:Takeout

# Transport
p=opposite:eq:DiDi=>Expenses:Transport:Ride
p=note:re:metro|subway=>Expenses:Transport:Transit

# Income
direct=in;;gt=5000.0=>Income:Salary
direct=in=>Income:Misc
```

## How it works

```
Bank Excel (.xls/.xlsx)
        │
        ▼
  bill_resolver       ← parses bank-specific formats into Bill objects
        │
        ▼
  transformer.rule    ← matches each Bill against the rule set
        │
        ▼
  beancount.core      ← assembles BeanCount transactions, dumps to text
        │
        ▼
  output.beancount
```

## Project structure

```
beancount-helper/
├── src/beancount_helper/
│   ├── cli.py                    CLI entry point
│   ├── beancount/core.py         BeanCount model + serialization
│   ├── bill_resolver/
│   │   ├── ccb.py                CCB Excel parser
│   │   └── wechat.py             WeChat Excel parser
│   └── transformer/rule.py       Rule matching engine
├── rules.example.txt             Sample rule set
└── test/
    ├── test_beancount_core.py
    └── test_rule_matcher.py
```

## Development

```bash
# Clone & editable install
git clone https://github.com/FishCat233/beancount-helper.git
cd beancount-helper
uv sync

# Run tests
uv run pytest

# Lint
uv run ruff check .
```

## License

MIT
