Skip to content

Commit

Permalink
v0.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rlan committed Jun 11, 2024
1 parent 8a94dc6 commit 1bd2f6f
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The following financial institutions are supported:
* [Rakuten Bank 楽天銀行](https://www.rakuten-bank.co.jp/)
* [SBI Shinsei Bank 新生銀行](https://www.sbishinseibank.co.jp/)
* [SBI Sumishin Net Bank 住信SBIネット銀行](https://www.netbk.co.jp/)
* USA
* [Chase Sapphire Preferred Card](https://www.chase.com/)

To get started, see the [documentation](https://rlan.github.io/beancount-multitool).

Expand Down
32 changes: 32 additions & 0 deletions docs/institutions/chase_sp_card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Chase Sappire Preferred VISA Card

[https://www.chase.com/](https://www.chase.com/)

## How to download transactions

TODO

## CSV file

Header row:

```csv
Transaction Date,Post Date,Description,Category,Type,Amount,Memo
```

### Regular expressions

Regular expressions uses `Description` for matching.

## Example: label all transactions as default

[One](https://github.com/rlan/beancount-multitool/tree/main/tests/data/chase_sp_card) of the automated tests does exactly this. Let's download and run it locally.

```sh
wget https://raw.githubusercontent.com/rlan/beancount-multitool/main/tests/data/chase_sp_card/config.toml
wget https://raw.githubusercontent.com/rlan/beancount-multitool/main/tests/data/chase_sp_card/credit_mapping.toml
wget https://raw.githubusercontent.com/rlan/beancount-multitool/main/tests/data/chase_sp_card/debit_mapping.toml
wget https://raw.githubusercontent.com/rlan/beancount-multitool/main/tests/data/chase_sp_card/test.bean
wget https://raw.githubusercontent.com/rlan/beancount-multitool/main/tests/data/chase_sp_card/test.csv
bean-mt chase_sp_card config.toml test.csv --output out.bean
```
6 changes: 6 additions & 0 deletions docs/institutions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ title: Supported institutions

Here is a list of financial institutions whose CSV files the CLI tool can read:

Japan

* [JA Bank JAネットバンク](ja_bank.md)
* [Rakuten Bank 楽天銀行](rakuten_bank.md)
* [Rakuten Card 楽天カード](rakuten_card.md)
* [SBI Shinsei Bank 新生銀行](shinsei_bank.md)
* [SBI Sumishin Net Bank 住信SBIネット銀行](sumishin_net_bank.md)

USA

* [Chase Sapphire Preferred Card](chase_sp_card.md)
4 changes: 4 additions & 0 deletions docs/reference/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

0.7.0

* Supports Chase Sapphire Preferred Card.

0.6.1

* Typing errors in Python 3.9.
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nav:
- usage/examples.md
- Financial Institutions:
- institutions/index.md
- institutions/chase_sp_card.md
- institutions/ja_bank.md
- institutions/rakuten_bank.md
- institutions/rakuten_card.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "beancount-multitool"
version = "0.6.1"
version = "0.7.0"
description = "A CLI tool that converts financial data to Beancount files"
authors = ["Rick Lan <[email protected]>"]
license = "MIT"
Expand Down
138 changes: 138 additions & 0 deletions src/beancount_multitool/ChaseSPCard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from decimal import Decimal
from pathlib import Path

import pandas as pd

from .Institution import Institution
from .MappingDatabase import MappingDatabase
from .read_config import read_config
from .as_transaction import as_transaction
from .get_value import get_value
from .get_beancount_config import get_beancount_config


class ChaseSPCard(Institution):
NAME = "chase_sp_card" # used in cli.py and in tests

def __init__(self, config_file: str):
# params
self.config_file = config_file
# attributes
self.config = read_config(config_file)
self.beancount_config = get_beancount_config(self.config)
# Use basedir of config_file to read mapping database files
base_dir = Path(config_file).parent
debit_file = get_value(self.config, "database", "debit_mapping")
self.debit_file = str(base_dir / debit_file)
self.debit_db = MappingDatabase(self.debit_file)

def read_transaction(self, file_name: str) -> pd.DataFrame:
"""Read financial transactions into a Pandas DataFrame.
Parameters
----------
file_name : str
Input file name.
Returns
-------
pd.DataFrame
A dataframe after pre-processing.
"""
converters = {
"Transaction Date": pd.to_datetime,
"Post Date": pd.to_datetime,
"Description": str,
"Category": str,
"Type": str,
"Amount": str,
"Memo": str,
}
df = pd.read_csv(file_name, converters=converters)
print(f"Found {len(df.index)} transactions in {file_name}")

# Transaction Date,Post Date,Description,Category,Type,Amount,Memo
# Lowercase names will be keyword arguments later.
column_names = {
"Transaction Date": "date",
"Amount": "amount",
"Description": "memo", # note this is all lower case
}
df.rename(columns=column_names, inplace=True)

df["amount"] = df["amount"].apply(Decimal)
# Drop positive amounts as they are credit card payments
df.drop(df.loc[df["amount"] > 0].index, inplace=True)
# Reverse sign as all transactions are now spending.
df["amount"] = -df["amount"]

# Reverse row order because the oldest transaction is on the bottom
# Note: the index column is also reversed
df = df[::-1]

# print(df.dtypes) # debug
# print(df) # debug
return df

def write_bean(self, df: pd.DataFrame, file_name: str) -> None:
"""Write Beancount transactions to file
Parameters
----------
df : pd.DataFrame
Transaction dataframe.
file_name : str
Output file name.
Returns
-------
None
"""
try:
with open(file_name, "w", encoding="utf-8") as f:
for row in df.index:
date = df["date"][row]
amount = df["amount"][row]
memo = df["memo"][row]
metadata = {
"memo": memo,
}

accounts = self.debit_db.match(memo)

account_metadata = {}
for x in range(1, len(accounts)):
account_metadata[f"match{x+1}"] = str(accounts[x])

output = as_transaction(
date=date,
amount=amount,
metadata=metadata,
account_metadata=account_metadata,
**accounts[0],
**self.beancount_config,
)
# print(output) # debug
f.write(output)
print(f"Written {file_name}")
except IOError as e:
print(f"Error encountered while writing to: {file_name}")
print(e)

def convert(self, csv_file: str, bean_file: str):
"""Convert transactions in a CSV file to a Beancount file
Parameters
----------
csv_file : str
Input CSV file name.
bean_file : str
Output Beancount file name.
Returns
-------
None
"""
df = self.read_transaction(csv_file)
self.write_bean(df, bean_file)
2 changes: 2 additions & 0 deletions src/beancount_multitool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

# from .as_transaction import as_transaction
# from .read_config import read_config
from .ChaseSPCard import ChaseSPCard
from .JABank import JABank
from .RakutenBank import RakutenBank
from .RakutenCard import RakutenCard
from .ShinseiBank import ShinseiBank
from .SumishinNetBank import SumishinNetBank

__INSTITUTIONS__ = []
__INSTITUTIONS__.append(ChaseSPCard.NAME)
__INSTITUTIONS__.append(JABank.NAME)
__INSTITUTIONS__.append(RakutenBank.NAME)
__INSTITUTIONS__.append(RakutenCard.NAME)
Expand Down
2 changes: 1 addition & 1 deletion src/beancount_multitool/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6.1"
__version__ = "0.7.0"
5 changes: 4 additions & 1 deletion src/beancount_multitool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Get available finanicial institutions
from beancount_multitool import __INSTITUTIONS__

from beancount_multitool import ChaseSPCard
from beancount_multitool import JABank
from beancount_multitool import RakutenBank
from beancount_multitool import RakutenCard
Expand Down Expand Up @@ -40,7 +41,9 @@ def main(name: str, config: str, data: str, output: str):
DATA is the raw financial data downloaded from NAME, e.g. input.csv.
"""
if name == JABank.NAME:
if name == ChaseSPCard.NAME:
tool = ChaseSPCard(config)
elif name == JABank.NAME:
tool = JABank(config)
elif name == RakutenBank.NAME:
tool = RakutenBank(config)
Expand Down
9 changes: 9 additions & 0 deletions tests/data/chase_sp_card/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Config for Chase Sapphire Preferred Card

[beancount]
currency = "USD"
source_account = "Liabilities:US:ChaseSapphirePreferredCard"

[database]
# File path is relative to current file
debit_mapping = "debit_mapping.toml"
19 changes: 19 additions & 0 deletions tests/data/chase_sp_card/debit_mapping.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Debit mapping database for Chase Sapphire Preferred Card

[default]
regex = ""
account = "Expenses:Unknown:ChaseSapphirePreferredCard"
payee = "Unknown payee"
narration = ""
tags = ["#NoMatch"]
flag = "!"

[blank]
regex = ""
account = ""
payee = ""
narration = ""
tags = []
flag = ""

# List mappings below
5 changes: 5 additions & 0 deletions tests/data/chase_sp_card/test.bean
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

2024-04-17 * "Unknown payee" "" #NoMatch
memo: "APPLE.COM/BILL"
Liabilities:US:ChaseSapphirePreferredCard
! Expenses:Unknown:ChaseSapphirePreferredCard 0.99 USD
3 changes: 3 additions & 0 deletions tests/data/chase_sp_card/test.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Transaction Date,Post Date,Description,Category,Type,Amount,Memo
04/26/2024,04/26/2024,AUTOMATIC PAYMENT - THANK,,Payment,0.99,
04/17/2024,04/17/2024,APPLE.COM/BILL,Shopping,Sale,-0.99,
4 changes: 3 additions & 1 deletion tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def assets(tmp_path, request):


def test_data(assets):
if assets["name"] == bcmt.JABank.NAME:
if assets["name"] == bcmt.ChaseSPCard.NAME:
app = bcmt.ChaseSPCard(assets["config_file"])
elif assets["name"] == bcmt.JABank.NAME:
app = bcmt.JABank(assets["config_file"])
elif assets["name"] == bcmt.RakutenBank.NAME:
app = bcmt.RakutenBank(assets["config_file"])
Expand Down

0 comments on commit 1bd2f6f

Please sign in to comment.