Skip to content

Commit ffbf8cd

Browse files
committed
Add auto-detect for transfers
- Auto-detect for transfers between accounts (accounts must be enabled) - Add response display in get/post requests - improve tools methods
1 parent a708ec4 commit ffbf8cd

File tree

3 files changed

+139
-37
lines changed

3 files changed

+139
-37
lines changed

firefly3.py

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ def __init__(self):
2121
self.token = PERSONAL_TOKEN_DEFAULT
2222
self.hostname = HOSTNAME_DEFAULT
2323
self.name_format = ACCOUNTS_NAME_FORMAT_DEFAULT
24+
self.auto_detect_transfers = bool(AUTO_DETECT_TRANSFERS_DEFAULT)
25+
self.transfer_source_transaction = TRANSFER_SOURCE_TRANSACTION_NAME_DEFAULT.strip().split(",")
26+
self.transfer_destination_transaction = TRANSFER_DESTINATION_TRANSACTION_NAME_DEFAULT.strip().split(",")
2427
self.headers = None
2528

2629
def _post(self, endpoint, payload):
2730
response = requests.post("{}{}".format(self.hostname, endpoint), json=payload, headers=self.headers)
2831
if response.status_code != 200:
29-
raise ValueError("Request to your Firefly3 instance failed. Please double check your personal token.")
32+
raise ValueError("Request to your Firefly3 instance failed. Please double check your personal token. Error : " + str(response.json()))
3033
return response.json()
3134

3235
def _get(self, endpoint, params=None):
3336
response = requests.get("{}{}".format(self.hostname, endpoint), params=params, headers=self.headers)
3437
if response.status_code != 200:
35-
raise ValueError("Request to your Firefly3 instance failed. Please double check your personal token.")
38+
raise ValueError("Request to your Firefly3 instance failed. Please double check your personal token. Error : " + str(response.json()))
3639
return response.json()
3740

3841
def validate(self):
@@ -110,14 +113,15 @@ def __init__(self, f3_cli, account_id):
110113
self.f3_cli = f3_cli
111114
self.account_id = int(account_id)
112115
self.payloads = []
113-
self.transfer_out = [] # [date, montant]
114-
self.transfer_in = [] # [date, montant]
115-
# --> On extrait pour chaque compte, une liste unique de dates. Et pour chacunes de ces dates on regarde,
116-
# quel compte a emis, quel compte a reçu. Une fois qu'on les a, on crée le transfert (add_transfert?) et pas oublier
117-
# de supprimer les 2 transactions du payload (ou alors juste on ne les ajotue pas au payload dans ce genre de cas
116+
self.transfer_out = {}
117+
self.transfer_in = {}
118118

119119
def __len__(self):
120-
return len(self.payloads)
120+
count = 0
121+
for transfer_list in [self.transfer_in, self.transfer_out]:
122+
for date in transfer_list:
123+
count = count + len(transfer_list[date])
124+
return len(self.payloads) + count
121125

122126
def add_transaction(self, ca_payload):
123127
payload = {"transactions": [{}]}
@@ -128,22 +132,12 @@ def add_transaction(self, ca_payload):
128132
renames = get_key_from_value(self.f3_cli.a_rename_transaction, transaction_name)
129133
transaction["description"] = renames[0] if len(renames) > 0 else transaction_name
130134

131-
date = time.mktime(time.strptime(ca_payload["dateOperation"], '%b %d, %Y %H:%M:%S %p'))
132-
transaction["date"] = time.strftime("%Y-%m-%d", time.gmtime(date))
135+
date = time.mktime(time.strptime(ca_payload["dateOperation"], '%b %d, %Y, %H:%M:%S %p'))
136+
transaction["date"] = time.strftime("%Y-%m-%dT%T", time.gmtime(date))
133137

134138
transaction["amount"] = abs(ca_payload["montant"])
135139
transaction["currency_code"] = ca_payload["idDevise"]
136140

137-
accounts = get_key_from_value(self.f3_cli.aa_account, transaction_name)
138-
if ca_payload["montant"] > 0:
139-
transaction["type"] = "deposit"
140-
transaction["source_name"] = accounts[0] if len(accounts) > 0 else "Cash account"
141-
transaction["destination_id"] = self.account_id
142-
else:
143-
transaction["type"] = "withdrawal"
144-
transaction["source_id"] = self.account_id
145-
transaction["destination_name"] = accounts[0] if len(accounts) > 0 else "Cash account"
146-
147141
budgets = get_key_from_value(self.f3_cli.aa_budget, transaction_name)
148142
if len(budgets) != 0:
149143
transaction["budget_id"] = self.f3_cli.get_budget_id(budgets[0])
@@ -157,12 +151,86 @@ def add_transaction(self, ca_payload):
157151
tags.append(tag)
158152
transaction["tags"] = tags
159153

