Converting a GnuCash budget to Actual
Warning
The following example is not guaranteed to work, and you might need to change some parameters to make it work for your use case.
When I first migrated to Actual, I was using GnuCash to manage my budget. I then started writing actualpy as a way to automate this migration, since at the time it seemed like it was going to be straightforward: read the GnuCash data with the pre-existing Python library, then import everything into Actual using HTTP requests.
This led me on an entire journey of re-engineering the Actual protocol, and trying to provide a high-quality API for the Python community.
If you still want to use this, here is the original code, without any warranty that this will actually work.
import datetime
import decimal
import pathlib
import piecash
from actual import Actual
from actual.queries import (
create_transaction,
create_transfer,
get_or_create_account,
get_or_create_category,
get_or_create_payee,
)
def insert_transaction(
session, account_source: str, expense_source: str, notes: str, date: datetime.date, value: decimal.Decimal
):
# do inserts, if it's an expense or transfer
payee = get_or_create_payee(session, "") # payee is non-existing on gnucash
if account_source.startswith("Assets:") and expense_source.startswith("Expenses:"):
account = get_or_create_account(session, account_source.replace("Assets:", ""))
group_name, _, category_name = expense_source.partition(":")
category = get_or_create_category(session, category_name, group_name)
create_transaction(session, date, account, payee, notes, category, -value)
elif account_source.startswith("Income:") and expense_source.startswith("Assets:"):
expense = get_or_create_account(session, expense_source.replace("Assets:", ""))
group_name, _, category_name = account_source.partition(":")
category = get_or_create_category(session, category_name, group_name)
create_transaction(session, date, expense, payee, notes, category, value)
elif account_source.startswith("Assets:") and expense_source.startswith("Assets:"):
account = get_or_create_account(session, account_source.replace("Assets:", ""))
expense = get_or_create_account(session, expense_source.replace("Assets:", ""))
session.flush()
# transfer between accounts
if value < 0:
# reverse everything
account, expense, value = expense, account, -value
create_transfer(session, date, account, expense, value, notes)
else:
print(f"Could not parse transaction '{account_source}' to '{expense_source}', '{notes}', {value}")
session.flush()
def parse_transaction(session, transaction: piecash.Transaction):
notes: str = transaction.description
date: datetime.date = transaction.post_date
expense_source: str = transaction.splits[0].account.fullname
account_source: str = transaction.splits[1].account.fullname
value: decimal.Decimal = transaction.splits[0].quantity
# swap around if it's a transfer back, set it with negative value
if (account_source.startswith("Expenses:") and expense_source.startswith("Assets:")) or (
account_source.startswith("Assets:") and expense_source.startswith("Income:")
):
expense_source, account_source, value = account_source, expense_source, -value
# create accounts for assets
insert_transaction(session, account_source, expense_source, notes, date, value)
def main():
with Actual(password="mypass", bootstrap=True) as actual:
actual.create_budget("Gnucash Import")
# go through files from gnucash and find all that match .gnucash extension
path = pathlib.Path(__file__).parent / "files/"
for file in path.rglob("*.gnucash"):
book = piecash.open_book(str(file.absolute()), readonly=True)
for transaction in book.transactions:
if len(transaction.splits) > 2:
print(
f"Could not parse transaction {transaction.guid}. "
"Please, make sure you support splits manually"
)
continue
# for the actual transaction, get account in and out
parse_transaction(actual.session, transaction)
# if everything goes well we upload our budget
actual.session.commit()
actual.upload_budget()
if __name__ == "__main__":
main()