mercury_banking 0.5.37 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +46 -2
- data/Gemfile +13 -12
- data/Gemfile.lock +1 -1
- data/Rakefile +3 -1
- data/bin/console +1 -0
- data/bin/mercury +2 -1
- data/lib/mercury_banking/api.rb +26 -22
- data/lib/mercury_banking/cli/accounts.rb +24 -24
- data/lib/mercury_banking/cli/base.rb +13 -28
- data/lib/mercury_banking/cli/financials.rb +247 -195
- data/lib/mercury_banking/cli/reconciliation.rb +284 -371
- data/lib/mercury_banking/cli/reports.rb +82 -74
- data/lib/mercury_banking/cli/transactions.rb +74 -61
- data/lib/mercury_banking/cli.rb +51 -49
- data/lib/mercury_banking/formatters/export_formatter.rb +99 -97
- data/lib/mercury_banking/formatters/table_formatter.rb +32 -30
- data/lib/mercury_banking/multi.rb +43 -37
- data/lib/mercury_banking/recipient.rb +17 -9
- data/lib/mercury_banking/reconciliation.rb +57 -58
- data/lib/mercury_banking/reports/balance_sheet.rb +210 -218
- data/lib/mercury_banking/reports/reconciliation.rb +114 -100
- data/lib/mercury_banking/utils/command_utils.rb +3 -1
- data/lib/mercury_banking/version.rb +3 -1
- data/lib/mercury_banking.rb +2 -0
- data/mercury_banking.gemspec +15 -12
- metadata +39 -38
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module MercuryBanking
|
2
4
|
module Reports
|
3
5
|
# Module for generating balance sheet reports
|
@@ -9,57 +11,55 @@ module MercuryBanking
|
|
9
11
|
puts "Error: 'ledger' command not found. Please install ledger (https://www.ledger-cli.org/)."
|
10
12
|
return nil
|
11
13
|
end
|
12
|
-
|
14
|
+
|
13
15
|
# Verify the file exists
|
14
16
|
unless File.exist?(file_path)
|
15
17
|
puts "Error: Ledger file not found at #{file_path}"
|
16
18
|
return nil
|
17
19
|
end
|
18
|
-
|
20
|
+
|
19
21
|
# Verify and potentially fix the ledger file
|
20
22
|
verified_file_path = verify_ledger_file_syntax(file_path)
|
21
23
|
return nil unless verified_file_path
|
22
|
-
|
24
|
+
|
23
25
|
# Generate the balance sheet
|
24
26
|
generate_ledger_balance_report(verified_file_path, end_date)
|
25
27
|
end
|
26
|
-
|
28
|
+
|
27
29
|
# Verify the syntax of a ledger file and fix common issues if needed
|
28
30
|
def verify_ledger_file_syntax(file_path)
|
29
31
|
check_cmd = "ledger -f #{file_path} --verify"
|
30
32
|
check_output = `#{check_cmd}`
|
31
|
-
|
32
|
-
if $?.success?
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
|
33
|
+
|
34
|
+
return file_path if $?.success?
|
35
|
+
|
36
|
+
puts "Warning: Ledger file verification failed. The file may contain syntax errors."
|
37
|
+
puts "Error output: #{check_output}"
|
38
|
+
|
39
|
+
# Try to identify problematic lines
|
40
|
+
problematic_lines = `grep -n '^[0-9]' #{file_path} | head -10`
|
41
|
+
puts "First few transaction lines for inspection:"
|
42
|
+
puts problematic_lines
|
43
|
+
|
44
|
+
# Try to fix common issues
|
45
|
+
puts "Attempting to fix common issues in the ledger file..."
|
46
|
+
fix_ledger_file_syntax(file_path)
|
47
|
+
end
|
48
|
+
|
49
49
|
# Fix common syntax issues in a ledger file
|
50
50
|
def fix_ledger_file_syntax(file_path)
|
51
51
|
fixed_file_path = "#{file_path}.fixed"
|
52
|
-
|
52
|
+
|
53
53
|
# Create a fixed version of the file with safer formatting
|
54
54
|
File.open(fixed_file_path, 'w') do |fixed_file|
|
55
55
|
File.foreach(file_path) do |line|
|
56
|
-
if line.match?(
|
56
|
+
if line.match?(%r{^\d{4}[/\-.]\d{2}[/\-.]\d{2}})
|
57
57
|
# This is a transaction line, ensure description is safe
|
58
58
|
date, description = line.strip.split(' ', 2)
|
59
|
-
if description && (description.match?(/^\d/) ||
|
60
|
-
description.match?(/^[v#]/) ||
|
61
|
-
description.match?(/^\s*\d{4}/) ||
|
62
|
-
description.match?(
|
59
|
+
if description && (description.match?(/^\d/) ||
|
60
|
+
description.match?(/^[v#]/) ||
|
61
|
+
description.match?(/^\s*\d{4}/) ||
|
62
|
+
description.match?(%r{^\s*\d{2}/\d{2}}))
|
63
63
|
fixed_file.puts "#{date} - #{description}"
|
64
64
|
else
|
65
65
|
fixed_file.puts line
|
@@ -69,42 +69,42 @@ module MercuryBanking
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
puts "Created fixed ledger file at #{fixed_file_path}"
|
74
74
|
fixed_file_path
|
75
75
|
end
|
76
|
-
|
76
|
+
|
77
77
|
# Generate a balance sheet report using ledger
|
78
78
|
def generate_ledger_balance_report(file_path, end_date = nil)
|
79
79
|
# Construct the ledger command
|
80
80
|
cmd = "ledger -f #{file_path}"
|
81
81
|
cmd += " --end #{end_date}" if end_date
|
82
82
|
cmd += " balance --flat Assets Liabilities Equity"
|
83
|
-
|
83
|
+
|
84
84
|
# Execute the command
|
85
85
|
output = `#{cmd}`
|
86
|
-
|
86
|
+
|
87
87
|
# Check if the command was successful
|
88
88
|
if $?.success?
|
89
89
|
output
|
90
90
|
else
|
91
91
|
puts "Error executing ledger command: #{cmd}"
|
92
92
|
puts "Error output: #{output}"
|
93
|
-
|
93
|
+
|
94
94
|
# Try with a more permissive approach
|
95
95
|
try_permissive_ledger_command(file_path, end_date)
|
96
96
|
end
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
# Try a more permissive ledger command as a fallback
|
100
100
|
def try_permissive_ledger_command(file_path, end_date = nil)
|
101
101
|
puts "Trying with more permissive options..."
|
102
102
|
cmd = "ledger -f #{file_path} --permissive"
|
103
103
|
cmd += " --end #{end_date}" if end_date
|
104
104
|
cmd += " balance --flat Assets Liabilities Equity"
|
105
|
-
|
105
|
+
|
106
106
|
output = `#{cmd}`
|
107
|
-
|
107
|
+
|
108
108
|
if $?.success?
|
109
109
|
output
|
110
110
|
else
|
@@ -113,7 +113,7 @@ module MercuryBanking
|
|
113
113
|
nil
|
114
114
|
end
|
115
115
|
end
|
116
|
-
|
116
|
+
|
117
117
|
# Generate a beancount balance sheet
|
118
118
|
def generate_beancount_balance_sheet(file_path, end_date = nil)
|
119
119
|
# Check if bean-report command exists
|
@@ -121,14 +121,14 @@ module MercuryBanking
|
|
121
121
|
puts "Error: 'bean-report' command not found. Please install beancount (https://beancount.github.io/)."
|
122
122
|
return nil
|
123
123
|
end
|
124
|
-
|
124
|
+
|
125
125
|
# Construct the beancount command
|
126
126
|
cmd = "bean-report #{file_path} balances"
|
127
127
|
cmd += " --end #{end_date}" if end_date
|
128
|
-
|
128
|
+
|
129
129
|
# Execute the command
|
130
130
|
output = `#{cmd}`
|
131
|
-
|
131
|
+
|
132
132
|
# Check if the command was successful
|
133
133
|
if $?.success?
|
134
134
|
output
|
@@ -138,17 +138,17 @@ module MercuryBanking
|
|
138
138
|
nil
|
139
139
|
end
|
140
140
|
end
|
141
|
-
|
141
|
+
|
142
142
|
# Parse balance sheet output to extract account balances
|
143
143
|
def parse_balance_sheet_output(output, format, debug = false)
|
144
144
|
balances = {}
|
145
|
-
|
145
|
+
|
146
146
|
if debug
|
147
147
|
puts "\n=== Balance Sheet Parsing Debug ==="
|
148
148
|
puts "Raw output to parse:"
|
149
149
|
puts output
|
150
150
|
end
|
151
|
-
|
151
|
+
|
152
152
|
case format
|
153
153
|
when 'ledger'
|
154
154
|
balances = parse_ledger_balance_output(output, debug)
|
@@ -157,63 +157,63 @@ module MercuryBanking
|
|
157
157
|
else
|
158
158
|
puts "Unsupported format: #{format}"
|
159
159
|
end
|
160
|
-
|
160
|
+
|
161
161
|
balances
|
162
162
|
end
|
163
|
-
|
163
|
+
|
164
164
|
def parse_ledger_balance_output(output, debug = false)
|
165
165
|
balances = {}
|
166
|
-
|
166
|
+
|
167
167
|
output.each_line do |line|
|
168
168
|
puts "Parsing line: #{line.inspect}" if debug
|
169
169
|
line = line.strip
|
170
|
-
|
170
|
+
|
171
171
|
# Skip empty lines and summary lines
|
172
172
|
if line.empty? || line.include?('----')
|
173
173
|
puts " Skipping line (empty or summary)" if debug
|
174
174
|
next
|
175
175
|
end
|
176
|
-
|
176
|
+
|
177
177
|
# Extract account name and balance using a more flexible regex
|
178
178
|
# The ledger output format is typically something like:
|
179
179
|
# $18.10 Assets:Mercury Checking ••1090
|
180
|
-
if match = line.match(/\$\s*([\d,.-]+)\s+(.+)/)
|
180
|
+
if (match = line.match(/\$\s*([\d,.-]+)\s+(.+)/))
|
181
181
|
balance = match[1].gsub(',', '').to_f
|
182
182
|
account = match[2].strip
|
183
|
-
|
183
|
+
|
184
184
|
puts " Extracted: Account='#{account}', Balance=$#{balance}" if debug
|
185
|
-
|
185
|
+
|
186
186
|
# Add to balances hash
|
187
187
|
balances[account] = balance
|
188
188
|
puts " Added to balances hash" if debug
|
189
|
-
|
190
|
-
puts " No match for balance pattern"
|
189
|
+
elsif debug
|
190
|
+
puts " No match for balance pattern"
|
191
191
|
end
|
192
192
|
end
|
193
|
-
|
193
|
+
|
194
194
|
balances
|
195
195
|
end
|
196
|
-
|
197
|
-
def parse_beancount_balance_output(output,
|
196
|
+
|
197
|
+
def parse_beancount_balance_output(output, _debug = false)
|
198
198
|
balances = {}
|
199
|
-
|
199
|
+
|
200
200
|
output.each_line do |line|
|
201
201
|
# Skip header lines and summary lines
|
202
202
|
next if line.strip.empty? || line.include?('---') || line.include?('Assets:') || line.include?('Liabilities:')
|
203
|
-
|
203
|
+
|
204
204
|
# Extract account name and balance
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
205
|
+
next unless line =~ /(\S.*?)\s+([\d,.-]+)\s+USD/
|
206
|
+
|
207
|
+
account = ::Regexp.last_match(1).strip
|
208
|
+
balance = ::Regexp.last_match(2).gsub(',', '').to_f
|
209
|
+
|
210
|
+
# Add to balances hash
|
211
|
+
balances[account] = balance
|
212
212
|
end
|
213
|
-
|
213
|
+
|
214
214
|
balances
|
215
215
|
end
|
216
|
-
|
216
|
+
|
217
217
|
# Generate ledger reports (P&L, balance sheet, etc.)
|
218
218
|
def generate_ledger_reports(file_path, report_type, category = nil, end_date = nil)
|
219
219
|
# Check if ledger is installed
|
@@ -221,128 +221,120 @@ module MercuryBanking
|
|
221
221
|
puts "Error: 'ledger' command not found. Please install Ledger CLI to generate reports."
|
222
222
|
return
|
223
223
|
end
|
224
|
-
|
224
|
+
|
225
225
|
# Build date filter
|
226
226
|
date_filter = end_date ? " --end #{end_date}" : ""
|
227
|
-
|
227
|
+
|
228
228
|
# Build category filter
|
229
229
|
category_filter = category ? " #{category}" : ""
|
230
|
-
|
230
|
+
|
231
231
|
# Prepare to capture output
|
232
232
|
report_output = ""
|
233
233
|
report_output << "=== Mercury Banking Ledger Reports ===\n\n"
|
234
|
-
|
234
|
+
|
235
235
|
puts "\n=== Mercury Banking Ledger Reports ===\n"
|
236
|
-
|
236
|
+
|
237
237
|
# Generate requested reports
|
238
|
-
if [
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
if [
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
if report_type == 'register'
|
251
|
-
report_output << generate_ledger_register_report_section(file_path, date_filter, category)
|
252
|
-
end
|
253
|
-
|
254
|
-
return report_output
|
255
|
-
end
|
256
|
-
|
238
|
+
report_output << generate_ledger_balance_report_section(file_path, date_filter, category_filter) if %w[all balance].include?(report_type)
|
239
|
+
|
240
|
+
report_output << generate_ledger_pl_report_section(file_path, date_filter, category_filter) if %w[all pl].include?(report_type)
|
241
|
+
|
242
|
+
report_output << generate_ledger_monthly_report_section(file_path, date_filter, category) if %w[all monthly].include?(report_type)
|
243
|
+
|
244
|
+
report_output << generate_ledger_register_report_section(file_path, date_filter, category) if report_type == 'register'
|
245
|
+
|
246
|
+
report_output
|
247
|
+
end
|
248
|
+
|
257
249
|
def generate_ledger_balance_report_section(file_path, date_filter, category_filter)
|
258
250
|
section_output = ""
|
259
|
-
|
251
|
+
|
260
252
|
balance_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Assets Liabilities Equity#{category_filter}"
|
261
253
|
balance_output = `#{balance_cmd}`
|
262
|
-
|
254
|
+
|
263
255
|
puts "\n=== Balance Sheet ===\n"
|
264
256
|
puts balance_output
|
265
|
-
|
257
|
+
|
266
258
|
section_output << "=== Balance Sheet ===\n"
|
267
259
|
section_output << balance_output
|
268
260
|
section_output << "\n"
|
269
|
-
|
261
|
+
|
270
262
|
section_output
|
271
263
|
end
|
272
|
-
|
264
|
+
|
273
265
|
def generate_ledger_pl_report_section(file_path, date_filter, category_filter)
|
274
266
|
section_output = ""
|
275
|
-
|
267
|
+
|
276
268
|
pl_cmd = "ledger -f #{file_path}#{date_filter} balance --flat Income Expenses#{category_filter}"
|
277
269
|
pl_output = `#{pl_cmd}`
|
278
|
-
|
270
|
+
|
279
271
|
puts "\n=== Income Statement ===\n"
|
280
272
|
puts pl_output
|
281
|
-
|
273
|
+
|
282
274
|
section_output << "=== Income Statement ===\n"
|
283
275
|
section_output << pl_output
|
284
276
|
section_output << "\n"
|
285
|
-
|
277
|
+
|
286
278
|
section_output
|
287
279
|
end
|
288
|
-
|
280
|
+
|
289
281
|
def generate_ledger_monthly_report_section(file_path, date_filter, category)
|
290
282
|
section_output = ""
|
291
|
-
|
283
|
+
|
292
284
|
if category
|
293
285
|
monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
|
294
286
|
monthly_output = `#{monthly_cmd}`
|
295
|
-
|
287
|
+
|
296
288
|
puts "\n=== Monthly #{category} ===\n"
|
297
289
|
puts monthly_output
|
298
|
-
|
290
|
+
|
299
291
|
section_output << "=== Monthly #{category} ===\n"
|
300
292
|
section_output << monthly_output
|
301
293
|
section_output << "\n"
|
302
294
|
else
|
303
295
|
# Generate monthly expenses report
|
304
296
|
section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Expenses")
|
305
|
-
|
297
|
+
|
306
298
|
# Generate monthly income report
|
307
299
|
section_output << generate_ledger_monthly_category_report(file_path, date_filter, "Income")
|
308
300
|
end
|
309
|
-
|
301
|
+
|
310
302
|
section_output
|
311
303
|
end
|
312
|
-
|
304
|
+
|
313
305
|
def generate_ledger_monthly_category_report(file_path, date_filter, category)
|
314
306
|
section_output = ""
|
315
|
-
|
307
|
+
|
316
308
|
monthly_cmd = "ledger -f #{file_path}#{date_filter} --monthly --collapse register #{category}"
|
317
309
|
monthly_output = `#{monthly_cmd}`
|
318
|
-
|
310
|
+
|
319
311
|
puts "\n=== Monthly #{category} ===\n"
|
320
312
|
puts monthly_output
|
321
|
-
|
313
|
+
|
322
314
|
section_output << "=== Monthly #{category} ===\n"
|
323
315
|
section_output << monthly_output
|
324
316
|
section_output << "\n"
|
325
|
-
|
317
|
+
|
326
318
|
section_output
|
327
319
|
end
|
328
|
-
|
320
|
+
|
329
321
|
def generate_ledger_register_report_section(file_path, date_filter, category)
|
330
322
|
section_output = ""
|
331
|
-
|
323
|
+
|
332
324
|
register_filter = category || "Assets Liabilities Equity Income Expenses"
|
333
325
|
register_cmd = "ledger -f #{file_path}#{date_filter} register #{register_filter}"
|
334
326
|
register_output = `#{register_cmd}`
|
335
|
-
|
327
|
+
|
336
328
|
puts "\n=== Transaction Register ===\n"
|
337
329
|
puts register_output
|
338
|
-
|
330
|
+
|
339
331
|
section_output << "=== Transaction Register ===\n"
|
340
332
|
section_output << register_output
|
341
333
|
section_output << "\n"
|
342
|
-
|
334
|
+
|
343
335
|
section_output
|
344
336
|
end
|
345
|
-
|
337
|
+
|
346
338
|
# Generate beancount reports
|
347
339
|
def generate_beancount_reports(file_path, report_type, category = nil, end_date = nil)
|
348
340
|
# Check if bean-report is installed
|
@@ -350,101 +342,93 @@ module MercuryBanking
|
|
350
342
|
puts "Error: 'bean-report' command not found. Please install Beancount to generate reports."
|
351
343
|
return
|
352
344
|
end
|
353
|
-
|
345
|
+
|
354
346
|
# Prepare to capture output
|
355
347
|
report_output = ""
|
356
348
|
report_output << "=== Mercury Banking Beancount Reports ===\n\n"
|
357
|
-
|
349
|
+
|
358
350
|
puts "\n=== Mercury Banking Beancount Reports ===\n"
|
359
|
-
|
351
|
+
|
360
352
|
# Build date filter (beancount uses different syntax)
|
361
353
|
date_filter = end_date ? " -e #{end_date}" : ""
|
362
|
-
|
354
|
+
|
363
355
|
# Generate requested reports
|
364
|
-
if [
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
if [
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
if report_type == 'register'
|
377
|
-
report_output << generate_beancount_register_report_section(file_path, date_filter, category)
|
378
|
-
end
|
379
|
-
|
380
|
-
return report_output
|
381
|
-
end
|
382
|
-
|
356
|
+
report_output << generate_beancount_balance_report_section(file_path, date_filter) if %w[all balance].include?(report_type)
|
357
|
+
|
358
|
+
report_output << generate_beancount_pl_report_section(file_path, date_filter) if %w[all pl].include?(report_type)
|
359
|
+
|
360
|
+
report_output << generate_beancount_monthly_report_section(file_path, date_filter) if %w[all monthly].include?(report_type)
|
361
|
+
|
362
|
+
report_output << generate_beancount_register_report_section(file_path, date_filter, category) if report_type == 'register'
|
363
|
+
|
364
|
+
report_output
|
365
|
+
end
|
366
|
+
|
383
367
|
def generate_beancount_balance_report_section(file_path, date_filter)
|
384
368
|
section_output = ""
|
385
|
-
|
369
|
+
|
386
370
|
balance_cmd = "bean-report #{file_path}#{date_filter} balsheet"
|
387
371
|
balance_output = `#{balance_cmd}`
|
388
|
-
|
372
|
+
|
389
373
|
puts "\n=== Balance Sheet ===\n"
|
390
374
|
puts balance_output
|
391
|
-
|
375
|
+
|
392
376
|
section_output << "=== Balance Sheet ===\n"
|
393
377
|
section_output << balance_output
|
394
378
|
section_output << "\n"
|
395
|
-
|
379
|
+
|
396
380
|
section_output
|
397
381
|
end
|
398
|
-
|
382
|
+
|
399
383
|
def generate_beancount_pl_report_section(file_path, date_filter)
|
400
384
|
section_output = ""
|
401
|
-
|
385
|
+
|
402
386
|
pl_cmd = "bean-report #{file_path}#{date_filter} income"
|
403
387
|
pl_output = `#{pl_cmd}`
|
404
|
-
|
388
|
+
|
405
389
|
puts "\n=== Income Statement ===\n"
|
406
390
|
puts pl_output
|
407
|
-
|
391
|
+
|
408
392
|
section_output << "=== Income Statement ===\n"
|
409
393
|
section_output << pl_output
|
410
394
|
section_output << "\n"
|
411
|
-
|
395
|
+
|
412
396
|
section_output
|
413
397
|
end
|
414
|
-
|
398
|
+
|
415
399
|
def generate_beancount_monthly_report_section(file_path, date_filter)
|
416
400
|
section_output = ""
|
417
|
-
|
401
|
+
|
418
402
|
monthly_cmd = "bean-report #{file_path}#{date_filter} monthly"
|
419
403
|
monthly_output = `#{monthly_cmd}`
|
420
|
-
|
404
|
+
|
421
405
|
puts "\n=== Monthly Activity ===\n"
|
422
406
|
puts monthly_output
|
423
|
-
|
407
|
+
|
424
408
|
section_output << "=== Monthly Activity ===\n"
|
425
409
|
section_output << monthly_output
|
426
410
|
section_output << "\n"
|
427
|
-
|
411
|
+
|
428
412
|
section_output
|
429
413
|
end
|
430
|
-
|
414
|
+
|
431
415
|
def generate_beancount_register_report_section(file_path, date_filter, category)
|
432
416
|
section_output = ""
|
433
|
-
|
417
|
+
|
434
418
|
register_filter = category ? " --account #{category}" : ""
|
435
419
|
register_cmd = "bean-report #{file_path}#{date_filter}#{register_filter} register"
|
436
420
|
register_output = `#{register_cmd}`
|
437
|
-
|
421
|
+
|
438
422
|
puts "\n=== Transaction Register ===\n"
|
439
423
|
puts register_output
|
440
|
-
|
424
|
+
|
441
425
|
section_output << "=== Transaction Register ===\n"
|
442
426
|
section_output << register_output
|
443
427
|
section_output << "\n"
|
444
|
-
|
428
|
+
|
445
429
|
section_output
|
446
430
|
end
|
447
|
-
|
431
|
+
|
448
432
|
def export_transactions_to_ledger(transactions, file_path)
|
449
433
|
# Create a temporary file for ledger export
|
450
434
|
File.open(file_path, 'w') do |f|
|
@@ -453,86 +437,90 @@ module MercuryBanking
|
|
453
437
|
write_ledger_transactions(f, transactions)
|
454
438
|
end
|
455
439
|
end
|
456
|
-
|
440
|
+
|
457
441
|
def write_ledger_header(file)
|
458
442
|
file.puts "; Mercury Bank Transactions Export"
|
459
443
|
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
460
444
|
file.puts
|
461
445
|
end
|
462
|
-
|
446
|
+
|
463
447
|
def write_ledger_account_declarations(file, transactions)
|
464
|
-
#
|
465
|
-
|
448
|
+
# First extract account info from transactions
|
449
|
+
account_info = transactions.map do |t|
|
466
450
|
{
|
467
451
|
"name" => t["accountName"] || "Unknown Account",
|
468
452
|
"number" => t["accountNumber"]
|
469
453
|
}
|
470
|
-
end
|
471
|
-
|
454
|
+
end
|
455
|
+
# Then make unique based on name and number
|
456
|
+
accounts = account_info.uniq { |a| [a["name"], a["number"]] }
|
457
|
+
|
472
458
|
# Define accounts dynamically based on transaction data
|
473
459
|
accounts.each do |account|
|
474
460
|
account_name = account["name"]
|
475
461
|
account_number = account["number"]
|
476
|
-
|
462
|
+
|
477
463
|
# Format the account name to include the account number if available
|
478
464
|
formatted_account_name = if account_number
|
479
|
-
"#{account_name} #{account_number.to_s[-4
|
465
|
+
"#{account_name} #{account_number.to_s[-4..]}"
|
480
466
|
else
|
481
467
|
account_name
|
482
468
|
end
|
483
|
-
|
469
|
+
|
484
470
|
file.puts "account Assets:#{formatted_account_name}"
|
485
471
|
end
|
486
472
|
file.puts "account Expenses:Unknown"
|
487
473
|
file.puts "account Income:Unknown"
|
488
474
|
file.puts
|
489
475
|
end
|
490
|
-
|
476
|
+
|
491
477
|
def write_ledger_transactions(file, transactions)
|
492
478
|
# Filter out failed transactions
|
493
479
|
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
494
|
-
|
480
|
+
|
495
481
|
# Sort transactions by date
|
496
482
|
valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
|
497
483
|
write_ledger_transaction(file, t)
|
498
484
|
end
|
499
485
|
end
|
500
|
-
|
486
|
+
|
501
487
|
def write_ledger_transaction(file, transaction)
|
502
|
-
date = transaction["postedAt"]
|
503
|
-
|
504
|
-
|
488
|
+
date = if transaction["postedAt"]
|
489
|
+
Time.parse(transaction["postedAt"]).strftime("%Y/%m/%d")
|
490
|
+
else
|
491
|
+
Time.parse(transaction["createdAt"]).strftime("%Y/%m/%d")
|
492
|
+
end
|
505
493
|
description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
|
506
494
|
amount = transaction["amount"]
|
507
495
|
account_name = transaction["accountName"] || "Unknown Account"
|
508
496
|
account_number = transaction["accountNumber"]
|
509
|
-
|
497
|
+
|
510
498
|
file.puts "#{date} #{description}"
|
511
|
-
file.puts " ; Transaction ID: #{transaction[
|
512
|
-
file.puts " ; Status: #{transaction[
|
499
|
+
file.puts " ; Transaction ID: #{transaction['id']}"
|
500
|
+
file.puts " ; Status: #{transaction['status']}"
|
513
501
|
file.puts " ; Reconciled: No"
|
514
|
-
|
502
|
+
|
515
503
|
write_ledger_postings(file, amount, account_name, account_number)
|
516
504
|
file.puts
|
517
505
|
end
|
518
|
-
|
506
|
+
|
519
507
|
def write_ledger_postings(file, amount, account_name, account_number = nil)
|
520
508
|
# Format the account name to include the account number if available
|
521
509
|
formatted_account_name = if account_number
|
522
|
-
"#{account_name} #{account_number.to_s[-4
|
510
|
+
"#{account_name} #{account_number.to_s[-4..]}"
|
523
511
|
else
|
524
512
|
account_name
|
525
513
|
end
|
526
|
-
|
527
|
-
if amount
|
528
|
-
file.puts " Income:Unknown $-#{format(
|
529
|
-
file.puts " Assets:#{formatted_account_name} $#{format(
|
514
|
+
|
515
|
+
if amount.positive?
|
516
|
+
file.puts " Income:Unknown $-#{format('%.2f', amount)}"
|
517
|
+
file.puts " Assets:#{formatted_account_name} $#{format('%.2f', amount)}"
|
530
518
|
else
|
531
|
-
file.puts " Expenses:Unknown $#{format(
|
532
|
-
file.puts " Assets:#{formatted_account_name} $-#{format(
|
519
|
+
file.puts " Expenses:Unknown $#{format('%.2f', amount.abs)}"
|
520
|
+
file.puts " Assets:#{formatted_account_name} $-#{format('%.2f', amount.abs)}"
|
533
521
|
end
|
534
522
|
end
|
535
|
-
|
523
|
+
|
536
524
|
def export_transactions_to_beancount(transactions, file_path)
|
537
525
|
# Create a temporary file for beancount export
|
538
526
|
File.open(file_path, 'w') do |f|
|
@@ -541,7 +529,7 @@ module MercuryBanking
|
|
541
529
|
write_beancount_transactions(f, transactions)
|
542
530
|
end
|
543
531
|
end
|
544
|
-
|
532
|
+
|
545
533
|
def write_beancount_header(file)
|
546
534
|
file.puts "; Mercury Bank Transactions Export"
|
547
535
|
file.puts "; Generated on #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
@@ -550,85 +538,89 @@ module MercuryBanking
|
|
550
538
|
file.puts "option \"operating_currency\" \"USD\""
|
551
539
|
file.puts
|
552
540
|
end
|
553
|
-
|
541
|
+
|
554
542
|
def write_beancount_account_declarations(file, transactions)
|
555
|
-
#
|
556
|
-
|
543
|
+
# First extract account info from transactions
|
544
|
+
account_info = transactions.map do |t|
|
557
545
|
{
|
558
546
|
"name" => t["accountName"] || "Unknown Account",
|
559
547
|
"number" => t["accountNumber"]
|
560
548
|
}
|
561
|
-
end
|
562
|
-
|
549
|
+
end
|
550
|
+
# Then make unique based on name and number
|
551
|
+
accounts = account_info.uniq { |a| [a["name"], a["number"]] }
|
552
|
+
|
563
553
|
# Define accounts dynamically based on transaction data
|
564
554
|
accounts.each do |account|
|
565
555
|
account_name = account["name"]
|
566
556
|
account_number = account["number"]
|
567
|
-
|
557
|
+
|
568
558
|
# Sanitize account name for beancount format
|
569
559
|
safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
|
570
|
-
|
560
|
+
|
571
561
|
# Format the account name to include the account number if available
|
572
562
|
formatted_account_name = if account_number
|
573
|
-
"#{safe_account_name}-#{account_number.to_s[-4
|
563
|
+
"#{safe_account_name}-#{account_number.to_s[-4..]}"
|
574
564
|
else
|
575
565
|
safe_account_name
|
576
566
|
end
|
577
|
-
|
567
|
+
|
578
568
|
file.puts "1970-01-01 open Assets:#{formatted_account_name}"
|
579
569
|
end
|
580
570
|
file.puts "1970-01-01 open Expenses:Unknown"
|
581
571
|
file.puts "1970-01-01 open Income:Unknown"
|
582
572
|
file.puts
|
583
573
|
end
|
584
|
-
|
574
|
+
|
585
575
|
def write_beancount_transactions(file, transactions)
|
586
576
|
# Filter out failed transactions
|
587
577
|
valid_transactions = transactions.reject { |t| t["status"] == "failed" }
|
588
|
-
|
578
|
+
|
589
579
|
# Sort transactions by date
|
590
580
|
valid_transactions.sort_by! { |t| t["postedAt"] || t["createdAt"] }.each do |t|
|
591
581
|
write_beancount_transaction(file, t)
|
592
582
|
end
|
593
583
|
end
|
594
|
-
|
584
|
+
|
595
585
|
def write_beancount_transaction(file, transaction)
|
596
|
-
date = transaction["postedAt"]
|
597
|
-
|
598
|
-
|
586
|
+
date = if transaction["postedAt"]
|
587
|
+
Time.parse(transaction["postedAt"]).strftime("%Y-%m-%d")
|
588
|
+
else
|
589
|
+
Time.parse(transaction["createdAt"]).strftime("%Y-%m-%d")
|
590
|
+
end
|
599
591
|
description = transaction["bankDescription"] || transaction["externalMemo"] || "Unknown transaction"
|
600
592
|
amount = transaction["amount"]
|
601
593
|
account_name = transaction["accountName"] || "Unknown Account"
|
602
594
|
account_number = transaction["accountNumber"]
|
603
|
-
|
595
|
+
|
604
596
|
# Sanitize account name for beancount format
|
605
597
|
safe_account_name = account_name.gsub(/[^a-zA-Z0-9]/, '-')
|
606
|
-
|
598
|
+
|
607
599
|
file.puts "#{date} * \"#{description}\""
|
608
|
-
file.puts " ; Transaction ID: #{transaction[
|
609
|
-
file.puts " ; Status: #{transaction[
|
600
|
+
file.puts " ; Transaction ID: #{transaction['id']}"
|
601
|
+
file.puts " ; Status: #{transaction['status']}"
|
610
602
|
file.puts " ; Reconciled: No"
|
611
|
-
|
603
|
+
|
612
604
|
write_beancount_postings(file, amount, safe_account_name, account_number)
|
613
605
|
file.puts
|
614
606
|
end
|
615
|
-
|
607
|
+
|
616
608
|
def write_beancount_postings(file, amount, account_name, account_number = nil)
|
617
609
|
# Format the account name to include the account number if available
|
618
610
|
formatted_account_name = if account_number
|
619
|
-
"#{account_name}-#{account_number.to_s[-4
|
611
|
+
"#{account_name}-#{account_number.to_s[-4..]}"
|
620
612
|
else
|
621
613
|
account_name
|
622
614
|
end
|
623
|
-
|
624
|
-
if amount
|
625
|
-
file.puts " Income:Unknown -#{format(
|
626
|
-
file.puts " Assets:#{formatted_account_name} #{format(
|
615
|
+
|
616
|
+
if amount.positive?
|
617
|
+
file.puts " Income:Unknown -#{format('%.2f', amount)} USD"
|
618
|
+
file.puts " Assets:#{formatted_account_name} #{format('%.2f', amount)} USD"
|
627
619
|
else
|
628
|
-
file.puts " Expenses:Unknown #{format(
|
629
|
-
file.puts " Assets:#{formatted_account_name} -#{format(
|
620
|
+
file.puts " Expenses:Unknown #{format('%.2f', amount.abs)} USD"
|
621
|
+
file.puts " Assets:#{formatted_account_name} -#{format('%.2f', amount.abs)} USD"
|
630
622
|
end
|
631
623
|
end
|
632
624
|
end
|
633
625
|
end
|
634
|
-
end
|
626
|
+
end
|