From 9d18fef59df5489a0f4234845bac209581fc3283 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Sat, 23 Nov 2024 20:40:20 +0200 Subject: [PATCH 01/13] extend `addTransaction` signature to handle split transactions. --- main.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index e316dc8..8155748 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from splitwise.user import ExpenseUser from typing import Generator, TypedDict, Union from functools import wraps +from typing import Union import os import requests @@ -269,17 +270,24 @@ def updateTransaction(newTxn: dict, oldTxnBody: dict) -> None: print(f"Updated Transaction: {newTxn['description']}") -def addTransaction(newTxn: dict) -> None: +def addTransaction(newTxn: Union[dict, list[dict]], group_title=None) -> None: """ Add a transaction to Firefly. - :param newTxn: A dictionary of the transaction body. + + If newTxn is a dictionary, add a single transaction. If newTxn is a list of dictionaries, add a split transaction. + + :param newTxn: A dictionary of the transaction body, or a list of such dictionaries for a split transaction. + :param group_title: The title of the transaction group. If None, use the description of the first transaction. :return: None - :raises: Exception if the transaction addition fails + :raises: Exception if the transaction add fails. """ + + txns: list[dict] = [newTxn] if isinstance(newTxn, dict) else newTxn + group_title = group_title or txns[0]["description"] body = { "error_if_duplicate_hash": True, - "group_title": newTxn["description"], - "transactions": [newTxn] + "group_title": group_title, + "transactions": txns } try: callApi("transactions", method="POST", body=body).json() From ca5a1dc147f42cb4848ccb2331003652e82ecef1 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Sat, 23 Nov 2024 20:48:15 +0200 Subject: [PATCH 02/13] Add cover transaction to SW balance strategy --- strategies/sw_balance.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/strategies/sw_balance.py b/strategies/sw_balance.py index eeda7aa..307f0d6 100644 --- a/strategies/sw_balance.py +++ b/strategies/sw_balance.py @@ -10,25 +10,33 @@ def __init__(self, get_expense_transaction_body, sw_balance_account, apply_trans def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: txns = {} - paid_txn = self._get_expense_transaction_body(exp, myshare, data) - paid_txn = self._apply_transaction_amount(paid_txn, exp, myshare.getPaidShare()) - if float(paid_txn['amount']) != 0: # I paid; payment txn needed - txns['paid'] = paid_txn - - balance_txn = paid_txn.copy() + owed_txn = self._get_expense_transaction_body(exp, myshare, data) + description = owed_txn['description'] balance = float(myshare.getNetBalance()) + + # Create cover transaction + cover_txn = self._apply_transaction_amount(owed_txn.copy(), exp, balance) + cover_txn.update({ + 'description': f"Cover for: {description}", + 'category_name': '' + }) + + if float(owed_txn['amount']) != 0: # I paid; payment txn needed + txns['paid'] = [owed_txn, cover_txn] + + balance_txn = owed_txn.copy() if balance != 0: # I owe or am owed; balance txn needed txns['balance'] = balance_txn if balance > 0: # I am owed; difference credited to balance account balance_txn['source_name'] = self._sw_balance_account + " balancer" balance_txn['destination_name'] = self._sw_balance_account balance_txn['type'] = 'deposit' - balance_txn['description'] = f"Balance transfer for: {paid_txn['description']}" + balance_txn['description'] = f"Balance transfer for: {description}" balance_txn = self._apply_transaction_amount(balance_txn, exp, balance) else: # I owe; difference debited from balance account balance_txn['source_name'] = self._sw_balance_account - balance_txn['destination_name'] = paid_txn['destination_name'] + balance_txn['destination_name'] = owed_txn['destination_name'] balance_txn['type'] = "withdrawal" - balance_txn['description'] = f"Balance transfer for: {paid_txn['description']}" + balance_txn['description'] = f"Balance transfer for: {description}" balance_txn = self._apply_transaction_amount(balance_txn, exp, -balance) return list(txns.values()) \ No newline at end of file From dffdc40450df5817e907844eeed787411fe62e31 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Sat, 23 Nov 2024 20:55:51 +0200 Subject: [PATCH 03/13] minor refactor for readability --- strategies/sw_balance.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/strategies/sw_balance.py b/strategies/sw_balance.py index 307f0d6..d52ef26 100644 --- a/strategies/sw_balance.py +++ b/strategies/sw_balance.py @@ -20,23 +20,28 @@ def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str 'description': f"Cover for: {description}", 'category_name': '' }) - + if float(owed_txn['amount']) != 0: # I paid; payment txn needed txns['paid'] = [owed_txn, cover_txn] - balance_txn = owed_txn.copy() if balance != 0: # I owe or am owed; balance txn needed - txns['balance'] = balance_txn + balance_txn = owed_txn.copy() + balance_txn.update({ + 'description': f"Balance transfer for: {description}", + 'type': 'deposit' if balance > 0 else 'withdrawal' + }) + if balance > 0: # I am owed; difference credited to balance account - balance_txn['source_name'] = self._sw_balance_account + " balancer" - balance_txn['destination_name'] = self._sw_balance_account - balance_txn['type'] = 'deposit' - balance_txn['description'] = f"Balance transfer for: {description}" - balance_txn = self._apply_transaction_amount(balance_txn, exp, balance) + balance_txn.update({ + 'source_name': self._sw_balance_account + " balancer", + 'destination_name': self._sw_balance_account + }) else: # I owe; difference debited from balance account - balance_txn['source_name'] = self._sw_balance_account - balance_txn['destination_name'] = owed_txn['destination_name'] - balance_txn['type'] = "withdrawal" - balance_txn['description'] = f"Balance transfer for: {description}" - balance_txn = self._apply_transaction_amount(balance_txn, exp, -balance) + balance_txn.update({ + 'source_name': self._sw_balance_account, + 'destination_name': owed_txn['destination_name'] + }) + balance = -balance + balance_txn = self._apply_transaction_amount(balance_txn, exp, balance) + txns['balance'] = balance_txn return list(txns.values()) \ No newline at end of file From d7146ddbdb1ee08996ff87ccec00494a9938670e Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Sat, 23 Nov 2024 20:59:15 +0200 Subject: [PATCH 04/13] Update tests --- tests/test_strategies.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_strategies.py b/tests/test_strategies.py index 2b3987d..e9390e9 100644 --- a/tests/test_strategies.py +++ b/tests/test_strategies.py @@ -52,8 +52,11 @@ def test_sw_balance_strategy(): transactions = strategy.create_transactions(mock_expense, mock_user, []) assert len(transactions) == 2 - assert transactions[0]["amount"] == "110.00" - assert transactions[0]["description"] == "Test Expense" + assert transactions[0][0]["amount"] == "60.00" + assert transactions[0][0]["description"] == "Test Expense" + assert float(transactions[0][1]["amount"]) == float("50.00") + assert transactions[0][1]["description"] == "Cover for: Test Expense" + assert transactions[0][1]["category_name"] == "" assert float(transactions[1]["amount"]) == float("50.00") assert transactions[1]["type"] == "deposit" assert transactions[1]["destination_name"] == "Splitwise Balance" From 163365677f122cbcbea328fcf3068c56d1015ec0 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 19:54:01 +0200 Subject: [PATCH 05/13] Update readme with changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc0139c..2521aeb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ When enabled, tracks Splitwise payable and receivable debts in an account define For example, assume you paid 100$ but your share was only 40$. Splitwise records correctly that you are owed 60$ - so your total assets haven't really decreased by 100$, only by 40$. Enabling this feature correctly tracks this in Firefly, without compromising on recording the real 100$ transaction you will see in your bank statement. For each Splitwise expense, create two Firefly transactions: -1. A withdrawal from a real account, recording the real amount of money paid in the expense +1. A withdrawal from a real account, recording the real amount of money paid in the expense. The withdrawal is split in two parts - the owed amount and the remainder. This allows, for example, to assign only the owed part to a budget. 2. A deposit to the `SW_BALANCE_ACCOUNT` equal the difference between the amount paid and the amount owed. ## Note/Comment format From 6195e948d72e0ae1390245ea46b360a2935bf87b Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:33:46 +0200 Subject: [PATCH 06/13] Handle split txns in processExpense --- main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 8155748..41ad1f4 100644 --- a/main.py +++ b/main.py @@ -310,12 +310,16 @@ def processExpense(past_day: datetime, txns: dict[dict], exp: Expense, *args) -> """ strategy = get_transaction_strategy() - new_txns: list[dict] = strategy.create_transactions(exp, *args) + new_txns: list = strategy.create_transactions(exp, *args) for idx, new_txn in enumerate(new_txns): external_url = getSWUrlForExpense(exp) if idx > 0: external_url += f"-balance_transfer-{idx}" - new_txn["external_url"] = external_url + if isinstance(new_txn, dict): + new_txn["external_url"] = external_url + else: + for split in new_txn: + split["external_url"] = external_url if oldTxnBody := txns.get(external_url): print(f"Updating transaction {idx + 1}...") From 0fdababd1ade6ca8925435b8648e39d632af9450 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:34:45 +0200 Subject: [PATCH 07/13] Generalize `create_transactions` signature to handle split transactions. --- strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strategies/base.py b/strategies/base.py index 2f64e8e..18dfa87 100644 --- a/strategies/base.py +++ b/strategies/base.py @@ -4,5 +4,5 @@ class TransactionStrategy(ABC): @abstractmethod - def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: + def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list: pass \ No newline at end of file From 6d8b7c89e8c3da412da92e1629d118f906ebe736 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:36:39 +0200 Subject: [PATCH 08/13] doc --- strategies/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/strategies/base.py b/strategies/base.py index 18dfa87..e2646e5 100644 --- a/strategies/base.py +++ b/strategies/base.py @@ -5,4 +5,11 @@ class TransactionStrategy(ABC): @abstractmethod def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list: + """ + Create transactions for the given expense and user's share of the expense. + + :param exp: Expense to create transactions from + :param myshare: ExpenseUser object representing the user's share in the expense + :param data: List of strings containing additional data for the transaction + """ pass \ No newline at end of file From 9b1b3c93e492a2238a5cb42543eb8c9ed8c2f7cf Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:43:51 +0200 Subject: [PATCH 09/13] doc --- strategies/standard.py | 16 ++++++++++++++++ strategies/sw_balance.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/strategies/standard.py b/strategies/standard.py index 745cf2a..ba2f05d 100644 --- a/strategies/standard.py +++ b/strategies/standard.py @@ -4,7 +4,23 @@ class StandardTransactionStrategy(TransactionStrategy): def __init__(self, get_expense_transaction_body) -> None: + """ + Initialize the StandardTransactionStrategy with the function to get the transaction body. + + :param get_expense_transaction_body: Function to get the transaction body for the expense. Must take the expense, user's share, and additional data as arguments. + """ + self._get_expense_transaction_body = get_expense_transaction_body def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: + """ + Create a transaction for the given expense and user's share of the expense. + + Create a single transaction for the expense using the provided function to get the transaction from the expense, user's share, and additional data. + + :param exp: Expense to create transactions from + :param myshare: ExpenseUser object representing the user's share in the expense + :param data: List of strings containing additional data for the transaction + """ + return [self._get_expense_transaction_body(exp, myshare, data)] \ No newline at end of file diff --git a/strategies/sw_balance.py b/strategies/sw_balance.py index d52ef26..96cb3ff 100644 --- a/strategies/sw_balance.py +++ b/strategies/sw_balance.py @@ -4,11 +4,31 @@ class SWBalanceTransactionStrategy(TransactionStrategy): def __init__(self, get_expense_transaction_body, sw_balance_account, apply_transaction_amount) -> None: + """ + Initialize the SWBalanceTransactionStrategy. + + :param get_expense_transaction_body: Function to get the transaction body for the expense. Must take the expense, user's share, and additional data as arguments. + :param sw_balance_account: Name of the Splitwise balance account for the user. + :param apply_transaction_amount: Function to apply the transaction amount to the transaction body. Must take the transaction body, expense, and amount as arguments. + """ + self._get_expense_transaction_body = get_expense_transaction_body self._sw_balance_account = sw_balance_account self._apply_transaction_amount = apply_transaction_amount def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: + """ + Create transactions for the given expense and user's share of the expense. + + Create transactions for the expense using the provided function to get the transaction from the expense, user's share, and additional data. + If the user paid for the expense, create a payment withdrawal transaction and a cover deposit transaction to the Splitwise balance. Split the payment transaction to the owed amount and the cover amount. + If the user owes money for the expense, create a balance transfer withdrawal transaction from the Splitwise balance account. + + :param exp: Expense to create transactions from + :param myshare: ExpenseUser object representing the user's share in the expense + :param data: List of strings containing additional data for the transaction + """ + txns = {} owed_txn = self._get_expense_transaction_body(exp, myshare, data) description = owed_txn['description'] From aee2c3cf5fd0f86847272ae5ad733cc45f3a40ae Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:46:17 +0200 Subject: [PATCH 10/13] bugfix --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 41ad1f4..1204ba9 100644 --- a/main.py +++ b/main.py @@ -295,7 +295,7 @@ def addTransaction(newTxn: Union[dict, list[dict]], group_title=None) -> None: print( f"Transaction {newTxn['description']} errored, body: {body}, e: {e}") raise - print(f"Added Transaction: {newTxn['description']}") + print(f"Added Transaction: {group_title}") def processExpense(past_day: datetime, txns: dict[dict], exp: Expense, *args) -> None: From f003666b051e99728e97d34b2fca01aaeb448d33 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 20:52:53 +0200 Subject: [PATCH 11/13] bugfix --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 1204ba9..8949c1c 100644 --- a/main.py +++ b/main.py @@ -293,7 +293,7 @@ def addTransaction(newTxn: Union[dict, list[dict]], group_title=None) -> None: callApi("transactions", method="POST", body=body).json() except Exception as e: print( - f"Transaction {newTxn['description']} errored, body: {body}, e: {e}") + f"Transaction {group_title} errored, body: {body}, e: {e}") raise print(f"Added Transaction: {group_title}") From 7c2043e4c1970fbaa5a5f1a04e87b96a9a52bd9c Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 21:02:07 +0200 Subject: [PATCH 12/13] bugfix: skip withdrawal transactions if I didn't pay --- strategies/sw_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strategies/sw_balance.py b/strategies/sw_balance.py index 96cb3ff..de04a1b 100644 --- a/strategies/sw_balance.py +++ b/strategies/sw_balance.py @@ -41,7 +41,7 @@ def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str 'category_name': '' }) - if float(owed_txn['amount']) != 0: # I paid; payment txn needed + if float(myshare.getPaidShare()) != 0: # I paid; payment txn needed txns['paid'] = [owed_txn, cover_txn] if balance != 0: # I owe or am owed; balance txn needed From 49725bc2b028e506cb8603a39aad31dbab023f63 Mon Sep 17 00:00:00 2001 From: "LAPTOP-8G8AGL8G\\yotam" Date: Tue, 3 Dec 2024 21:24:39 +0200 Subject: [PATCH 13/13] Remove categorization of SW balance transfers --- strategies/sw_balance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/strategies/sw_balance.py b/strategies/sw_balance.py index de04a1b..4727c75 100644 --- a/strategies/sw_balance.py +++ b/strategies/sw_balance.py @@ -48,7 +48,8 @@ def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str balance_txn = owed_txn.copy() balance_txn.update({ 'description': f"Balance transfer for: {description}", - 'type': 'deposit' if balance > 0 else 'withdrawal' + 'type': 'deposit' if balance > 0 else 'withdrawal', + 'category_name': '' }) if balance > 0: # I am owed; difference credited to balance account