| @@ -0,0 +1,23 @@ | |||||
| import pandas as pd | |||||
| 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) | |||||
| @@ -0,0 +1,53 @@ | |||||
| import re | |||||
| import pandas as pd | |||||
| 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 date object | |||||
| return pd.to_datetime(date_str, format='%d.%m.%Y') | |||||
| except (ValueError, TypeError): | |||||
| logger.error(f"Invalid date format: {date_str}") | |||||
| exit(1) | |||||
| def convert_memo(original): | |||||
| original = original.replace(" Kurs: 1.0000", "") | |||||
| words = original.split(" ") | |||||
| while len(words) > 0: | |||||
| if words[0] == "": | |||||
| # It's empty | |||||
| del words[0] | |||||
| elif m := re.match(r'\*(\d{4})', words[0]): | |||||
| # It's the last four numbers of the card, ignore it | |||||
| del words[0] | |||||
| elif m := re.match(r'\d{2}\.\d{2}', words[0]): | |||||
| # It's the date. Move it to the end | |||||
| words.append(words.pop(0)) | |||||
| elif (m1 := re.match(r'^[A-Z]{3}$', words[0])) and (m2 := re.match(r'[\d]+\.[\d]+', words[1])): | |||||
| # It's the original currency | |||||
| if words[0] == "NOK": | |||||
| # It's Norwegian kroner, ignoring | |||||
| del words[0] | |||||
| del words[0] | |||||
| else: | |||||
| # It's some other currency, move it to the end | |||||
| words.append(words.pop(0)) | |||||
| words.append(words.pop(0)) | |||||
| else: | |||||
| break | |||||
| return " ".join(words) | |||||
| @@ -0,0 +1,29 @@ | |||||
| import pandas as pd | |||||
| from bank_parsers.helpers import parse_norwegian_number, parse_norwegian_date | |||||
| def parse_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('Beskrivelse', ''), | |||||
| 'Memo': row.get('Til konto', ''), | |||||
| 'Outflow': outflow, | |||||
| 'Inflow': inflow | |||||
| }) | |||||
| return pd.DataFrame(result) | |||||
| @@ -0,0 +1,30 @@ | |||||
| import pandas as pd | |||||
| from bank_parsers.helpers import parse_norwegian_date, convert_memo | |||||
| def parse_sparebanken_norge(data): | |||||
| """ | |||||
| Parse Sparebanken Norge bank data | |||||
| """ | |||||
| result = [] | |||||
| for _, row in data.iterrows(): | |||||
| if row.get('Status') != "Bokført": | |||||
| continue | |||||
| if row.get('Valuta') != 'NOK': | |||||
| raise ValueError(f"Unknown currency {row['Valuta']}") | |||||
| payee = convert_memo(row.get('Beskrivelse', '')) | |||||
| memo = convert_memo(row.get('Melding/KID/Fakt.nr', '')) | |||||
| result.append({ | |||||
| 'Date': parse_norwegian_date(row.get('Bokført dato')), | |||||
| 'Payee': payee, | |||||
| 'Memo': memo, | |||||
| 'Outflow': -float(row['Beløp ut'] or '0'), | |||||
| 'Inflow': float(row['Beløp inn'] or '0'), | |||||
| }) | |||||
| return pd.DataFrame(result) | |||||
| @@ -0,0 +1,171 @@ | |||||
| import glob | |||||
| import logging | |||||
| import pandas as pd | |||||
| from pathlib import Path | |||||
| from bank_parsers.sparebank1 import parse_sparebank1 | |||||
| from bank_parsers.bank_norwegian import parse_bank_norwegian | |||||
| from bank_parsers.sparebanken_norge import parse_sparebanken_norge | |||||
| # Dictionary of banks, filename patterns, and parsing functions | |||||
| BANKS = { | |||||
| "SparebankenNorge": { | |||||
| "patterns": ["Transaksjoner*.csv"], | |||||
| "encoding": "latin1", | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_sparebanken_norge, | |||||
| "delimiter": ";" | |||||
| }, | |||||
| "Sparebank1": { | |||||
| "patterns": ["OversiktKonti*.csv"], | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_sparebank1, | |||||
| "delimiter": ";" | |||||
| }, | |||||
| "Norwegian": { | |||||
| "patterns": ["BankNorwegian*.xlsx", "Statement*.xlsx"], | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_bank_norwegian | |||||
| } | |||||
| # Add more banks and patterns as needed | |||||
| } | |||||
| 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 process_bank_statement(file_path, parse_function, delimiter, encoding): | |||||
| """ | |||||
| 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<str>): 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, encoding=encoding) | |||||
| # Handle Excel files | |||||
| elif file_extension in [".xlsx", ".xls"]: | |||||
| data = pd.read_excel(file_path) | |||||
| else: | |||||
| logging.warning(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: | |||||
| logging.error(f"Error processing file {file_path}: {e}") | |||||
| raise e | |||||
| return pd.DataFrame() | |||||
| def convert_bank_statements_to_ynab(input_paths, output_directory): | |||||
| """ | |||||
| Convert bank statements to YNAB format | |||||
| Args: | |||||
| input_paths (list): List of specific files or directories to process | |||||
| """ | |||||
| # Create output directory if it doesn't exist | |||||
| output_directory.mkdir(exist_ok=True, parents=True) | |||||
| # Get list of files to process | |||||
| files_to_process = [] | |||||
| for path in input_paths: | |||||
| if not path.exists(): | |||||
| logging.warning(f"Path does not exist: {file_path}") | |||||
| elif path.is_file(): | |||||
| files_to_process.append(path) | |||||
| elif path.is_dir(): | |||||
| logging.debug(f"Looking for matching files in {path}") | |||||
| for bank_config in BANKS.values(): | |||||
| for pattern in bank_config["patterns"]: | |||||
| matching_files = glob.glob(str(path / pattern)) | |||||
| files_to_process.extend([Path(f) for f in matching_files]) | |||||
| files_processed = False | |||||
| # Process each file | |||||
| logging.info(f"Processing {len(files_to_process)} file(s)...") | |||||
| for file_path in files_to_process: | |||||
| logging.debug(f"Processing {file_path}") | |||||
| if not file_path.exists(): | |||||
| logging.warning(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: | |||||
| logging.warning(f"No bank configuration found for file: {file_path.name}") | |||||
| continue | |||||
| logging.info(f"Processing file: {file_path} for {bank_name}") | |||||
| parse_function = bank_config["parse_function"] | |||||
| delimiter = bank_config.get("delimiter", ",") | |||||
| encoding = bank_config.get("encoding", "utf-8") | |||||
| # Process the file | |||||
| ynab_data = process_bank_statement(str(file_path), parse_function, delimiter, encoding) | |||||
| if ynab_data.empty: | |||||
| logging.warning(f"No data processed for {file_path}") | |||||
| continue | |||||
| filename_placeholders = { | |||||
| 'bank': bank_name, | |||||
| 'first_date': ynab_data['Date'].min().date(), | |||||
| 'last_date': ynab_data['Date'].max().date(), | |||||
| } | |||||
| file_retry_count = 0 | |||||
| while True: | |||||
| output_filename = bank_config["output_filename"].format(**filename_placeholders) | |||||
| if file_retry_count > 0: | |||||
| output_filename += f" ({file_retry_count})" | |||||
| output_filename += ".csv" | |||||
| output_file = output_directory / output_filename | |||||
| if not output_file.exists(): | |||||
| break | |||||
| file_retry_count += 1 | |||||
| # Export to CSV for YNAB import | |||||
| ynab_data.to_csv(output_file, index=False) | |||||
| logging.info(f"Data saved to {output_file}") | |||||
| files_processed = True | |||||
| if not files_processed: | |||||
| logging.warning("No files were processed. Make sure your files match the expected patterns.") | |||||
| @@ -0,0 +1,93 @@ | |||||
| #!/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 argparse | |||||
| import logging | |||||
| import yaml | |||||
| from pathlib import Path | |||||
| from convert import convert_bank_statements_to_ynab | |||||
| def setup_logging(verbosity): | |||||
| if verbosity <= 0: | |||||
| log_level = logging.WARNING | |||||
| elif verbosity == 1: | |||||
| log_level = logging.INFO | |||||
| elif verbosity >= 2: | |||||
| log_level = logging.DEBUG | |||||
| logging.basicConfig( | |||||
| level=log_level, | |||||
| format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', | |||||
| datefmt='%Y-%m-%dT%H:%M:%S', | |||||
| ) | |||||
| if __name__ == "__main__": | |||||
| parser = argparse.ArgumentParser( | |||||
| prog='YNAB', | |||||
| description='Prepare bank transcripts for import to You Need A Budget', | |||||
| ) | |||||
| parser.add_argument( | |||||
| 'filenames', | |||||
| type=Path, | |||||
| nargs='*', | |||||
| help='The files to process', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-o', '--output-dir', | |||||
| type=Path, | |||||
| default=None, | |||||
| help='The location to store the converted files', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-v', '--verbose', | |||||
| default=0, | |||||
| action='count', | |||||
| help='Increase logging verbosity', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-c', '--config', | |||||
| type=Path, | |||||
| help='Path to the config file', | |||||
| ) | |||||
| args = parser.parse_args() | |||||
| setup_logging(args.verbose) | |||||
| config_path = args.config or Path.home() / '.config/ynab/config.yaml' | |||||
| if config_path and config_path.exists(): | |||||
| with config_path.open('r') as config_file: | |||||
| config = yaml.safe_load(config_file) | |||||
| logging.debug(f"Loaded config file {config_path}") | |||||
| else: | |||||
| logging.debug(f"Could not find config file {config_path}") | |||||
| config = {} | |||||
| verbosity = config.get('verbosity') | |||||
| if args.verbose == 0 and verbosity: | |||||
| setup_logging(verbosity) | |||||
| current_directory = Path.cwd() | |||||
| inputs = args.filenames or config.get('default_inputs') | |||||
| if not inputs: | |||||
| logging.info("Processing all files in current directory") | |||||
| inputs = [current_directory] | |||||
| output_dir = args.output_dir or Path(config.get('output_dir')) | |||||
| if not output_dir: | |||||
| output_dir = current_directory / "YNAB_Outputs" | |||||
| logging.debug(f"No output directory set. Defaulting to {output_dir}") | |||||
| convert_bank_statements_to_ynab(inputs, output_dir) | |||||
| @@ -1,384 +0,0 @@ | |||||
| #!/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 re | |||||
| import argparse | |||||
| import logging | |||||
| import yaml | |||||
| import pandas as pd | |||||
| from pathlib import Path | |||||
| logger = logging.getLogger() | |||||
| 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 date object | |||||
| return pd.to_datetime(date_str, format='%d.%m.%Y') | |||||
| except (ValueError, TypeError): | |||||
| logger.error(f"Invalid date format: {date_str}") | |||||
| exit(1) | |||||
| def convert_memo(original): | |||||
| original = original.replace(" Kurs: 1.0000", "") | |||||
| words = original.split(" ") | |||||
| while len(words) > 0: | |||||
| if words[0] == "": | |||||
| # It's empty | |||||
| del words[0] | |||||
| elif m := re.match(r'\*(\d{4})', words[0]): | |||||
| # It's the last four numbers of the card, ignore it | |||||
| del words[0] | |||||
| elif m := re.match(r'\d{2}\.\d{2}', words[0]): | |||||
| # It's the date. Move it to the end | |||||
| words.append(words.pop(0)) | |||||
| elif (m1 := re.match(r'^[A-Z]{3}$', words[0])) and (m2 := re.match(r'[\d]+\.[\d]+', words[1])): | |||||
| # It's the original currency | |||||
| if words[0] == "NOK": | |||||
| # It's Norwegian kroner, ignoring | |||||
| del words[0] | |||||
| del words[0] | |||||
| else: | |||||
| # It's some other currency, move it to the end | |||||
| words.append(words.pop(0)) | |||||
| words.append(words.pop(0)) | |||||
| else: | |||||
| break | |||||
| return " ".join(words) | |||||
| def parse_bank_sor(data): | |||||
| """ | |||||
| Parse Sparebank 1 bank data | |||||
| Expected columns: Dato, Beskrivelse, Rentedato, Inn, Ut, Til konto, Fra konto | |||||
| """ | |||||
| result = [] | |||||
| for _, row in data.iterrows(): | |||||
| if row.get('Status') != "Bokført": | |||||
| continue | |||||
| if row.get('Valuta') != 'NOK': | |||||
| raise ValueError(f"Unknown currency {row['Valuta']}") | |||||
| payee = convert_memo(row.get('Beskrivelse', '')) | |||||
| memo = convert_memo(row.get('Melding/KID/Fakt.nr', '')) | |||||
| result.append({ | |||||
| 'Date': parse_norwegian_date(row.get('Bokført dato')), | |||||
| 'Payee': payee, | |||||
| 'Memo': memo, | |||||
| 'Outflow': -float(row['Beløp ut'] or '0'), | |||||
| 'Inflow': float(row['Beløp inn'] or '0'), | |||||
| }) | |||||
| return pd.DataFrame(result) | |||||
| 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('Beskrivelse', ''), | |||||
| 'Memo': row.get('Til konto', ''), | |||||
| '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 = { | |||||
| "SparebankenNorge": { | |||||
| "patterns": ["Transaksjoner*.csv"], | |||||
| "encoding": "latin1", | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_bank_sor, | |||||
| "delimiter": ";" | |||||
| }, | |||||
| "Sparebank1": { | |||||
| "patterns": ["OversiktKonti*.csv"], | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_bank_sparebank1, | |||||
| "delimiter": ";" | |||||
| }, | |||||
| "Norwegian": { | |||||
| "patterns": ["BankNorwegian*.xlsx", "Statement*.xlsx"], | |||||
| "output_filename": "YNAB-{bank}-FROM-{first_date}-TO-{last_date}", | |||||
| "parse_function": parse_bank_norwegian | |||||
| } | |||||
| # Add more banks and patterns as needed | |||||
| } | |||||
| def process_bank_statement(file_path, parse_function, delimiter, encoding): | |||||
| """ | |||||
| 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<str>): 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, encoding=encoding) | |||||
| # Handle Excel files | |||||
| elif file_extension in [".xlsx", ".xls"]: | |||||
| data = pd.read_excel(file_path) | |||||
| else: | |||||
| logger.warning(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: | |||||
| logger.error(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_paths, output_directory): | |||||
| """ | |||||
| Convert bank statements to YNAB format | |||||
| Args: | |||||
| input_paths (list): List of specific files or directories to process | |||||
| """ | |||||
| # Create output directory if it doesn't exist | |||||
| output_directory.mkdir(exist_ok=True, parents=True) | |||||
| # Get list of files to process | |||||
| files_to_process = [] | |||||
| for path in input_paths: | |||||
| if not path.exists(): | |||||
| logger.warning(f"Path does not exist: {file_path}") | |||||
| elif path.is_file(): | |||||
| files_to_process.append(path) | |||||
| elif path.is_dir(): | |||||
| logger.debug(f"Looking for matching files in {path}") | |||||
| for bank_config in BANKS.values(): | |||||
| for pattern in bank_config["patterns"]: | |||||
| matching_files = glob.glob(str(path / pattern)) | |||||
| files_to_process.extend([Path(f) for f in matching_files]) | |||||
| files_processed = False | |||||
| # Process each file | |||||
| logger.info(f"Processing {len(files_to_process)} file(s)...") | |||||
| for file_path in files_to_process: | |||||
| logger.debug(f"Processing {file_path}") | |||||
| if not file_path.exists(): | |||||
| logger.warning(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: | |||||
| logger.warning(f"No bank configuration found for file: {file_path.name}") | |||||
| continue | |||||
| logger.info(f"Processing file: {file_path} for {bank_name}") | |||||
| parse_function = bank_config["parse_function"] | |||||
| delimiter = bank_config.get("delimiter", ",") | |||||
| encoding = bank_config.get("encoding", "utf-8") | |||||
| # Process the file | |||||
| ynab_data = process_bank_statement(str(file_path), parse_function, delimiter, encoding) | |||||
| if ynab_data.empty: | |||||
| logger.warning(f"No data processed for {file_path}") | |||||
| continue | |||||
| filename_placeholders = { | |||||
| 'bank': bank_name, | |||||
| 'first_date': ynab_data['Date'].min().date(), | |||||
| 'last_date': ynab_data['Date'].max().date(), | |||||
| } | |||||
| file_retry_count = 0 | |||||
| while True: | |||||
| output_filename = bank_config["output_filename"].format(**filename_placeholders) | |||||
| if file_retry_count > 0: | |||||
| output_filename += f" ({file_retry_count})" | |||||
| output_filename += ".csv" | |||||
| output_file = output_directory / output_filename | |||||
| if not output_file.exists(): | |||||
| break | |||||
| file_retry_count += 1 | |||||
| # Export to CSV for YNAB import | |||||
| ynab_data.to_csv(output_file, index=False) | |||||
| logger.info(f"Data saved to {output_file}") | |||||
| files_processed = True | |||||
| if not files_processed: | |||||
| logger.warning("No files were processed. Make sure your files match the expected patterns.") | |||||
| def setup_logger(verbosity): | |||||
| if verbosity <= 0: | |||||
| log_level = logging.WARNING | |||||
| elif verbosity == 1: | |||||
| log_level = logging.INFO | |||||
| elif verbosity >= 2: | |||||
| log_level = logging.DEBUG | |||||
| logging.basicConfig( | |||||
| level=log_level, | |||||
| format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', | |||||
| datefmt='%Y-%m-%dT%H:%M:%S', | |||||
| ) | |||||
| if __name__ == "__main__": | |||||
| parser = argparse.ArgumentParser( | |||||
| prog='YNAB', | |||||
| description='Prepare bank transcripts for import to You Need A Budget', | |||||
| ) | |||||
| parser.add_argument( | |||||
| 'filenames', | |||||
| type=Path, | |||||
| nargs='*', | |||||
| help='The files to process', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-o', '--output-dir', | |||||
| type=Path, | |||||
| default=None, | |||||
| help='The location to store the converted files', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-v', '--verbose', | |||||
| default=0, | |||||
| action='count', | |||||
| help='Increase logging verbosity', | |||||
| ) | |||||
| parser.add_argument( | |||||
| '-c', '--config', | |||||
| type=Path, | |||||
| help='Path to the config file', | |||||
| ) | |||||
| args = parser.parse_args() | |||||
| setup_logger(args.verbose) | |||||
| config_path = args.config or Path.home() / '.config/ynab/config.yaml' | |||||
| if config_path and config_path.exists(): | |||||
| with config_path.open('r') as config_file: | |||||
| config = yaml.safe_load(config_file) | |||||
| logger.debug(f"Loaded config file {config_path}") | |||||
| else: | |||||
| logger.debug(f"Could not find config file {config_path}") | |||||
| config = {} | |||||
| verbosity = config.get('verbosity') | |||||
| if args.verbose == 0 and verbosity: | |||||
| setup_logger(verbosity) | |||||
| current_directory = Path.cwd() | |||||
| inputs = args.filenames or config.get('default_inputs') | |||||
| if not inputs: | |||||
| logger.info("Processing all files in current directory") | |||||
| inputs = [current_directory] | |||||
| output_dir = args.output_dir or Path(config.get('output_dir')) | |||||
| if not output_dir: | |||||
| output_dir = current_directory / "YNAB_Outputs" | |||||
| logger.debug(f"No output directory set. Defaulting to {output_dir}") | |||||
| convert_bank_statements_to_ynab(inputs, output_dir) | |||||