192 lines
7.0 KiB
Python
192 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Infer 'Person', 'Purpose', and 'Amount' for transactions in Google Sheets."""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
# Add the current directory to sys.path to import local modules
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from googleapiclient.discovery import build
|
|
from sync_fio_to_sheets import get_sheets_service, DEFAULT_SPREADSHEET_ID
|
|
from match_payments import infer_transaction_details
|
|
from attendance import get_members_with_fees
|
|
|
|
def parse_czk_amount(val) -> float:
|
|
"""Parse Czech currency string or handle raw numeric value."""
|
|
if val is None or val == "":
|
|
return 0.0
|
|
if isinstance(val, (int, float)):
|
|
return float(val)
|
|
|
|
val = str(val)
|
|
# Strip currency symbol and spaces
|
|
val = val.replace("Kč", "").replace("CZK", "").strip()
|
|
# Remove thousand separators (often space or dot)
|
|
# Heuristic: if there's a comma, it's the decimal separator.
|
|
# If there's a dot, it might be a thousand separator OR decimal separator.
|
|
if "," in val:
|
|
# 1.500,00 -> 1500.00
|
|
val = val.replace(".", "").replace(" ", "").replace(",", ".")
|
|
else:
|
|
# 1 500.00 -> 1500.00 or 1.500.00 -> ???
|
|
# If there are multiple dots, it's thousand separator.
|
|
if val.count(".") > 1:
|
|
val = val.replace(".", "").replace(" ", "")
|
|
# If there's one dot, it might be decimal separator.
|
|
else:
|
|
val = val.replace(" ", "")
|
|
|
|
try:
|
|
return float(val)
|
|
except ValueError:
|
|
return 0.0
|
|
|
|
# Column names as requested by the user
|
|
COL_MANUAL = "manual fix"
|
|
COL_PERSON = "Person"
|
|
COL_PURPOSE = "Purpose"
|
|
COL_AMOUNT = "Inferred Amount"
|
|
|
|
def infer_payments(spreadsheet_id: str, credentials_path: str, dry_run: bool = False):
|
|
print(f"Connecting to Google Sheets...")
|
|
service = get_sheets_service(credentials_path)
|
|
sheet = service.spreadsheets()
|
|
|
|
# 1. Fetch all data from the sheet
|
|
print("Reading sheet data...")
|
|
result = sheet.values().get(
|
|
spreadsheetId=spreadsheet_id,
|
|
range="A1:Z", # Read a broad range to find existing columns
|
|
valueRenderOption="UNFORMATTED_VALUE"
|
|
).execute()
|
|
rows = result.get("values", [])
|
|
if not rows:
|
|
print("Sheet is empty.")
|
|
return
|
|
|
|
header = rows[0]
|
|
|
|
# Identify indices of existing columns
|
|
def get_col_index(label):
|
|
normalized_label = label.lower().strip()
|
|
for i, h in enumerate(header):
|
|
if h.lower().strip() == normalized_label:
|
|
return i
|
|
return -1
|
|
|
|
idx_date = get_col_index("Date")
|
|
idx_amount_raw = get_col_index("Amount") # Bank Amount
|
|
idx_sender = get_col_index("Sender")
|
|
idx_message = get_col_index("Message")
|
|
idx_vs = get_col_index("VS")
|
|
|
|
target_labels = [COL_MANUAL, COL_PERSON, COL_PURPOSE, COL_AMOUNT]
|
|
|
|
# Refresh indices
|
|
idx_manual = get_col_index(COL_MANUAL)
|
|
idx_inferred_person = get_col_index(COL_PERSON)
|
|
idx_inferred_purpose = get_col_index(COL_PURPOSE)
|
|
idx_inferred_amount = get_col_index(COL_AMOUNT)
|
|
|
|
if idx_inferred_person == -1 or idx_inferred_purpose == -1 or idx_inferred_amount == -1:
|
|
print(f"Error: Required columns {target_labels[1:]} not found in sheet.")
|
|
print(f"Current header: {header}")
|
|
return
|
|
|
|
# 2. Fetch members for matching
|
|
print("Fetching member list for matching...")
|
|
members_data, _ = get_members_with_fees()
|
|
member_names = [m[0] for m in members_data]
|
|
|
|
# 3. Process rows
|
|
print("Inffering details for empty rows...")
|
|
updates = []
|
|
|
|
for i, row in enumerate(rows[1:], start=2):
|
|
# Extend row if it's shorter than existing header
|
|
while len(row) < len(header):
|
|
row.append("")
|
|
|
|
# Check if already filled (manual override)
|
|
val_manual = str(row[idx_manual]) if idx_manual != -1 and idx_manual < len(row) else ""
|
|
val_person = str(row[idx_inferred_person]) if idx_inferred_person < len(row) else ""
|
|
val_purpose = str(row[idx_inferred_purpose]) if idx_inferred_purpose < len(row) else ""
|
|
|
|
if val_manual.strip() or val_person.strip() or val_purpose.strip():
|
|
continue
|
|
|
|
# Prepare transaction dict for matching logic
|
|
tx = {
|
|
"date": row[idx_date] if idx_date != -1 and idx_date < len(row) else "",
|
|
"amount": parse_czk_amount(row[idx_amount_raw]) if idx_amount_raw != -1 and idx_amount_raw < len(row) and row[idx_amount_raw] else 0,
|
|
"sender": row[idx_sender] if idx_sender != -1 and idx_sender < len(row) else "",
|
|
"message": row[idx_message] if idx_message != -1 and idx_message < len(row) else "",
|
|
"vs": row[idx_vs] if idx_vs != -1 and idx_vs < len(row) else "",
|
|
}
|
|
|
|
inference = infer_transaction_details(tx, member_names)
|
|
|
|
# Sort members by confidence and add markers
|
|
peeps = []
|
|
for name, conf in inference["members"]:
|
|
prefix = "[?] " if conf == "review" else ""
|
|
peeps.append(f"{prefix}{name}")
|
|
|
|
matched_months = inference["months"]
|
|
|
|
if peeps or matched_months:
|
|
person_val = ", ".join(peeps)
|
|
purpose_val = ", ".join(matched_months)
|
|
amount_val = str(tx["amount"]) # For now, use total amount
|
|
|
|
print(f"Row {i}: Inferred {person_val} for {purpose_val} ({amount_val} CZK)")
|
|
|
|
# Update the row in memory (for terminal output/dry run)
|
|
row[idx_inferred_person] = person_val
|
|
row[idx_inferred_purpose] = purpose_val
|
|
row[idx_inferred_amount] = amount_val
|
|
|
|
# Prepare batch update
|
|
updates.append({
|
|
"range": f"R{i}C{idx_inferred_person+1}:R{i}C{idx_inferred_amount+1}",
|
|
"values": [[person_val, purpose_val, amount_val]]
|
|
})
|
|
|
|
if not updates:
|
|
print("No new inferences to make.")
|
|
return
|
|
|
|
if dry_run:
|
|
print(f"Dry run: would update {len(updates)} rows.")
|
|
else:
|
|
print(f"Applying {len(updates)} updates to the sheet...")
|
|
body = {
|
|
"valueInputOption": "USER_ENTERED",
|
|
"data": updates
|
|
}
|
|
sheet.values().batchUpdate(
|
|
spreadsheetId=spreadsheet_id,
|
|
body=body
|
|
).execute()
|
|
print("Update completed successfully.")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Infer payment details in Google Sheets.")
|
|
parser.add_argument("--sheet-id", default=DEFAULT_SPREADSHEET_ID, help="Google Sheet ID")
|
|
parser.add_argument("--credentials", default="credentials.json", help="Path to Google API credentials JSON")
|
|
parser.add_argument("--dry-run", action="store_true", help="Print updates without applying them")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
infer_payments(args.sheet_id, args.credentials, args.dry_run)
|
|
except Exception as e:
|
|
print(f"Inference failed: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|