160-
transaction["notes"] = transaction_name
154+
transaction["notes"] = "CREDIT AGRICOLE NAME : " + transaction_name
155+
156+
if self.f3_cli.auto_detect_transfers and is_in_list(self.f3_cli.transfer_source_transaction, transaction_name):
157+
key = transaction["date"]
158+
if key not in self.transfer_out:
159+
self.transfer_out[key] = []
160+
self.transfer_out[key].append(payload)
161161

162-
if
163-
self.payloads.append(payload)
162+
elif self.f3_cli.auto_detect_transfers and is_in_list(self.f3_cli.transfer_destination_transaction, transaction_name):
163+
key = transaction["date"]
164+
if key not in self.transfer_in:
165+
self.transfer_in[key] = []
166+
self.transfer_in[key].append(payload)
167+
168+
else:
169+
accounts = get_key_from_value(self.f3_cli.aa_account, transaction_name)
170+
if ca_payload["montant"] > 0:
171+
transaction["type"] = "deposit"
172+
transaction["source_name"] = accounts[0] if len(accounts) > 0 else "Cash account"
173+
transaction["destination_id"] = self.account_id
174+
else:
175+
transaction["type"] = "withdrawal"
176+
transaction["source_id"] = self.account_id
177+
transaction["destination_name"] = accounts[0] if len(accounts) > 0 else "Cash account"
178+
179+
self.payloads.append(payload)
164180

165181
def post(self):
166182
for payload in self.payloads:
167183
print(".", end='')
168184
self.f3_cli._post(endpoint=_TRANSACTIONS_ENDPOINT, payload=payload)
185+
186+
@staticmethod
187+
def post_transfers(f3transactions_list, f3_cli):
188+
payloads = []
189+
detected_transfers = 0
190+
191+
# Loop through all transaction packages
192+
for f3_from_transactions in f3transactions_list:
193+
# Count detected transfers for later test
194+
detected_transfers = detected_transfers + len(f3_from_transactions) - len(f3_from_transactions.payloads)
195+
# Loop through dates of outgoing transfers of the above transaction package
196+
for date_out in f3_from_transactions.transfer_out.keys():
197+
# Loop through outgoing transfers for the above date
198+
for transfer_out in f3_from_transactions.transfer_out[date_out]:
199+
# Get the amount
200+
amount_out = transfer_out["transactions"][0]["amount"]
201+
# Now loop through other transaction packages
202+
for f3_to_transactions in f3transactions_list:
203+
# If the above transaction package is the same than the first one, skip it
204+
if f3_to_transactions.account_id == f3_from_transactions.account_id:
205+
continue
206+
# Loop through dates of incoming transfers
207+
for date_in in f3_to_transactions.transfer_in.keys():
208+
# If the incoming transfer date is the same than our outgoing transfer date, check amounts
209+
if date_in == date_out:
210+
# Loop through incoming transfers
211+
for transfer_in in f3_to_transactions.transfer_in[date_in]:
212+
# Get the amount
213+
amount_in = transfer_in["transactions"][0]["amount"]
214+
# If incoming transfer amount is the same than outgoing transfer amount,
215+
# it means that we found a corresponding incoming transfer for our outgoing transfer
216+
if amount_in == amount_out:
217+
transfer_out["transactions"][0]["type"] = "transfer"
218+
# We clarify the transfer payload (who's the source and destination)
219+
transfer_out["transactions"][0]["source_id"] = f3_from_transactions.account_id
220+
transfer_out["transactions"][0]["destination_id"] = f3_to_transactions.account_id
221+
# We rename the transaction
222+
transfer_out["transactions"][0]["description"] = "Personal transfer"
223+
# We save the payload to push it later
224+
payloads.append(transfer_out)
225+
226+
# Check amount of transfers
227+
if detected_transfers % 2 != 0 or len(payloads) * 2 != detected_transfers:
228+
print("\nWARN: Wrong quantity of transfers detected (" + str(detected_transfers) + ") for " + str(len(payloads)) + " payload(s). You must double check your \"transfer-source-transaction-name\" and \"transfer-destination-transaction-name\" because some transfers hadn't been recognized.")
229+
230+
# Now push each payload
231+
for payload in payloads:
232+
print(".", end='')
233+
f3_cli._post(endpoint=_TRANSACTIONS_ENDPOINT, payload=payload)
234+
235+
236+

