Skip to content

Instantly share code, notes, and snippets.

@esseti
Last active June 20, 2023 10:12
Show Gist options
  • Save esseti/0f2687f065a1896c04525c0f46478a2d to your computer and use it in GitHub Desktop.
Save esseti/0f2687f065a1896c04525c0f46478a2d to your computer and use it in GitHub Desktop.
Payee to split categories YNAB
  • you need to have python (3)
  • install the requirements pip install -r requirements.txt
  • change the settings split.yaml accordingly to your needs
    • you either specify category: percentage or category. In the first case the percentage is used, otherwise the amount is splitted evenly among categories.
  • create a transaction with the payee of your choice (note: the script check if the string is contained, so if your payee is vacation and you have a transaction such as italy vacation this is matched)
  • run python split.py
  • in case you have problem with categories, try python split.py -c

Reddit discussion: https://www.reddit.com/r/ynab/comments/14e3t64/solution_script_to_automate_payee_to_split/

yaml
requests
cleantext
import logging
from uuid import uuid4
import yaml
import requests
import json
from cleantext import clean
logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
class SplitCategories(object):
def __init__(self, api_key, config):
self.api_token = api_key
self.config=config
def run(self,what=None):
all_categories = self.get_all_categories()
for budget, categories in self.config.items():
budget_id = self.find_budget_id(budget)
budget_categories = all_categories[budget_id]
logging.debug(f"Action:{what}")
if what == 'categories':
# this utils to test the settings/categories
for item in categories:
(payee, split_categories), = item.items()
for split_category in split_categories:
split_category=clean(split_category,no_emoji=True)
logging.debug(f"{split_category} - {str(budget_categories[split_category])}")
else:
transactions = self.get_all_transactions(budget_id)
# for all the settings that have categories
for item in categories:
# we get the data
(payee, split_categories), = item.items()
for transaction in transactions:
# if the transaction has the payee
# and is in our settings,
# we update the transaction
if not transaction['payee_name']:
continue
if payee in transaction['payee_name']:
if transaction['memo'] != 'parsed':
subtransactions = []
new_transaction = {
"account_id": transaction['account_id'],
"date": transaction['date'],
"payee_name": transaction['payee_name'],
"amount": transaction['amount'],
"memo": transaction['memo'],
"category_id": transaction['category_id'],
"cleared": "cleared",
"import_id": str(uuid4()), # we need a fake value
"payee_id": transaction['payee_id'],
"category_id": transaction['category_id'],
"approved": transaction['approved'],
# "flag_color": "red",
"memo":"parsed",
}
total = transaction['amount']
for index, split_category in enumerate(split_categories):
if type(split_category) == dict:
(k, v), = split_category.items()
logging.debug(f"{k},{v}")
# this to ensure that the sum of the sub categories
# matches with the total.
if index == len(split_categories)-1:
amount = total
else:
amount = int(transaction['amount']*v/100)
# this works only for outflow, inflow will probably break here.
total -= amount
name = k
else:
if index == len(split_categories)-1:
amount = total
else:
amount = int(transaction['amount']/len(split_categories))
total -= amount
name = split_category
name=clean(name,no_emoji=True)
category_id = budget_categories[name]
subtransactions.append({
# the amount is *100. so 2$ is 200
"amount": amount,
"payee_id": transaction['payee_id'],
# not needed
# "payee_name": transaction['payee_name'],
# "category_name": split_category,
"category_id": category_id,
"memo": ""
})
new_transaction['subtransactions']= subtransactions
self.put_transaction(budget_id, transaction['id'], {'transaction': new_transaction})
def put_transaction(self, budget_id,transaction_id, transaction):
"""
Update the transaction
"""
# send our data to API
logging.info("Uploading transactions to YNAB...")
url = (
"https://api.youneedabudget.com/v1/budgets/"
+ "{}/transactions/{}?access_token={}".format(
budget_id, transaction_id, self.api_token
)
)
logging.debug(json.dumps(transaction, indent=2))
post_response = requests.put(url, json=transaction)
# response handling - TODO: make this more thorough!
try:
self.process_api_response(json.loads(post_response.text)["error"])
except KeyError:
logging.info("Success!")
def find_budget_id(self, budget):
"""
gets id of a budget
"""
budgets = self.api_read(None, "budgets")
if budgets[0] == "ERROR":
return budgets
for budget_ynab in budgets:
logging.debug(f"{budget_ynab['name']} vs {budget}")
if budget_ynab['name']==budget:
return budget_ynab["id"]
return "ERROR"
def _parse_cartegories(self, group_categories):
"""
Util to return a dict where key is the category name
and value the id.
"""
res = {}
for group in group_categories:
for category in group['categories']:
res[clean(category['name'],no_emoji=True)]=category['id']
return res
def get_all_categories(self):
"""
Get all the categories for all the budegets
"""
res = {}
for budget, _ in self.config.items():
budget_id = self.find_budget_id(budget)
all_categories = self._get_all_categories_api(budget_id)
res[budget_id]=self._parse_cartegories(all_categories)
return res
def _get_all_categories_api(self, budget_id):
"""
Api call to get all transactions
"""
logging.debug(f"Budget {budget_id}")
categories = self.api_read(budget_id, "categories","category_groups")
if categories[0] == "ERROR":
return categories
if len(categories) > 0:
logging.debug("Listing categories:")
return categories
else:
logging.debug("no categories found")
def get_all_transactions(self, budget_id):
"""
Function to get all transactions
"""
logging.debug(f"Budget {budget_id}")
transactions = self.api_read(budget_id, "transactions")
if transactions[0] == "ERROR":
return transactions
if len(transactions) > 0:
logging.debug("Listing transactions:")
return transactions
else:
logging.debug("no transactions found")
def api_read(self, budget_id, kwd, kwd_ret=None):
"""
STOLEN FROM bank2ynab
General function for reading data from YNAB API
:param budget: boolean indicating if there's a default budget
:param kwd: keyword for data type, e.g. transactions
:param kwd_ret: the item to retrurn (some api do not match kwd / kwd_ret)
:return error_codes: if it fails we return our error
"""
api_t = self.api_token
base_url = "https://api.youneedabudget.com/v1/budgets/"
if budget_id is None:
# only happens when we're looking for the list of budgets
url = base_url + "?access_token={}".format(api_t)
else:
url = base_url + "{}/{}?access_token={}".format(
budget_id, kwd, api_t
)
logging.info(url)
response = requests.get(url)
try:
if not kwd_ret:
kwd_ret=kwd
read_data = response.json()["data"][kwd_ret]
except KeyError:
logging.error(response.json())
# the API has returned an error so let's handle it
return self.process_api_response(response.json()["error"])
return read_data
def process_api_response(self, details):
"""
STOLEN FROM bank2ynab
Prints details about errors returned by the YNAB api
:param details: dictionary of returned error info from the YNAB api
:return id: HTTP error ID
:return detail: human-understandable explanation of error
"""
# TODO: make this function into a general response handler instead
errors = {
"400": "Bad syntax or validation error",
"401": "API access token missing, invalid, revoked, or expired",
"403.1": "The subscription for this account has lapsed.",
"403.2": "The trial for this account has expired.",
"404.1": "The specified URI does not exist.",
"404.2": "Resource not found",
"409": "Conflict error",
"429": "Too many requests. Wait a while and try again.",
"500": "Unexpected error",
}
id = details["id"]
name = details["name"]
detail = errors[id]
logging.debug(details)
logging.error("{} - {} ({})".format(id, detail, name))
return ["ERROR", id, detail]
def categories(self):
print(api_read())
import argparse
parser = argparse.ArgumentParser(
prog='SplitTransaction',
description='What the program does',
epilog='Text at the bottom of help')
parser.add_argument('-c', '--categories', dest='categories',
action='store_true')
args = parser.parse_args()
if __name__ == "__main__":
import yaml
with open("split.yaml", 'r') as stream:
split_config = yaml.safe_load(stream)
api_key = split_config.pop('api_key')
split = SplitCategories(api_key, split_config)
if args.categories:
split.run('categories')
else:
split.run()
api_key: 6b5...............ff # your api key
Home: # your budget name
- split: #name of the payee
- eletricity: 50 #category name: percentage
- gas: 25 #category name: percentage
- rent: 25 #category name: percentage
- split flat: #name of the payee
- eletricity #category name
- gas #category name
- rent #category name
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment