"""Ledger reconciler: compares the bank statement Excel with LedgerPro and posts missing entries. Shipped as LedgerBot.exe (PyInstaller) and started through launcher.exe.""" import json import logging import os import subprocess import sys import time from datetime import datetime from pathlib import Path import pyautogui from openpyxl import Workbook, load_workbook VERSION = "3.2.0" # shown in the window title "LedgerBot v3.2.0" FROZEN = getattr(sys, "frozen", False) BASE = Path(sys.executable).parent if FROZEN else Path(__file__).parent ASSETS = Path(getattr(sys, "_MEIPASS", BASE)) / "assets" SHORTCUT_DIRS = [Path(os.environ.get("USERPROFILE", "")) / "Desktop", Path(r"C:\Users\Public\Desktop")] CONFIDENCE = 0.9 # image match; buttons were captured at 100% display scaling, 1920x1080 FIND_WAIT_S = 20 TOLERANCE = 0.01 logging.basicConfig(filename=BASE / "logs" / f"ledger_{datetime.now():%Y%m%d}.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("ledger") def find_shortcut(): for d in SHORTCUT_DIRS: lnk = d / "LedgerPro.lnk" if lnk.exists(): return lnk raise RuntimeError("LedgerPro shortcut not found on the Desktop") def click(image): end = time.time() + FIND_WAIT_S while time.time() < end: hit = pyautogui.locateCenterOnScreen(str(ASSETS / image), confidence=CONFIDENCE) if hit: pyautogui.click(hit) return time.sleep(1) pyautogui.screenshot(str(BASE / "logs" / f"notfound_{image}")) raise RuntimeError(f"Button {image} not found on screen") def ledger_entries(path): """LedgerPro export (File > Export > Excel) saved by the bot to data/ledger_export.xlsx.""" ws = load_workbook(path, data_only=True).active return {str(r[0]): float(r[1]) for r in ws.iter_rows(min_row=2, values_only=True) if r[0]} def reconcile(statement, ledger): for ref, amount in statement.items(): if ref not in ledger: yield ref, amount, "Missing", "Not in LedgerPro - posting" elif abs(ledger[ref] - amount) > TOLERANCE: yield ref, amount, "Mismatch", f"LedgerPro has {ledger[ref]:.2f}" else: yield ref, amount, "OK", "" def post_entry(ref, amount): click("new_entry.png") pyautogui.write(ref) pyautogui.press("tab") pyautogui.write(f"{amount:.2f}") click("save_btn.png") def main(statement_path): (BASE / "BOT_WAS_RUN").write_text("1") subprocess.Popen(["cmd", "/c", "start", "", str(find_shortcut())]) time.sleep(15) click("export_excel.png") time.sleep(10) ledger = ledger_entries(BASE / "data" / "ledger_export.xlsx") ws_in = load_workbook(statement_path, data_only=True).active statement = {str(r[0]): float(r[1]) for r in ws_in.iter_rows(min_row=2, values_only=True) if r[0]} out = Workbook() ws = out.active ws.append(["Reference", "Amount", "Result", "Note"]) counts = {"OK": 0, "Missing": 0, "Mismatch": 0, "Error": 0} for ref, amount, result, note in reconcile(statement, ledger): if result == "Missing": try: post_entry(ref, amount) note = "Posted" except RuntimeError as e: result, note = "Error", str(e) counts[result] += 1 ws.append([ref, amount, result, note]) log.info("%s %s %s", ref, result, note) out.save(BASE / "data" / "output" / f"Reconcile_{datetime.now():%Y%m%d_%H%M}.xlsx") print(json.dumps({"summary": counts})) if __name__ == "__main__": main(sys.argv[1])