#!/usr/bin/env python3 """ FJCCV Ledger Auto-Update Automatically add donation entries to the Excel ledger. """ import argparse import sys from pathlib import Path from datetime import datetime import openpyxl from openpyxl.styles import Font, Alignment def get_ledger_path(): """Get the path to the FJCCV ledger""" # Primary location: workspace root workspace = Path.home() / ".openclaw/agents/main/workspace" primary_ledger = workspace / "FJCCV_Ledger.xlsx" if primary_ledger.exists(): return primary_ledger # Fallback: skill directory script_dir = Path(__file__).parent skill_dir = script_dir.parent return skill_dir / "FJCCV_Ledger.xlsx" def calculate_stripe_fee(gross_amount: float) -> float: """ Calculate Stripe fee for a donation. Stripe charges 2.9% + $0.30 per transaction. """ return (gross_amount * 0.029) + 0.30 def add_donation(ledger_path: Path, date_str: str, donor: str, gross_amount: float, payment_method: str, transaction_id: str = None, stripe_fee: float = None) -> dict: """ Add a donation entry to the ledger. Returns dict with: - success: bool - receipt_number: str (if successful) - row_number: int (if successful) - message: str """ # Parse date try: if '/' in date_str: # Handle MM/DD/YY or MM/DD/YYYY parts = date_str.split('/') if len(parts[2]) == 2: # Two-digit year - assume 20xx year = 2000 + int(parts[2]) else: year = int(parts[2]) date_obj = datetime(year, int(parts[0]), int(parts[1])) else: # ISO format YYYY-MM-DD date_obj = datetime.fromisoformat(date_str) except Exception as e: return { 'success': False, 'message': f"Invalid date format: {e}" } year = date_obj.year # Load workbook try: wb = openpyxl.load_workbook(ledger_path) except Exception as e: return { 'success': False, 'message': f"Could not load ledger: {e}" } # Find or create donations sheet for year sheet_name = f"Donations {year}" if sheet_name not in wb.sheetnames: return { 'success': False, 'message': f"Sheet '{sheet_name}' not found. Create it manually first." } ws = wb[sheet_name] # Calculate Stripe fee if using Stripe and not provided if payment_method and 'stripe' in payment_method.lower(): if stripe_fee is None: stripe_fee = calculate_stripe_fee(gross_amount) net_amount = gross_amount - stripe_fee else: stripe_fee = 0 net_amount = gross_amount # Determine receipt number from ledger_helper import get_next_receipt_number result = get_next_receipt_number(ledger_path, donor, year) if not result: return { 'success': False, 'message': "Could not calculate receipt number" } receipt_number, donor_id, donation_count, total_with_initial = result # Find the last data row (before totals/summaries) last_data_row = 1 for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2): date_val = row[0] # Stop when we hit empty rows or formula rows (TOTAL, By Donor, etc.) if date_val is None or (isinstance(date_val, str) and 'TOTAL' in date_val.upper()): break last_data_row = row_idx # Insert new row after last data row insert_row = last_data_row + 1 # Write data ws.cell(row=insert_row, column=1).value = date_obj # Date ws.cell(row=insert_row, column=2).value = donor # Donor ws.cell(row=insert_row, column=3).value = gross_amount # Gross Amount ws.cell(row=insert_row, column=4).value = stripe_fee # Stripe Fee ws.cell(row=insert_row, column=5).value = net_amount # Net Amount ws.cell(row=insert_row, column=6).value = receipt_number # Receipt # ws.cell(row=insert_row, column=7).value = transaction_id if transaction_id else "" # Transaction ID # Format date cell ws.cell(row=insert_row, column=1).number_format = 'M/D/YYYY' # Format currency cells for col in [3, 4, 5]: # Gross, Fee, Net ws.cell(row=insert_row, column=col).number_format = '#,##0.00' # Save workbook try: wb.save(ledger_path) return { 'success': True, 'receipt_number': receipt_number, 'row_number': insert_row, 'net_amount': net_amount, 'stripe_fee': stripe_fee, 'message': f"Added donation to row {insert_row} in '{sheet_name}'" } except Exception as e: return { 'success': False, 'message': f"Failed to save ledger: {e}" } def add_bank_reconciliation(ledger_path: Path, date_str: str, donor: str, net_amount: float, description: str = None) -> dict: """ Add a bank reconciliation entry for a donation. Returns dict with success status. """ # Parse date try: if '/' in date_str: parts = date_str.split('/') if len(parts[2]) == 2: year = 2000 + int(parts[2]) else: year = int(parts[2]) date_obj = datetime(year, int(parts[0]), int(parts[1])) else: date_obj = datetime.fromisoformat(date_str) except Exception as e: return { 'success': False, 'message': f"Invalid date format: {e}" } # Load workbook try: wb = openpyxl.load_workbook(ledger_path, data_only=False) except Exception as e: return { 'success': False, 'message': f"Could not load ledger: {e}" } if "Bank Reconciliation" not in wb.sheetnames: return { 'success': False, 'message': "Bank Reconciliation sheet not found" } ws = wb["Bank Reconciliation"] # Find last row with data last_row = 1 for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2): if row[0]: # Has a date last_row = row_idx insert_row = last_row + 1 # Get previous running balance prev_balance = ws.cell(row=last_row, column=5).value or 0 if isinstance(prev_balance, str): # It's a formula, try to get the value prev_balance = 0 new_balance = prev_balance + net_amount # Description if not description: description = f"FJCCV Donation ({donor} ${net_amount:,.2f})" # Write data ws.cell(row=insert_row, column=1).value = date_obj # Date ws.cell(row=insert_row, column=2).value = description # Description ws.cell(row=insert_row, column=3).value = net_amount # Amount ws.cell(row=insert_row, column=4).value = "Donation" # Category ws.cell(row=insert_row, column=5).value = new_balance # Running Balance # Format cells ws.cell(row=insert_row, column=1).number_format = 'M/D/YYYY' ws.cell(row=insert_row, column=3).number_format = '#,##0.00' ws.cell(row=insert_row, column=5).number_format = '#,##0.00' # Save try: wb.save(ledger_path) return { 'success': True, 'row_number': insert_row, 'new_balance': new_balance, 'message': f"Added bank reconciliation entry to row {insert_row}" } except Exception as e: return { 'success': False, 'message': f"Failed to save ledger: {e}" } def main(): parser = argparse.ArgumentParser(description='Add donation to FJCCV ledger') parser.add_argument('--date', required=True, help='Date (MM/DD/YY or YYYY-MM-DD)') parser.add_argument('--donor', required=True, help='Donor name') parser.add_argument('--amount', required=True, type=float, help='Gross donation amount') parser.add_argument('--payment-method', required=True, help='Payment method (Stripe, PayPal, Zelle, etc.)') parser.add_argument('--transaction', help='Transaction ID (optional)') parser.add_argument('--stripe-fee', type=float, help='Custom Stripe fee (auto-calculated if using Stripe)') parser.add_argument('--bank-reconciliation', action='store_true', help='Also add entry to Bank Reconciliation sheet') parser.add_argument('--dry-run', action='store_true', help='Show what would be added without saving') args = parser.parse_args() ledger_path = get_ledger_path() if not ledger_path.exists(): print(f"āŒ Error: Ledger not found at {ledger_path}") return 1 if args.dry_run: print("šŸ” DRY RUN - No changes will be saved\n") # Add donation result = add_donation( ledger_path=ledger_path, date_str=args.date, donor=args.donor, gross_amount=args.amount, payment_method=args.payment_method, transaction_id=args.transaction, stripe_fee=args.stripe_fee ) if not result['success']: print(f"āŒ Error: {result['message']}") return 1 print(f"āœ… Donation added successfully") print(f" Receipt #: {result['receipt_number']}") print(f" Gross: ${args.amount:,.2f}") print(f" Fee: ${result['stripe_fee']:,.2f}") print(f" Net: ${result['net_amount']:,.2f}") print(f" Row: {result['row_number']}") # Add bank reconciliation if requested if args.bank_reconciliation: bank_result = add_bank_reconciliation( ledger_path=ledger_path, date_str=args.date, donor=args.donor, net_amount=result['net_amount'] ) if bank_result['success']: print(f"\nāœ… Bank reconciliation added") print(f" Row: {bank_result['row_number']}") print(f" New Balance: ${bank_result['new_balance']:,.2f}") else: print(f"\nāš ļø Warning: Could not add bank reconciliation: {bank_result['message']}") return 0 if __name__ == '__main__': sys.exit(main())