thefox-wallet 0.8.1

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.
@@ -0,0 +1,4 @@
1
+
2
+ require 'wallet/version'
3
+ require 'wallet/wallet'
4
+ require 'wallet/entry'
@@ -0,0 +1,102 @@
1
+
2
+ module TheFox
3
+ module Wallet
4
+
5
+ class Entry
6
+
7
+ attr_reader :title
8
+ attr_reader :date
9
+ attr_reader :revenue
10
+ attr_reader :expense
11
+ attr_reader :balance
12
+ attr_reader :category
13
+ attr_reader :comment
14
+
15
+ def initialize(title = '', date = Date.today, revenue = 0.0, expense = 0.0, category = 'default', comment = '')
16
+ revenue_t = revenue.to_f
17
+ expense_t = expense.to_f
18
+
19
+ self.title = title
20
+ self.date = date
21
+
22
+ @revenue = 0.0
23
+ @expense = 0.0
24
+ @balance = 0.0
25
+
26
+ if revenue_t < 0 && expense_t == 0
27
+ self.revenue = 0.0
28
+ self.expense = revenue_t
29
+ else
30
+ self.revenue = revenue_t
31
+ self.expense = expense_t
32
+ end
33
+
34
+ self.category = category
35
+ self.comment = comment
36
+ end
37
+
38
+ def title=(title)
39
+ @title = title.to_s
40
+ end
41
+
42
+ def date=(date)
43
+ if date.is_a? String
44
+ @date = Date.parse(date)
45
+ elsif date.is_a? Date
46
+ @date = date
47
+ else
48
+ raise ArgumentError, 'date must be a String or a Date instance'
49
+ end
50
+ end
51
+
52
+ def revenue=(revenue)
53
+ revenue_t = revenue.to_f
54
+
55
+ if revenue_t < 0
56
+ raise RangeError, 'revenue (' + revenue_t.to_s + ') cannot be < 0. use expense instead!'
57
+ end
58
+
59
+ @revenue = revenue_t
60
+ calc_balance()
61
+ end
62
+
63
+ def expense=(expense)
64
+ expense_t = expense.to_f
65
+
66
+ if expense_t > 0
67
+ raise RangeError, 'expense (' + expense_t.to_s + ') cannot be > 0. use revenue instead!'
68
+ end
69
+
70
+ @expense = expense_t
71
+ calc_balance()
72
+ end
73
+
74
+ def category=(category)
75
+ @category = category.nil? ? 'default' : category.to_s
76
+ end
77
+
78
+ def comment=(comment)
79
+ @comment = comment.nil? ? '' : comment.to_s
80
+ end
81
+
82
+ def to_h
83
+ {
84
+ 'title' => @title,
85
+ 'date' => @date.to_s,
86
+ 'revenue' => @revenue,
87
+ 'expense' => @expense,
88
+ 'balance' => @balance,
89
+ 'category' => @category,
90
+ 'comment' => @comment,
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ def calc_balance
97
+ @balance = (@revenue.round(3) + @expense.round(3)).to_f.round(3)
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module TheFox
3
+ module Wallet
4
+ NAME = 'Wallet'
5
+ VERSION = '0.8.1'
6
+ DATE = '2015-12-07'
7
+ HOMEPAGE = 'https://github.com/TheFox/wallet'
8
+ NUMBER_FORMAT = '%.2f'
9
+ end
10
+ end
@@ -0,0 +1,848 @@
1
+
2
+ # Schmeisst die Fuffies durch den Club und schreit Boah Boah!
3
+
4
+ require 'yaml'
5
+ require 'yaml/store'
6
+ require 'csv'
7
+ require 'ostruct'
8
+
9
+
10
+ module TheFox
11
+ module Wallet
12
+
13
+ class Wallet
14
+
15
+ attr_reader :html_path
16
+
17
+ def initialize(dir_path = 'wallet')
18
+ @exit = false
19
+ @dir_path = dir_path
20
+ @data_path = File.expand_path('data', @dir_path)
21
+ @html_path = File.expand_path('html', @dir_path)
22
+ @tmp_path = File.expand_path('tmp', @dir_path)
23
+
24
+ @has_transaction = false
25
+ @transaction_files = {}
26
+
27
+ Signal.trap('SIGINT') do
28
+ puts
29
+ puts 'received SIGINT. break ...'
30
+ @exit = true
31
+ end
32
+ end
33
+
34
+ def add(entry)
35
+ if !entry.is_a? Entry
36
+ raise ArgumentError, 'variable must be a Entry instance'
37
+ end
38
+
39
+ date = entry.date
40
+ date_s = date.to_s
41
+ dbfile_basename = 'month_' + date.strftime('%Y_%m') + '.yml'
42
+ dbfile_path = File.expand_path(dbfile_basename, @data_path)
43
+ tmpfile_path = dbfile_path + '.tmp'
44
+ file = {
45
+ 'meta' => {
46
+ 'version' => 1,
47
+ 'created_at' => DateTime.now.to_s,
48
+ 'updated_at' => DateTime.now.to_s,
49
+ },
50
+ 'days' => {}
51
+ }
52
+
53
+ # puts 'dbfile_basename: ' + dbfile_basename
54
+ # puts 'dbfile_path: ' + dbfile_path
55
+ # puts 'tmpfile_path: ' + tmpfile_path
56
+ # puts
57
+
58
+ if @has_transaction
59
+ if @transaction_files.has_key? dbfile_basename
60
+ file = @transaction_files[dbfile_basename]['file']
61
+ else
62
+ if File.exist? dbfile_path
63
+ file = YAML.load_file(dbfile_path)
64
+ file['meta']['updated_at'] = DateTime.now.to_s
65
+ end
66
+
67
+ @transaction_files[dbfile_basename] = {
68
+ 'basename' => dbfile_basename,
69
+ 'path' => dbfile_path,
70
+ 'tmp_path' => tmpfile_path,
71
+ 'file' => file,
72
+ }
73
+ end
74
+
75
+ if !file['days'].has_key? date_s
76
+ file['days'][date_s] = []
77
+ end
78
+
79
+ file['days'][date_s].push entry.to_h
80
+
81
+ @transaction_files[dbfile_basename]['file'] = file
82
+ else
83
+ create_dirs()
84
+
85
+ if File.exist? dbfile_path
86
+ file = YAML.load_file(dbfile_path)
87
+ file['meta']['updated_at'] = DateTime.now.to_s
88
+ end
89
+
90
+ if !file['days'].has_key? date_s
91
+ file['days'][date_s] = []
92
+ end
93
+
94
+ file['days'][date_s].push entry.to_h
95
+
96
+ store = YAML::Store.new tmpfile_path
97
+ store.transaction do
98
+ store['meta'] = file['meta']
99
+ store['days'] = file['days']
100
+ end
101
+
102
+ if File.exist? tmpfile_path
103
+ File.rename tmpfile_path, dbfile_path
104
+ end
105
+ end
106
+ end
107
+
108
+ def transaction_start
109
+ @has_transaction = true
110
+ @transaction_files = {}
111
+
112
+ create_dirs()
113
+ end
114
+
115
+ def transaction_end
116
+ catch(:done) do
117
+ @transaction_files.each do |tr_file_key, tr_file_data|
118
+ throw :done if @exit
119
+ # puts 'keys left: ' + @transaction_files.keys.count.to_s
120
+ # puts 'tr_file_key: ' + tr_file_key
121
+ # puts 'path: ' + tr_file_data['path']
122
+ # puts 'tmp_path: ' + tr_file_data['tmp_path']
123
+ # puts
124
+
125
+ store = YAML::Store.new tr_file_data['tmp_path']
126
+ store.transaction do
127
+ store['meta'] = tr_file_data['file']['meta']
128
+ store['days'] = tr_file_data['file']['days']
129
+ end
130
+ @transaction_files.delete tr_file_key
131
+
132
+ if File.exist? tr_file_data['tmp_path']
133
+ File.rename tr_file_data['tmp_path'], tr_file_data['path']
134
+ end
135
+ end
136
+ end
137
+
138
+ @has_transaction = false
139
+ @transaction_files = {}
140
+ end
141
+
142
+ def sum(year = nil, month = nil, day = nil, category = nil)
143
+ year_s = year.to_i.to_s
144
+ month_f = '%02d' % month.to_i
145
+ day_f = '%02d' % day.to_i
146
+
147
+ revenue = 0.0
148
+ expense = 0.0
149
+ balance = 0.0
150
+
151
+ glob = @data_path + '/month_'
152
+ if year == nil && month == nil
153
+ glob += '*.yml'
154
+ elsif year && month == nil
155
+ glob += year_s + '_*.yml'
156
+ elsif year && month
157
+ glob += year_s + '_' + month_f + '.yml'
158
+ end
159
+
160
+ Dir[glob].each do |file_path|
161
+ data = YAML.load_file(file_path)
162
+
163
+ if day
164
+ day_key = year_s + '-' + month_f + '-' + day_f
165
+ if data['days'].has_key?(day_key)
166
+ day_sum = calc_day(data['days'][day_key], category)
167
+ revenue += day_sum[:revenue]
168
+ expense += day_sum[:expense]
169
+ balance += day_sum[:balance]
170
+ end
171
+ else
172
+ data['days'].each do |day_name, day_items|
173
+ day_sum = calc_day(day_items, category)
174
+ revenue += day_sum[:revenue]
175
+ expense += day_sum[:expense]
176
+ balance += day_sum[:balance]
177
+ end
178
+ end
179
+ end
180
+
181
+ revenue = revenue.to_f.round(3)
182
+ expense = expense.to_f.round(3)
183
+ balance = (revenue + expense).round(3)
184
+
185
+ diff = revenue + expense - balance
186
+ if diff != 0
187
+ raise RuntimeError, 'diff between revenue and expense to balance is ' + diff.to_s
188
+ end
189
+
190
+ {
191
+ :revenue => revenue,
192
+ :expense => expense,
193
+ :balance => balance,
194
+ }
195
+ end
196
+
197
+ def sum_category(category)
198
+ sum(nil, nil, nil, category)
199
+ end
200
+
201
+ def entries(year = nil, month = nil, day = nil, category = nil)
202
+ year_s = year.to_i.to_s
203
+ month_f = '%02d' % month.to_i
204
+ day_f = '%02d' % day.to_i
205
+
206
+ glob = @data_path + '/month_'
207
+ if year == nil && month == nil
208
+ glob += '*.yml'
209
+ elsif year && month == nil
210
+ glob += year_s + '_*.yml'
211
+ elsif year && month
212
+ glob += year_s + '_' + month_f + '.yml'
213
+ end
214
+
215
+ # puts 'glob: ' + glob
216
+ # puts 'year: ' + '%-10s' % year.class.to_s + ' = "' + year.to_s + '"'
217
+ # puts 'month: ' + '%-10s' % month.class.to_s + ' = "' + month.to_s + '"'
218
+ # puts 'day: ' + '%-10s' % day.class.to_s + ' = "' + day.to_s + '"'
219
+ # puts 'category: ' + '%-10s' % category.class.to_s + ' = "' + category.to_s + '"'
220
+ # puts
221
+
222
+ entries_a = {}
223
+ Dir[glob].each do |file_path|
224
+ data = YAML.load_file(file_path)
225
+ if category.nil? || category.to_s.length == 0
226
+ if day
227
+ day_key = year_s + '-' + month_f + '-' + day_f
228
+ if data['days'].has_key?(day_key)
229
+ entries_a[day_key] = data['days'][day_key]
230
+ end
231
+ else
232
+ entries_a.merge! data['days']
233
+ end
234
+ else
235
+ category = category.to_s.downcase
236
+ if day
237
+ day_key = year_s + '-' + month_f + '-' + day_f
238
+ if data['days'].has_key?(day_key)
239
+ entries_a[day_key] = data['days'][day_key].keep_if{ |day_item|
240
+ day_item['category'].downcase == category
241
+ }
242
+ end
243
+ else
244
+ entries_a.merge! data['days'].map{ |day_name, day_items|
245
+ day_items.keep_if{ |day_item|
246
+ day_item['category'].downcase == category
247
+ }
248
+ [day_name, day_items]
249
+ }.to_h.keep_if{ |day_name, day_items|
250
+ day_items.count > 0
251
+ }
252
+ end
253
+ end
254
+
255
+ end
256
+ entries_a
257
+ end
258
+
259
+ def categories
260
+ categories_h = {}
261
+ Dir[@data_path + '/month_*.yml'].each do |file_path|
262
+ data = YAML.load_file(file_path)
263
+
264
+ data['days'].each do |day_name, day_items|
265
+ day_items.each do |entry|
266
+ category_t = entry['category']
267
+ if category_t.length > 0
268
+ categories_h[category_t] = true
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ categories_a = categories_h.keys.sort{ |a, b| a.downcase <=> b.downcase }
275
+ default_index = categories_a.index('default')
276
+ if !default_index.nil?
277
+ categories_a.delete_at(categories_a.index('default'))
278
+ end
279
+ categories_a.unshift('default')
280
+ categories_a
281
+ end
282
+
283
+ def gen_html
284
+ create_dirs()
285
+
286
+ html_options_path = "#{@html_path}/options.yml"
287
+ html_options = {
288
+ 'meta' => {
289
+ 'version' => 1,
290
+ 'created_at' => DateTime.now.to_s,
291
+ 'updated_at' => DateTime.now.to_s,
292
+ },
293
+ 'changes' => {},
294
+ }
295
+ if Dir.exist? @html_path
296
+ if File.exist? html_options_path
297
+ html_options = YAML.load_file(html_options_path)
298
+ html_options['meta']['updated_at'] = DateTime.now.to_s
299
+ end
300
+ else
301
+ Dir.mkdir(@html_path)
302
+ end
303
+
304
+ categories_available = categories()
305
+
306
+ categories_total_balance = {}
307
+ categories_available.map{ |item| categories_total_balance[item] = 0.0 }
308
+
309
+ gitignore_file = File.open(@html_path + '/.gitignore', 'w')
310
+ gitignore_file.write('*')
311
+ gitignore_file.close
312
+
313
+ css_file_path = @html_path + '/style.css'
314
+ css_file = File.open(css_file_path, 'w')
315
+ css_file.write('
316
+ html {
317
+ -webkit-text-size-adjust: none;
318
+ }
319
+ table.list, table.list th, table.list td {
320
+ border: 1px solid black;
321
+ }
322
+ th.left, td.left {
323
+ text-align: left;
324
+ }
325
+ th.right, td.right {
326
+ text-align: right;
327
+ }
328
+ th.first_column {
329
+ min-width: 180px;
330
+ width: 180px;
331
+ }
332
+ th.red, td.red {
333
+ color: #ff0000;
334
+ }
335
+ ')
336
+ css_file.close
337
+
338
+ index_file_path = @html_path + '/index.html'
339
+ index_file = File.open(index_file_path, 'w')
340
+ index_file.write('
341
+ <html>
342
+ <head>
343
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
344
+ <title>' + @dir_path + '</title>
345
+ <link rel="stylesheet" href="style.css" type="text/css" />
346
+ </head>
347
+ <body>
348
+ <h1>' + @dir_path + '</h1>
349
+ <p>Generated @ ' + DateTime.now.strftime('%Y-%m-%d %H:%M:%S') + ' by <a href="' + ::TheFox::Wallet::HOMEPAGE + '">' + ::TheFox::Wallet::NAME + '</a> ' + ::TheFox::Wallet::VERSION + '</p>
350
+ ')
351
+
352
+ years_total = {}
353
+ years.each do |year|
354
+ year_s = year.to_s
355
+ year_file_name = 'year_' + year_s + '.html'
356
+ year_file_path = @html_path + '/' + year_file_name
357
+
358
+ year_file = File.open(year_file_path, 'w')
359
+ year_file.write('
360
+ <html>
361
+ <head>
362
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
363
+ <title>' + year_s + ' - ' + @dir_path + '</title>
364
+ <link rel="stylesheet" href="style.css" type="text/css" />
365
+ </head>
366
+ <body>
367
+ <h1><a href="index.html">' + @dir_path + '</a></h1>
368
+ <p>Generated @ ' + DateTime.now.strftime('%Y-%m-%d %H:%M:%S') + ' by <a href="' + ::TheFox::Wallet::HOMEPAGE + '">' + ::TheFox::Wallet::NAME + '</a> ' + ::TheFox::Wallet::VERSION + '</p>
369
+
370
+ <h2>Year: ' + year_s + '</h2>
371
+ <table class="list">
372
+ <tr>
373
+ <th class="left">Month</th>
374
+ <th class="right">Revenue</th>
375
+ <th class="right">Expense</th>
376
+ <th class="right">Balance</th>
377
+ <th colspan="' + categories_available.count.to_s + '">' + categories_available.count.to_s + ' Categories</th>
378
+ </tr>
379
+ <tr>
380
+ <th colspan="4">&nbsp;</th>
381
+ ')
382
+ categories_available.each do |category|
383
+ year_file.write('<th class="right">' + category + '</th>')
384
+ end
385
+ year_file.write('</tr>')
386
+
387
+ revenue_year = 0.0
388
+ expense_year = 0.0
389
+ balance_year = 0.0
390
+ categories_year_balance = {}
391
+ categories_available.map{ |item| categories_year_balance[item] = 0.0 }
392
+ year_total = {}
393
+
394
+ puts 'generate year ' + year_s
395
+ Dir[@data_path + '/month_' + year_s + '_*.yml'].each do |file_path|
396
+ file_name = File.basename(file_path)
397
+ month_n = file_name[11, 2]
398
+ month_file_name = 'month_' + year_s + '_' + month_n + '.html'
399
+ month_file_path = @html_path + '/' + month_file_name
400
+
401
+ month_s = Date.parse('2015-' + month_n + '-15').strftime('%B')
402
+
403
+ revenue_month = 0.0
404
+ expense_month = 0.0
405
+ balance_month = 0.0
406
+ categories_month_balance = {}
407
+ categories_available.map{ |item| categories_month_balance[item] = 0.0 }
408
+
409
+ entry_n = 0
410
+ data = YAML.load_file(file_path)
411
+
412
+ generate_html = false
413
+ if html_options['changes'].has_key? file_name
414
+ if html_options['changes'][file_name]['updated_at'] != data['meta']['updated_at']
415
+ html_options['changes'][file_name]['updated_at'] = data['meta']['updated_at']
416
+ generate_html = true
417
+ end
418
+ else
419
+ html_options['changes'][file_name] = {
420
+ 'updated_at' => data['meta']['updated_at'],
421
+ }
422
+ generate_html = true
423
+ end
424
+ if !File.exist?(month_file_path)
425
+ generate_html = true
426
+ end
427
+
428
+ if generate_html
429
+ puts "\t" + 'file: ' + month_file_name + ' (from ' + file_name + ')'
430
+
431
+ month_file = File.open(month_file_path, 'w')
432
+ month_file.write('
433
+ <html>
434
+ <head>
435
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
436
+ <title>' + month_s + ' ' + year_s + ' - ' + @dir_path + '</title>
437
+ <link rel="stylesheet" href="style.css" type="text/css" />
438
+ </head>
439
+ <body>
440
+ <h1><a href="index.html">' + @dir_path + '</a></h1>
441
+ <p>Generated @ ' + DateTime.now.strftime('%Y-%m-%d %H:%M:%S') + ' by <a href="' + ::TheFox::Wallet::HOMEPAGE + '">' + ::TheFox::Wallet::NAME + '</a> ' + ::TheFox::Wallet::VERSION + ' from <code>' + file_name + '</code></p>
442
+
443
+ <h2>Month: ' + month_s + ' <a href="' + year_file_name + '">' + year_s + '</a></h2>
444
+ <table class="list">
445
+ <tr>
446
+ <th class="left">#</th>
447
+ <th class="left">Date</th>
448
+ <th class="left first_column">Title</th>
449
+ <th class="right">Revenue</th>
450
+ <th class="right">Expense</th>
451
+ <th class="right">Balance</th>
452
+ <th class="right">Category</th>
453
+ <th class="left">Comment</th>
454
+ </tr>
455
+ ')
456
+ end
457
+
458
+ data['days'].sort.each do |day_name, day_items|
459
+ #puts "\t\t" + 'day: ' + day_name
460
+ day_items.each do |entry|
461
+ entry_n += 1
462
+ revenue_month += entry['revenue']
463
+ expense_month += entry['expense']
464
+ balance_month += entry['balance']
465
+
466
+ categories_year_balance[entry['category']] += entry['balance']
467
+ categories_month_balance[entry['category']] += entry['balance']
468
+
469
+ revenue_out = entry['revenue'] > 0 ? ::TheFox::Wallet::NUMBER_FORMAT % entry['revenue'] : '&nbsp;'
470
+ expense_out = entry['expense'] < 0 ? ::TheFox::Wallet::NUMBER_FORMAT % entry['expense'] : '&nbsp;'
471
+ category_out = entry['category'] == 'default' ? '&nbsp;' : entry['category']
472
+ comment_out = entry['comment'] == '' ? '&nbsp;' : entry['comment']
473
+
474
+ if generate_html
475
+ month_file.write('
476
+ <tr>
477
+ <td valign="top" class="left">' + entry_n.to_s + '</td>
478
+ <td valign="top" class="left">' + Date.parse(entry['date']).strftime('%d.%m.%y') + '</td>
479
+ <td valign="top" class="left">' + entry['title'][0, 50] + '</td>
480
+ <td valign="top" class="right">' + revenue_out + '</td>
481
+ <td valign="top" class="right red">' + expense_out + '</td>
482
+ <td valign="top" class="right ' + (entry['balance'] < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % entry['balance'] + '</td>
483
+ <td valign="top" class="right">' + category_out + '</td>
484
+ <td valign="top" class="left">' + comment_out + '</td>
485
+ </tr>
486
+ ')
487
+ end
488
+ end
489
+ end
490
+
491
+ revenue_year += revenue_month
492
+ expense_year += expense_month
493
+ balance_year += balance_month
494
+
495
+ revenue_month_r = revenue_month.round(3)
496
+ expense_month_r = expense_month.round(3)
497
+ balance_month_r = balance_month.round(3)
498
+
499
+ year_total[month_n] = ::OpenStruct.new({
500
+ month: month_n.to_i,
501
+ month_s: '%02d' % month_n.to_i,
502
+ revenue: revenue_month_r,
503
+ expense: expense_month_r,
504
+ balance: balance_month_r,
505
+ })
506
+
507
+ balance_class = ''
508
+ if balance_month < 0
509
+ balance_class = 'red'
510
+ end
511
+ if generate_html
512
+ month_file.write('
513
+ <tr>
514
+ <th>&nbsp;</th>
515
+ <th>&nbsp;</th>
516
+ <th class="left"><b>TOTAL</b></th>
517
+ <th class="right">' + ::TheFox::Wallet::NUMBER_FORMAT % revenue_month + '</th>
518
+ <th class="right red">' + ::TheFox::Wallet::NUMBER_FORMAT % expense_month + '</th>
519
+ <th class="right ' + balance_class + '">' + ::TheFox::Wallet::NUMBER_FORMAT % balance_month + '</th>
520
+ <th>&nbsp;</th>
521
+ <th>&nbsp;</th>
522
+ </tr>
523
+ </table>')
524
+ month_file.write('</body></html>')
525
+ month_file.close
526
+ end
527
+
528
+ year_file.write('
529
+ <tr>
530
+ <td class="left"><a href="' + month_file_name + '">' + month_s + '</a></td>
531
+ <td class="right">' + ::TheFox::Wallet::NUMBER_FORMAT % revenue_month + '</td>
532
+ <td class="right red">' + ::TheFox::Wallet::NUMBER_FORMAT % expense_month + '</td>
533
+ <td class="right ' + balance_class + '">' + ::TheFox::Wallet::NUMBER_FORMAT % balance_month + '</td>')
534
+ categories_available.each do |category|
535
+ category_balance = categories_month_balance[category]
536
+ year_file.write('<td class="right ' + (category_balance < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % category_balance + '</td>')
537
+ end
538
+ year_file.write('</tr>')
539
+ end
540
+
541
+ year_total.sort.inject(0.0){ |sum, item| item[1].balance_total = (sum + item[1].balance).round(3) }
542
+
543
+ year_file.write('
544
+ <tr>
545
+ <th class="left"><b>TOTAL</b></th>
546
+ <th class="right">' + ::TheFox::Wallet::NUMBER_FORMAT % revenue_year + '</th>
547
+ <th class="right red">' + ::TheFox::Wallet::NUMBER_FORMAT % expense_year + '</th>
548
+ <th class="right ' + (balance_year < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % balance_year + '</th>')
549
+ categories_available.each do |category|
550
+ category_balance = categories_year_balance[category]
551
+ year_file.write('<td class="right ' + (category_balance < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % category_balance + '</td>')
552
+ end
553
+
554
+ year_file.write('
555
+ </tr>
556
+ </table>
557
+ ')
558
+
559
+ year_file.write("<p><img src=\"year_#{year_s}.png\"></p>")
560
+ year_file.write('</body></html>')
561
+ year_file.close
562
+
563
+ yeardat_file_path = "#{@tmp_path}/year_#{year_s}.dat"
564
+ yeardat_file = File.new(yeardat_file_path, 'w')
565
+ yeardat_file.write(year_total
566
+ .map{ |k, m| "#{year_s}-#{m.month_s} #{m.revenue} #{m.expense} #{m.balance} #{m.balance_total} #{m.balance_total}" }
567
+ .join("\n"))
568
+ yeardat_file.close
569
+
570
+ # year_max = year_total
571
+ # .map{ |k, m| [m.revenue, m.balance, m.balance_total] }
572
+ # .flatten
573
+ # .max
574
+ # .to_i
575
+
576
+ # year_min = year_total
577
+ # .map{ |k, m| [m.expense, m.balance, m.balance_total] }
578
+ # .flatten
579
+ # .min
580
+ # .to_i
581
+ # .abs
582
+
583
+ # year_max_rl = year_max.to_s.length - 2
584
+ # year_max_r = year_max.round(-year_max_rl)
585
+ # year_max_d = year_max_r - year_max
586
+ # year_max_r = year_max_r + 5 * 10 ** (year_max_rl - 1) if year_max_r < year_max
587
+ # year_max_r += 100
588
+
589
+ # year_min_rl = year_min.to_s.length - 2
590
+ # year_min_r = year_min.round(-year_min_rl)
591
+ # year_min_d = year_min_r - year_min
592
+ # year_min_r = year_min_r + 5 * 10 ** (year_min_rl - 1) if year_min_r < year_min
593
+ # year_min_r += 100
594
+
595
+ # puts "#{year_max} #{year_max.to_s.length} #{year_max_r} #{year_max_rl}"
596
+ # puts "#{year_min} #{year_min.to_s.length} #{year_min_r} #{year_min_rl}"
597
+
598
+ gnuplot_file = File.new("#{@tmp_path}/year_#{year_s}.gp", 'w')
599
+ gnuplot_file.puts("set title 'Year #{year_s}'")
600
+ gnuplot_file.puts("set xlabel 'Months'")
601
+ gnuplot_file.puts("set ylabel 'Euro'")
602
+ gnuplot_file.puts("set grid")
603
+ gnuplot_file.puts("set key below center horizontal noreverse enhanced autotitle box dashtype solid")
604
+ gnuplot_file.puts("set tics out nomirror")
605
+ gnuplot_file.puts("set border 3 front linetype black linewidth 1.0 dashtype solid")
606
+
607
+ gnuplot_file.puts("set timefmt '%Y-%m'")
608
+ gnuplot_file.puts("set xdata time")
609
+ gnuplot_file.puts("set format x '%b'")
610
+ gnuplot_file.puts("set xrange ['#{year_s}-01-01':'#{year_s}-12-31']")
611
+ gnuplot_file.puts("set xtics '#{year_s}-01-01', 2592000, '#{year_s}-12-31'")
612
+ # gnuplot_file.puts("set yrange [-#{year_min_r}:#{year_max_r}]")
613
+ gnuplot_file.puts("set autoscale y")
614
+
615
+ gnuplot_file.puts("set style line 1 linecolor rgb '#00ff00' linewidth 2 linetype 1 pointtype 2")
616
+ gnuplot_file.puts("set style line 2 linecolor rgb '#ff0000' linewidth 2 linetype 1 pointtype 2")
617
+ gnuplot_file.puts("set style line 3 linecolor rgb '#000000' linewidth 2 linetype 1 pointtype 2")
618
+ gnuplot_file.puts("set style line 4 linecolor rgb '#0000ff' linewidth 2 linetype 1 pointtype 2")
619
+ gnuplot_file.puts("set style data linespoints")
620
+ gnuplot_file.puts("set terminal png enhanced")
621
+ gnuplot_file.puts("set output '#{@html_path}/year_#{year_s}.png'")
622
+ gnuplot_file.puts("plot sum = 0, \\")
623
+ gnuplot_file.puts("\t'#{yeardat_file_path}' using 1:2 linestyle 1 title 'Revenue', \\")
624
+ gnuplot_file.puts("\t'' using 1:3 linestyle 2 title 'Expense', \\")
625
+ gnuplot_file.puts("\t'' using 1:4 linestyle 3 title 'Balance', \\")
626
+ gnuplot_file.puts("\t'' using 1:5 linestyle 4 title '∑ Balance'")
627
+ gnuplot_file.close
628
+ system("gnuplot #{@tmp_path}/year_#{year_s}.gp")
629
+
630
+ years_total[year_s] = ::OpenStruct.new({
631
+ year: year_s,
632
+ revenue: revenue_year.round(3),
633
+ expense: expense_year.round(3),
634
+ balance: balance_year.round(3),
635
+ })
636
+ end
637
+
638
+ years_total.sort.inject(0.0){ |sum, item| item[1].balance_total = (sum + item[1].balance).round(3) }
639
+
640
+ index_file.write('
641
+ <table class="list">
642
+ <tr>
643
+ <th class="left">Year</th>
644
+ <th class="right">Revenue</th>
645
+ <th class="right">Expense</th>
646
+ <th class="right">Balance</th>
647
+ <th class="right">Balance &#8721;</th>
648
+ </tr>')
649
+ years_total.each do |year_name, year_data|
650
+ index_file.write('
651
+ <tr>
652
+ <td class="left"><a href="year_' + year_name + '.html">' + year_name + '</a></td>
653
+ <td class="right">' + ::TheFox::Wallet::NUMBER_FORMAT % year_data.revenue + '</td>
654
+ <td class="right red">' + ::TheFox::Wallet::NUMBER_FORMAT % year_data.expense + '</td>
655
+ <td class="right ' + (year_data.balance < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % year_data.balance + '</td>
656
+ <td class="right ' + (year_data.balance_total < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % year_data.balance_total + '</td>
657
+ </tr>')
658
+ end
659
+
660
+ balance_total = years_total.inject(0.0){ |sum, item| sum + item[1].balance }
661
+
662
+ index_file.write('
663
+ <tr>
664
+ <th class="left"><b>TOTAL</b></th>
665
+ <th class="right">' + ::TheFox::Wallet::NUMBER_FORMAT % years_total.inject(0.0){ |sum, item| sum + item[1].revenue } + '</th>
666
+ <th class="right red">' + ::TheFox::Wallet::NUMBER_FORMAT % years_total.inject(0.0){ |sum, item| sum + item[1].expense } + '</th>
667
+ <th class="right ' + (balance_total < 0 ? 'red' : '') + '">' + ::TheFox::Wallet::NUMBER_FORMAT % balance_total + '</th>
668
+ <th>&nbsp;</th>
669
+ </tr>
670
+ </table>
671
+
672
+ <p><img src="total.png"></p>
673
+ ')
674
+ index_file.write('
675
+ </body>
676
+ </html>
677
+ ')
678
+ index_file.close
679
+
680
+ store = YAML::Store.new html_options_path
681
+ store.transaction do
682
+ store['meta'] = html_options['meta']
683
+ store['changes'] = html_options['changes']
684
+ end
685
+
686
+ totaldat_file_c = years_total.map{ |k, y| "#{y.year} #{y.revenue} #{y.expense} #{y.balance} #{y.balance_total}" }
687
+ totaldat_file_c = totaldat_file_c.slice(-6, 6) if totaldat_file_c.count > 6
688
+ totaldat_file_c = totaldat_file_c.join("\n")
689
+
690
+ totaldat_file_path = "#{@tmp_path}/total.dat"
691
+ totaldat_file = File.new(totaldat_file_path, 'w')
692
+ totaldat_file.write(totaldat_file_c)
693
+ totaldat_file.close
694
+
695
+ gnuplot_file = File.new("#{@tmp_path}/total.gp", 'w')
696
+ gnuplot_file.puts("set title 'Total'")
697
+ gnuplot_file.puts("set xlabel 'Years'")
698
+ gnuplot_file.puts("set ylabel 'Euro'")
699
+ gnuplot_file.puts("set grid")
700
+ gnuplot_file.puts("set key below center horizontal noreverse enhanced autotitle box dashtype solid")
701
+ gnuplot_file.puts("set tics out nomirror")
702
+ gnuplot_file.puts("set border 3 front linetype black linewidth 1.0 dashtype solid")
703
+ gnuplot_file.puts("set xtics 1")
704
+ gnuplot_file.puts("set style line 1 linecolor rgb '#00ff00' linewidth 2 linetype 1 pointtype 2")
705
+ gnuplot_file.puts("set style line 2 linecolor rgb '#ff0000' linewidth 2 linetype 1 pointtype 2")
706
+ gnuplot_file.puts("set style line 3 linecolor rgb '#000000' linewidth 2 linetype 1 pointtype 2")
707
+ gnuplot_file.puts("set style line 4 linecolor rgb '#0000ff' linewidth 2 linetype 1 pointtype 2")
708
+ gnuplot_file.puts("set style data linespoints")
709
+ gnuplot_file.puts("set terminal png enhanced")
710
+ gnuplot_file.puts("set output '#{@html_path}/total.png'")
711
+ gnuplot_file.puts("plot sum = 0, \\")
712
+ gnuplot_file.puts("\t'#{totaldat_file_path}' using 1:2 linestyle 1 title 'Revenue', \\")
713
+ gnuplot_file.puts("\t'' using 1:3 linestyle 2 title 'Expense', \\")
714
+ gnuplot_file.puts("\t'' using 1:4 linestyle 3 title 'Balance', \\")
715
+ gnuplot_file.puts("\t'' using 1:5 linestyle 4 title '∑ Balance'")
716
+ gnuplot_file.close
717
+ system("gnuplot #{@tmp_path}/total.gp")
718
+ end
719
+
720
+ def import_csv_file(file_path)
721
+ transaction_start()
722
+
723
+ catch(:done) do
724
+ row_n = 0
725
+ CSV.foreach(file_path) do |row|
726
+ throw :done if @exit
727
+ row_n += 1
728
+
729
+ date = ''
730
+ title = ''
731
+ revenue = 0.0
732
+ expense = 0.0
733
+ category = ''
734
+ comment = ''
735
+
736
+ print 'import row ' + row_n.to_s + "\r"
737
+
738
+ if row.count < 2
739
+ raise IndexError, 'invalid row ' + row_n.to_s + ': "' + row.join(',') + '"'
740
+ elsif row.count >= 2
741
+ date, title, revenue, expense, category, comment = row
742
+ revenue = revenue.to_f
743
+ if revenue < 0
744
+ expense = revenue
745
+ revenue = 0.0
746
+ end
747
+ end
748
+
749
+ add Entry.new(title, date, revenue, expense, category, comment)
750
+
751
+ end
752
+
753
+ puts
754
+ puts 'save data ...'
755
+ end
756
+
757
+ transaction_end()
758
+ end
759
+
760
+ def export_csv_file(file_path)
761
+ csv_file = File.open(file_path, 'w')
762
+ csv_file.puts 'Date,Title,Revenue,Expense,Balance,Category,Comment'
763
+
764
+ Dir[@data_path + '/month_*.yml'].each do |yaml_file_path|
765
+ puts 'export ' + File.basename(yaml_file_path)
766
+
767
+ data = YAML.load_file(yaml_file_path)
768
+
769
+ data['days'].each do |day_name, day_items|
770
+ day_items.each do |entry|
771
+ out = [
772
+ entry['date'],
773
+ '"'+entry['title']+'"',
774
+ ::TheFox::Wallet::NUMBER_FORMAT % entry['revenue'],
775
+ ::TheFox::Wallet::NUMBER_FORMAT % entry['expense'],
776
+ ::TheFox::Wallet::NUMBER_FORMAT % entry['balance'],
777
+ '"'+entry['category']+'"',
778
+ '"'+entry['comment']+'"',
779
+ ].join(',')
780
+
781
+ csv_file.puts out
782
+ end
783
+ end
784
+ end
785
+
786
+ csv_file.close
787
+ end
788
+
789
+ private
790
+
791
+ def create_dirs
792
+ if !Dir.exist? @dir_path
793
+ Dir.mkdir(@dir_path)
794
+ end
795
+
796
+ if !Dir.exist? @data_path
797
+ Dir.mkdir(@data_path)
798
+ end
799
+
800
+ if !Dir.exist? @tmp_path
801
+ Dir.mkdir(@tmp_path)
802
+ end
803
+
804
+ tmp_gitignore_path = @tmp_path + '/.gitignore'
805
+ if !File.exist?(tmp_gitignore_path)
806
+ gitignore_file = File.open(tmp_gitignore_path, 'w')
807
+ gitignore_file.write('*')
808
+ gitignore_file.close
809
+ end
810
+ end
811
+
812
+ def calc_day(day_a, category = nil)
813
+ revenue = 0
814
+ expense = 0
815
+ balance = 0
816
+ if category
817
+ category.to_s.downcase!
818
+
819
+ day_a.each do |entry|
820
+ if entry['category'] == category
821
+ revenue += entry['revenue']
822
+ expense += entry['expense']
823
+ balance += entry['balance']
824
+ end
825
+ end
826
+ else
827
+ day_a.each do |entry|
828
+ revenue += entry['revenue']
829
+ expense += entry['expense']
830
+ balance += entry['balance']
831
+ end
832
+ end
833
+
834
+ {
835
+ :revenue => revenue,
836
+ :expense => expense,
837
+ :balance => balance,
838
+ }
839
+ end
840
+
841
+ def years
842
+ Dir[@data_path + '/month_*.yml'].map{ |file_path| File.basename(file_path)[6, 4].to_i }.uniq
843
+ end
844
+
845
+ end
846
+
847
+ end
848
+ end