When I buy something and make payment sms immediately comes on mobile. I enter the transactions in Gnucash on Laptop with Endless OS manually. I asked Gemini how to automate this. Gemini replied that it does not have access to my sms messages but I can install an SMS Forwarder which can be configured with IP address of my Laptop. But the IP address should be static. I changed IP of my Wifi on Laptop with static IP 192,168.29.3 and configured DHCP in Jio Fiber modem to have start IP as 192.168.29.10. I already have static IP of 192,168.29.2 on Wifi Range extender modem.
The sms forwarder is configured for Webhook URL http://192.168.29.3:5000/sms
I generally use my ICICI Coral Rupay credit card and ICICI Savings Bank account for every day local and online shopping. The sms has ICICIT as sender. I configured this sender.
On Laptop I installed Flask by following command:
python3 -m pip install Flask --break-system-packages
I saved file sms_to_qif.py with following script
from flask import Flask, request
import re
from datetime import datetime
app = Flask(__name__)
# --- CATEGORY MAPPING ---
# Put your merchant names in ALL LOWERCASE here.
# Ensure the right side perfectly matches your GnuCash expense accounts.
CATEGORY_MAP = {
"SURJAN": "Expenses:Groceries:Vegetables",
"zomato": "Expenses:Dining",
"annapurna marathi": "Expenses:Dining",
"swiggy": "Expenses:Dining",
"idli & more": "Expenses:Dining",
"VAIBHAV KIRANA": "Expenses:Groceries:Milk, Curd, Ghee",
"jio": "Expenses:Broadband",
"aruna": "Current Assets:Loan to Aruna",
"innovati": "Expenses:Shopping" # Added based on your Rupay test
}
# The script checks these in order from top to bottom.
# --- CONFIGURATION RULES ---
ACCOUNT_RULES = [
# 1. RULES FOR SALAIYA ACCOUNT (XX030)
{
"name": "ICICI Savings Salaiya - EXPENSE",
"keyword": "XX030",
"gnucash_account": "Assets:Current Assets:ICICI Bank Salaiya",
"regex": r'debited for Rs\s*(\d+(?:\.\d{1,2})?).*?;\s*(.+?)\s+credited',
"is_credit": False
},
{
"name": "ICICI Savings Salaiya - INCOME",
"keyword": "XX030",
"gnucash_account": "Assets:Current Assets:ICICI Bank Salaiya",
"regex": r'Acct XX030 is credited with Rs\s*(\d+(?:\.\d{1,2})?).*?(?:from|by)\s+([A-Za-z0-9\s]+)',
"is_credit": True
},
# 2. RULES FOR 853 ACCOUNT (XX853)
{
"name": "ICICI Savings 853 - EXPENSE",
"keyword": "XX853",
"gnucash_account": "Assets:Current Assets:ICICI Bank",
"regex": r'debited for Rs\s*(\d+(?:\.\d{1,2})?).*?;\s*(.+?)\s+credited',
"is_credit": False
},
{
"name": "ICICI Savings 853 - INCOME",
"keyword": "XX853",
"gnucash_account": "Assets:Current Assets:ICICI Bank",
"regex": r'Acct XX853 is credited with Rs\s*(\d+(?:\.\d{1,2})?).*?(?:from|by)\s+([A-Za-z0-9\s]+)',
"is_credit": True
},
# 3. RULE FOR RUPAY CREDIT CARD (XX2000)
{
"name": "ICICI Credit Card - EXPENSE",
"keyword": "XX2000",
"gnucash_account": "Liabilities:Credit Cards:ICICI Rupay",
"regex": r'debited for INR\s*(\d+(?:\.\d{1,2})?).*?for\s+(.+?)\.\s*To dispute',
"is_credit": False
},
# 4. GENERIC FALLBACK (Catches anything else)
{
"name": "Generic Fallback",
"keyword": "",
"gnucash_account": "Imbalance-INR",
"regex": r'(?:Rs\.?|INR)\s*(\d+(?:\.\d{1,2})?).*?(?:to|at|for)\s+([A-Za-z0-9\s-]+?)(?:\.|\son)',
"is_credit": False
}
]
@app.route('/sms', methods=['POST'])
def receive_sms():
data = request.json or {}
# Safely get text and sender, accounting for different app payload formats
sms_text = data.get('text', data.get('body', ''))
sender = data.get('sender', data.get('from', ''))
print(f"\nReceived SMS from {sender}: {sms_text}")
date_str = datetime.now().strftime("%d/%m/%Y")
for rule in ACCOUNT_RULES:
# Check if the keyword is in the SMS text OR the Sender name
if rule["keyword"].lower() in sms_text.lower() or rule["keyword"].lower() in sender.lower():
match = re.search(rule["regex"], sms_text, re.IGNORECASE)
if match:
amount = match.group(1)
if len(match.groups()) >= 2:
merchant = match.group(2).strip()
else:
merchant = "Unknown Payee"
# --- AUTO-CATEGORIZATION ENGINE ---
category = "Expenses:Uncategorized"
merchant_lower = merchant.lower()
for key, mapped_category in CATEGORY_MAP.items():
if key in merchant_lower:
category = mapped_category
break
gnucash_account = rule["gnucash_account"]
sign = "" if rule.get("is_credit", False) else "-"
# Format for QIF with Account and Category flags
qif_entry = f"\n!Account\nN{gnucash_account}\n^\n!Type:Bank\nD{date_str}\nT{sign}{amount}\nP{merchant}\nM{sms_text}\nL{category}\n^\n"
with open("gnucash_import.qif", "a") as file:
file.write(qif_entry)
print(f"✅ Saved to {rule['name']}: {merchant} - Rs. {sign}{amount} ({category})")
return "Transaction Saved", 200
print("❌ No matching rule found or regex failed to extract data.")
return "Ignored", 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)then I started the server on Laptop with following command
python3 sms_to_qif.py
Now whenever the sms comes on mobile it comes on Laptop and saved as gnucash_import.gif
I can import the transaction on Gnucash
By the way I am not a Coder the above script and entire guidance is given by Gemini.
The system is in testing face and after a few days it will be totally automated without manually importing the file.
No comments:
Post a Comment