main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
f3_cli.name_format = str(firefly3_section.get(ACCOUNTS_NAME_FORMAT_FIELD, ACCOUNTS_NAME_FORMAT_DEFAULT))
6969
f3_cli.hostname = str(firefly3_section.get(HOSTNAME_FIELD, HOSTNAME_DEFAULT))
7070
f3_cli.token = str(firefly3_section.get(PERSONAL_TOKEN_FIELD, PERSONAL_TOKEN_DEFAULT))
71+
f3_cli.auto_detect_transfers = bool(settings_section.get(AUTO_DETECT_TRANSFERS_FIELD, AUTO_DETECT_TRANSFERS_DEFAULT))
72+
f3_cli.transfer_source_transaction = str(settings_section.get(TRANSFER_SOURCE_TRANSACTION_NAME_FIELD, TRANSFER_SOURCE_TRANSACTION_NAME_DEFAULT)).strip().split(",")
73+
f3_cli.transfer_destination_transaction = str(settings_section.get(TRANSFER_DESTINATION_TRANSACTION_NAME_FIELD, TRANSFER_DESTINATION_TRANSACTION_NAME_DEFAULT)).strip().split(",")
7174
f3_cli.init_auto_assign_values(a_rename_transaction_section, aa_budget_section, aa_category_section, aa_account_section, aa_tags_section)
7275
f3_cli.validate()
7376

@@ -78,6 +81,9 @@
7881
f3_accounts = f3_cli.get_accounts(account_type="asset")
7982
f3_accounts_number = [account.get("attributes").get("account_number") for account in f3_accounts]
8083

84+
# Keep track of transactions for each account
85+
account_transactions = []
86+
8187
# Loop through existing CreditAgricole accounts declared in config file
8288
for account in ca_cli.get_accounts():
8389

@@ -115,3 +121,10 @@
115121
# Push transactions to Firefly3
116122
transactions.post()
117123
print(" Done!")
124+
125+
account_transactions.append(transactions)
126+
127+
if f3_cli.auto_detect_transfers:
128+
print("-> Pushing transfers to Firefly3 instance ", end='')
129+
Firefly3Transactions.post_transfers(account_transactions, f3_cli)
130+
print(" Done!")

tool.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
1-
def get_key_from_value(dict, str_to_find):
1+
def get_key_from_value(dict, str_to_check):
22
"""
33
:param dict: dictionnary dict[key] = values
4-
:param str_to_find: occurence to find in the values for each key (* can be used)
4+
:param str_to_check: occurence to find in the values for each key (* can be used)
55
:return: the key where occurence is found in one of the values
66
"""
77
output = []
8-
str_to_find = str_to_find.upper()
98
for key in dict.keys():
109
for value in dict[key]:
11-
value = value.upper()
12-
add_key = False
13-
if value.startswith("*") and value.endswith("*") and value[1:-1] in str_to_find:
14-
add_key = True
15-
elif value.startswith("*") and str_to_find.endswith(value[1:]):
16-
add_key = True
17-
elif value.endswith("*") and str_to_find.startswith(value[:-1]):
18-
add_key = True
19-
elif value == str_to_find:
20-
add_key = True
21-
if add_key and key not in output:
10+
if is_equal(value, str_to_check) and key not in output:
2211
output.append(key.title().replace("_", " "))
2312
return output
13+
14+
15+
def is_in_list(list_to_check, str_to_check):
16+
"""
17+
:param value: Value to find in str_to_check
18+
:param list_to_check: List of string to check
19+
:return: if value is found in str_to_check
20+
"""
21+
for elem in list_to_check:
22+
if is_equal(elem, str_to_check):
23+
return True
24+
return False
25+
26+
27+
def is_equal(value, str_to_check):
28+
"""
29+
:param value: Value to find in str_to_check
30+
:param str_to_check: String in which we check
31+
:return: if value is found in str_to_check using star indicators
32+
"""
33+
output = False
34+
value = value.upper()
35+
str_to_check = str_to_check.upper()
36+
if value.startswith("*") and value.endswith("*") and value[1:-1] in str_to_check:
37+
output = True
38+
elif value.startswith("*") and str_to_check.endswith(value[1:]):
39+
output = True
40+
elif value.endswith("*") and str_to_check.startswith(value[:-1]):
41+
output = True
42+
elif value == str_to_check:
43+
output = True
44+
return output

0 commit comments

Comments
 (0)