thefox-wallet 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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