diff --git a/ynab-karianne.py b/ynab-karianne.py new file mode 100644 index 0000000..12a1abe --- /dev/null +++ b/ynab-karianne.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +""" +Bank Statement to YNAB Converter +Converts bank statements from various formats to YNAB-compatible CSV files +""" + +import os +import sys +import glob +import pandas as pd +from pathlib import Path + +def parse_norwegian_number(value): + """Convert Norwegian number format (comma decimal) to float""" + if pd.isna(value) or value == '': + return 0.0 + # Convert to string and replace comma with dot + str_value = str(value).replace(',', '.') + try: + return float(str_value) + except ValueError: + return 0.0 + +def parse_norwegian_date(date_str): + """Convert DD.MM.YYYY format to YYYY-MM-DD""" + if pd.isna(date_str) or date_str == '': + return '' + try: + # Parse DD.MM.YYYY and convert to YYYY-MM-DD + date_obj = pd.to_datetime(date_str, format='%d.%m.%Y') + return date_obj.strftime('%Y-%m-%d') + except (ValueError, TypeError): + return str(date_str) # Return original if parsing fails + +def parse_bank_sparebank1(data): + """ + Parse Sparebank 1 bank data + Expected columns: Dato, Beskrivelse, Rentedato, Inn, Ut, Til konto, Fra konto + """ + result = [] + + for _, row in data.iterrows(): + inflow = parse_norwegian_number(row.get('Inn')) + outflow = parse_norwegian_number(row.get('Ut')) + + # Convert outflow to positive if negative + if outflow < 0: + outflow = -outflow + + result.append({ + 'Date': parse_norwegian_date(row.get('Dato', '')), + 'Payee': row.get('Til konto', ''), + 'Memo': row.get('Beskrivelse', ''), + 'Outflow': outflow, + 'Inflow': inflow + }) + + return pd.DataFrame(result) + + +def parse_bank_norwegian(data): + """ + Parse Norwegian bank data + Expected columns: TransactionDate, Text, Memo, Amount + """ + result = [] + + for _, row in data.iterrows(): + amount = row.get('Amount', 0) + inflow = amount if amount > 0 else 0 + outflow = -amount if amount < 0 else 0 # Make outflow positive + + result.append({ + 'Date': row.get('TransactionDate', ''), + 'Payee': row.get('Text', ''), + 'Memo': row.get('Memo', ''), + 'Outflow': outflow, + 'Inflow': inflow + }) + + return pd.DataFrame(result) + + +# Dictionary of banks, filename patterns, and parsing functions +BANKS = { + "Sparebank1": { + "patterns": ["OversiktKonti*.csv"], + "parse_function": parse_bank_sparebank1, + "delimiter": ";" + }, + "Norwegian": { + "patterns": ["BankNorwegian*.xlsx", "Statement*.xlsx"], + "parse_function": parse_bank_norwegian + } + # Add more banks and patterns as needed +} + + +def process_bank_statement(file_path, parse_function, delimiter): + """ + Process a single bank statement file + + Args: + file_path (str): Path to the bank statement file + parse_function (callable): Function to parse the specific bank format + delimiter (Optional): Field delimiter + + Returns: + pd.DataFrame: Processed YNAB-compatible data + """ + file_extension = Path(file_path).suffix.lower() + + try: + # Handle CSV files + if file_extension == ".csv": + data = pd.read_csv(file_path, delimiter=delimiter) + # Handle Excel files + elif file_extension in [".xlsx", ".xls"]: + data = pd.read_excel(file_path) + else: + print(f"Skipping unsupported file type: {file_path}") + return pd.DataFrame() + + # Call the appropriate bank-specific parsing function + ynab_data = parse_function(data) + return ynab_data + + except Exception as e: + print(f"Error processing file {file_path}: {e}") + raise e + return pd.DataFrame() + + +def find_bank_config(filename): + """ + Find the appropriate bank configuration for a given filename + + Args: + filename (str): Name of the file to match + + Returns: + tuple: (bank_name, bank_config) or (None, None) if no match + """ + import fnmatch + + for bank_name, bank_config in BANKS.items(): + for pattern in bank_config["patterns"]: + if fnmatch.fnmatch(filename, pattern): + return bank_name, bank_config + + return None, None + + +def convert_bank_statements_to_ynab(input_files=None): + """ + Convert bank statements to YNAB format + + Args: + input_files (list): Optional list of specific files to process + If None, processes all files in current directory + """ + current_directory = Path.cwd() + output_directory = current_directory / "YNAB_Outputs" + + # Create output directory if it doesn't exist + output_directory.mkdir(exist_ok=True) + + # Get list of files to process + if input_files: + print(f"Processing {len(input_files)} dragged file(s)...") + files_to_process = [Path(f) for f in input_files if Path(f).exists()] + else: + print("Processing all files in current directory...") + files_to_process = [] + # Collect all files matching any bank pattern + for bank_config in BANKS.values(): + for pattern in bank_config["patterns"]: + matching_files = glob.glob(str(current_directory / pattern)) + files_to_process.extend([Path(f) for f in matching_files]) + + files_processed = False + + # Process each file + for file_path in files_to_process: + if not file_path.exists(): + print(f"File not found: {file_path}") + continue + + # Find matching bank configuration + bank_name, bank_config = find_bank_config(file_path.name) + + if not bank_config: + print(f"No bank configuration found for file: {file_path.name}") + continue + + print(f"Processing file: {file_path} for {bank_name}") + + parse_function = bank_config["parse_function"] + delimiter = bank_config.get("delimiter", ",") + + # Process the file + ynab_data = process_bank_statement(str(file_path), parse_function, delimiter) + + if ynab_data.empty: + print(f"No data processed for {file_path}") + continue + + # Define output file with "ynab-" prefix + output_filename = f"ynab-{file_path.name}" + if not output_filename.endswith('.csv'): + output_filename = Path(output_filename).stem + '.csv' + + output_file = output_directory / output_filename + + # Export to CSV for YNAB import + ynab_data.to_csv(output_file, index=False) + print(f"Data saved to {output_file}") + files_processed = True + + if not files_processed: + print("No files were processed. Make sure your files match the expected patterns.") + + +if __name__ == "__main__": + # Check if files were dragged onto the script + if len(sys.argv) > 1: + # Files were dragged - process them + files = sys.argv[1:] + convert_bank_statements_to_ynab(files) + else: + # No files dragged - run normal directory processing + convert_bank_statements_to_ynab() + + # Keep window open on Mac so user can see results + input("\nPress Enter to close...") \ No newline at end of file