A script to convert CSV exported from Sparebanken Sør to a format YNAB can import
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

235 wiersze
7.2KB

  1. #!/usr/bin/env python3
  2. """
  3. Bank Statement to YNAB Converter
  4. Converts bank statements from various formats to YNAB-compatible CSV files
  5. """
  6. import os
  7. import sys
  8. import glob
  9. import pandas as pd
  10. from pathlib import Path
  11. def parse_norwegian_number(value):
  12. """Convert Norwegian number format (comma decimal) to float"""
  13. if pd.isna(value) or value == '':
  14. return 0.0
  15. # Convert to string and replace comma with dot
  16. str_value = str(value).replace(',', '.')
  17. try:
  18. return float(str_value)
  19. except ValueError:
  20. return 0.0
  21. def parse_norwegian_date(date_str):
  22. """Convert DD.MM.YYYY format to YYYY-MM-DD"""
  23. if pd.isna(date_str) or date_str == '':
  24. return ''
  25. try:
  26. # Parse DD.MM.YYYY and convert to YYYY-MM-DD
  27. date_obj = pd.to_datetime(date_str, format='%d.%m.%Y')
  28. return date_obj.strftime('%Y-%m-%d')
  29. except (ValueError, TypeError):
  30. return str(date_str) # Return original if parsing fails
  31. def parse_bank_sparebank1(data):
  32. """
  33. Parse Sparebank 1 bank data
  34. Expected columns: Dato, Beskrivelse, Rentedato, Inn, Ut, Til konto, Fra konto
  35. """
  36. result = []
  37. for _, row in data.iterrows():
  38. inflow = parse_norwegian_number(row.get('Inn'))
  39. outflow = parse_norwegian_number(row.get('Ut'))
  40. # Convert outflow to positive if negative
  41. if outflow < 0:
  42. outflow = -outflow
  43. result.append({
  44. 'Date': parse_norwegian_date(row.get('Dato', '')),
  45. 'Payee': row.get('Til konto', ''),
  46. 'Memo': row.get('Beskrivelse', ''),
  47. 'Outflow': outflow,
  48. 'Inflow': inflow
  49. })
  50. return pd.DataFrame(result)
  51. def parse_bank_norwegian(data):
  52. """
  53. Parse Norwegian bank data
  54. Expected columns: TransactionDate, Text, Memo, Amount
  55. """
  56. result = []
  57. for _, row in data.iterrows():
  58. amount = row.get('Amount', 0)
  59. inflow = amount if amount > 0 else 0
  60. outflow = -amount if amount < 0 else 0 # Make outflow positive
  61. result.append({
  62. 'Date': row.get('TransactionDate', ''),
  63. 'Payee': row.get('Text', ''),
  64. 'Memo': row.get('Memo', ''),
  65. 'Outflow': outflow,
  66. 'Inflow': inflow
  67. })
  68. return pd.DataFrame(result)
  69. # Dictionary of banks, filename patterns, and parsing functions
  70. BANKS = {
  71. "Sparebank1": {
  72. "patterns": ["OversiktKonti*.csv"],
  73. "parse_function": parse_bank_sparebank1,
  74. "delimiter": ";"
  75. },
  76. "Norwegian": {
  77. "patterns": ["BankNorwegian*.xlsx", "Statement*.xlsx"],
  78. "parse_function": parse_bank_norwegian
  79. }
  80. # Add more banks and patterns as needed
  81. }
  82. def process_bank_statement(file_path, parse_function, delimiter):
  83. """
  84. Process a single bank statement file
  85. Args:
  86. file_path (str): Path to the bank statement file
  87. parse_function (callable): Function to parse the specific bank format
  88. delimiter (Optional<str>): Field delimiter
  89. Returns:
  90. pd.DataFrame: Processed YNAB-compatible data
  91. """
  92. file_extension = Path(file_path).suffix.lower()
  93. try:
  94. # Handle CSV files
  95. if file_extension == ".csv":
  96. data = pd.read_csv(file_path, delimiter=delimiter)
  97. # Handle Excel files
  98. elif file_extension in [".xlsx", ".xls"]:
  99. data = pd.read_excel(file_path)
  100. else:
  101. print(f"Skipping unsupported file type: {file_path}")
  102. return pd.DataFrame()
  103. # Call the appropriate bank-specific parsing function
  104. ynab_data = parse_function(data)
  105. return ynab_data
  106. except Exception as e:
  107. print(f"Error processing file {file_path}: {e}")
  108. raise e
  109. return pd.DataFrame()
  110. def find_bank_config(filename):
  111. """
  112. Find the appropriate bank configuration for a given filename
  113. Args:
  114. filename (str): Name of the file to match
  115. Returns:
  116. tuple: (bank_name, bank_config) or (None, None) if no match
  117. """
  118. import fnmatch
  119. for bank_name, bank_config in BANKS.items():
  120. for pattern in bank_config["patterns"]:
  121. if fnmatch.fnmatch(filename, pattern):
  122. return bank_name, bank_config
  123. return None, None
  124. def convert_bank_statements_to_ynab(input_files=None):
  125. """
  126. Convert bank statements to YNAB format
  127. Args:
  128. input_files (list): Optional list of specific files to process
  129. If None, processes all files in current directory
  130. """
  131. current_directory = Path.cwd()
  132. output_directory = current_directory / "YNAB_Outputs"
  133. # Create output directory if it doesn't exist
  134. output_directory.mkdir(exist_ok=True)
  135. # Get list of files to process
  136. if input_files:
  137. print(f"Processing {len(input_files)} dragged file(s)...")
  138. files_to_process = [Path(f) for f in input_files if Path(f).exists()]
  139. else:
  140. print("Processing all files in current directory...")
  141. files_to_process = []
  142. # Collect all files matching any bank pattern
  143. for bank_config in BANKS.values():
  144. for pattern in bank_config["patterns"]:
  145. matching_files = glob.glob(str(current_directory / pattern))
  146. files_to_process.extend([Path(f) for f in matching_files])
  147. files_processed = False
  148. # Process each file
  149. for file_path in files_to_process:
  150. if not file_path.exists():
  151. print(f"File not found: {file_path}")
  152. continue
  153. # Find matching bank configuration
  154. bank_name, bank_config = find_bank_config(file_path.name)
  155. if not bank_config:
  156. print(f"No bank configuration found for file: {file_path.name}")
  157. continue
  158. print(f"Processing file: {file_path} for {bank_name}")
  159. parse_function = bank_config["parse_function"]
  160. delimiter = bank_config.get("delimiter", ",")
  161. # Process the file
  162. ynab_data = process_bank_statement(str(file_path), parse_function, delimiter)
  163. if ynab_data.empty:
  164. print(f"No data processed for {file_path}")
  165. continue
  166. # Define output file with "ynab-" prefix
  167. output_filename = f"ynab-{file_path.name}"
  168. if not output_filename.endswith('.csv'):
  169. output_filename = Path(output_filename).stem + '.csv'
  170. output_file = output_directory / output_filename
  171. # Export to CSV for YNAB import
  172. ynab_data.to_csv(output_file, index=False)
  173. print(f"Data saved to {output_file}")
  174. files_processed = True
  175. if not files_processed:
  176. print("No files were processed. Make sure your files match the expected patterns.")
  177. if __name__ == "__main__":
  178. # Check if files were dragged onto the script
  179. if len(sys.argv) > 1:
  180. # Files were dragged - process them
  181. files = sys.argv[1:]
  182. convert_bank_statements_to_ynab(files)
  183. else:
  184. # No files dragged - run normal directory processing
  185. convert_bank_statements_to_ynab()
  186. # Keep window open on Mac so user can see results
  187. input("\nPress Enter to close...")