rock_books 0.2.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/LICENSE.txt +201 -21
  4. data/README.md +4 -2
  5. data/RELEASE_NOTES.md +41 -0
  6. data/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
  7. data/lib/rock_books/cmd_line/command_line_interface.rb +24 -13
  8. data/lib/rock_books/cmd_line/main.rb +1 -9
  9. data/lib/rock_books/documents/book_set.rb +5 -131
  10. data/lib/rock_books/documents/chart_of_accounts.rb +69 -37
  11. data/lib/rock_books/documents/journal.rb +47 -46
  12. data/lib/rock_books/documents/journal_entry.rb +7 -2
  13. data/lib/rock_books/documents/journal_entry_builder.rb +4 -0
  14. data/lib/rock_books/helpers/book_set_loader.rb +3 -3
  15. data/lib/rock_books/helpers/html_helper.rb +35 -0
  16. data/lib/rock_books/reports/balance_sheet.rb +8 -42
  17. data/lib/rock_books/reports/book_set_reporter.rb +218 -0
  18. data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
  19. data/lib/rock_books/reports/data/bs_is_section_data.rb +28 -0
  20. data/lib/rock_books/reports/data/journal_data.rb +37 -0
  21. data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +40 -0
  22. data/lib/rock_books/reports/data/multidoc_txn_report_data.rb +39 -0
  23. data/lib/rock_books/reports/data/receipts_report_data.rb +47 -0
  24. data/lib/rock_books/reports/data/tx_one_account_data.rb +37 -0
  25. data/lib/rock_books/reports/helpers/erb_helper.rb +26 -0
  26. data/lib/rock_books/reports/helpers/reporter.rb +134 -0
  27. data/lib/rock_books/reports/income_statement.rb +8 -46
  28. data/lib/rock_books/reports/journal_report.rb +72 -0
  29. data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +32 -0
  30. data/lib/rock_books/reports/multidoc_txn_report.rb +25 -0
  31. data/lib/rock_books/reports/receipts_report.rb +5 -41
  32. data/lib/rock_books/reports/report_context.rb +1 -4
  33. data/lib/rock_books/reports/templates/html/index.html.erb +141 -0
  34. data/lib/rock_books/reports/templates/html/report_page.html.erb +12 -0
  35. data/lib/rock_books/reports/templates/text/_receipt_section.txt.erb +17 -0
  36. data/lib/rock_books/reports/templates/text/_totals.txt.erb +8 -0
  37. data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +21 -0
  38. data/lib/rock_books/reports/templates/text/income_statement.txt.erb +21 -0
  39. data/lib/rock_books/reports/templates/text/journal.txt.erb +20 -0
  40. data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +28 -0
  41. data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +22 -0
  42. data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +13 -0
  43. data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +18 -0
  44. data/lib/rock_books/reports/tx_one_account.rb +9 -44
  45. data/lib/rock_books/types/account.rb +13 -1
  46. data/lib/rock_books/types/account_type.rb +18 -7
  47. data/lib/rock_books/version.rb +1 -1
  48. data/rock_books.gemspec +5 -3
  49. data/sample_data/minimal/rockbooks-reports/html/ck_hsbc_disb.html +40 -0
  50. data/sample_data/minimal/rockbooks-reports/html/index.html +271 -0
  51. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_accts_rec.html +27 -0
  52. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_bank_fees.html +27 -0
  53. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_books_refs.html +27 -0
  54. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cc_hsbc_visa.html +74 -0
  55. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cc_proc.html +27 -0
  56. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ck_hsbc.html +45 -0
  57. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_conf_fees.html +36 -0
  58. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cowork_fees.html +42 -0
  59. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_govt_fees.html +27 -0
  60. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_inet_fees.html +27 -0
  61. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_int_exp.html +27 -0
  62. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_loan_to_sh.html +47 -0
  63. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_meals_ent.html +27 -0
  64. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_misc_exp.html +27 -0
  65. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_mktng_exp.html +27 -0
  66. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_own_equity.html +35 -0
  67. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_prof_fees.html +27 -0
  68. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_repair_maint.html +27 -0
  69. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ret_earn.html +27 -0
  70. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ship_exp.html +27 -0
  71. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_sls_cons.html +35 -0
  72. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_sw_exp.html +27 -0
  73. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_airfare.html +35 -0
  74. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_autorent.html +27 -0
  75. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_gas_etc.html +27 -0
  76. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_govt.html +27 -0
  77. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_lodging.html +36 -0
  78. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_m_e.html +27 -0
  79. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_m_i.html +27 -0
  80. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_mileage.html +35 -0
  81. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_misc.html +27 -0
  82. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_parking.html +27 -0
  83. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_perdiem_mi.html +35 -0
  84. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_taxi.html +27 -0
  85. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_trainfare.html +27 -0
  86. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_unclass.html +27 -0
  87. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_acct.pdf +0 -0
  88. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_amount.pdf +0 -0
  89. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_date.pdf +0 -0
  90. data/sample_data/minimal/rockbooks-reports/pdf/balance_sheet.pdf +0 -0
  91. data/sample_data/minimal/rockbooks-reports/pdf/ck_hsbc_disb.pdf +0 -0
  92. data/sample_data/minimal/rockbooks-reports/pdf/general.pdf +0 -0
  93. data/sample_data/minimal/rockbooks-reports/pdf/hsbc_visa.pdf +0 -0
  94. data/sample_data/minimal/rockbooks-reports/pdf/income_statement.pdf +0 -0
  95. data/sample_data/minimal/rockbooks-reports/pdf/receipts.pdf +0 -0
  96. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_accts_rec.pdf +0 -0
  97. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_bank_fees.pdf +0 -0
  98. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_books_refs.pdf +0 -0
  99. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cc_hsbc_visa.pdf +0 -0
  100. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cc_proc.pdf +0 -0
  101. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ck_hsbc.pdf +0 -0
  102. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_conf_fees.pdf +0 -0
  103. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cowork_fees.pdf +0 -0
  104. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_govt_fees.pdf +0 -0
  105. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_inet_fees.pdf +0 -0
  106. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_insurance.pdf +0 -0
  107. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_int_exp.pdf +0 -0
  108. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_loan_to_sh.pdf +0 -0
  109. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_meals_ent.pdf +0 -0
  110. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_misc_exp.pdf +0 -0
  111. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_mktng_exp.pdf +0 -0
  112. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_own_equity.pdf +0 -0
  113. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_paypal.pdf +0 -0
  114. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_prof_fees.pdf +0 -0
  115. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_repair_maint.pdf +0 -0
  116. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ret_earn.pdf +0 -0
  117. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ship_exp.pdf +0 -0
  118. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_sls_cons.pdf +0 -0
  119. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_supplies.pdf +0 -0
  120. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_sw_exp.pdf +0 -0
  121. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_airfare.pdf +0 -0
  122. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_autorent.pdf +0 -0
  123. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_gas_etc.pdf +0 -0
  124. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_govt.pdf +0 -0
  125. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_lodging.pdf +0 -0
  126. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_m_e.pdf +0 -0
  127. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_m_i.pdf +0 -0
  128. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_mileage.pdf +0 -0
  129. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_misc.pdf +0 -0
  130. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_parking.pdf +0 -0
  131. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_perdiem_mi.pdf +0 -0
  132. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_taxi.pdf +0 -0
  133. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_trainfare.pdf +0 -0
  134. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_unclass.pdf +0 -0
  135. data/sample_data/minimal/rockbooks-reports/txt/ck_hsbc_disb.txt +24 -0
  136. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_accts_rec.txt +11 -0
  137. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_bank_fees.txt +11 -0
  138. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_books_refs.txt +11 -0
  139. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cc_hsbc_visa.txt +58 -0
  140. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cc_proc.txt +11 -0
  141. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ck_hsbc.txt +29 -0
  142. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_conf_fees.txt +20 -0
  143. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cowork_fees.txt +26 -0
  144. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_govt_fees.txt +11 -0
  145. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_inet_fees.txt +11 -0
  146. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_int_exp.txt +11 -0
  147. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_loan_to_sh.txt +31 -0
  148. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_meals_ent.txt +11 -0
  149. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_misc_exp.txt +11 -0
  150. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_mktng_exp.txt +11 -0
  151. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_own_equity.txt +19 -0
  152. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_prof_fees.txt +11 -0
  153. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_repair_maint.txt +11 -0
  154. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ret_earn.txt +11 -0
  155. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ship_exp.txt +11 -0
  156. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_sls_cons.txt +19 -0
  157. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_sw_exp.txt +11 -0
  158. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_airfare.txt +19 -0
  159. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_autorent.txt +11 -0
  160. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_gas_etc.txt +11 -0
  161. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_govt.txt +11 -0
  162. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_lodging.txt +20 -0
  163. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_m_e.txt +11 -0
  164. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_m_i.txt +11 -0
  165. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_mileage.txt +19 -0
  166. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_misc.txt +11 -0
  167. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_parking.txt +11 -0
  168. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_perdiem_mi.txt +19 -0
  169. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_taxi.txt +11 -0
  170. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_trainfare.txt +11 -0
  171. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_unclass.txt +11 -0
  172. metadata +176 -16
  173. data/lib/rock_books/documents/index.html.erb +0 -156
  174. data/lib/rock_books/documents/receipts.html.erb +0 -54
  175. data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
  176. data/lib/rock_books/reports/reporter.rb +0 -118
  177. data/lib/rock_books/reports/transaction_report.rb +0 -103
  178. data/lib/rock_books/reports/tx_by_account.rb +0 -82
@@ -1,3 +1,4 @@
1
+ require 'stringio'
1
2
  require_relative '../types/account'
2
3
  require_relative '../types/account_type'
3
4
  require_relative '../errors/error'
@@ -6,11 +7,12 @@ require_relative '../errors/date_range_error'
6
7
  module RockBooks
7
8
  class ChartOfAccounts
8
9
 
9
- attr_reader :doc_type, :title, :accounts, :entity, :start_date, :end_date
10
+ REQUIRED_FIELDS = %i(doc_type title accounts entity start_date end_date)
11
+ REQUIRED_FIELDS.each { |field| attr_reader(field) }
10
12
 
11
13
 
12
- def self.from_file(file)
13
- self.new(File.readlines(file).map(&:chomp))
14
+ def self.from_file(filespec)
15
+ self.new(File.readlines(filespec).map(&:chomp), filespec)
14
16
  end
15
17
 
16
18
 
@@ -19,10 +21,36 @@ class ChartOfAccounts
19
21
  end
20
22
 
21
23
 
22
- def initialize(input_lines)
24
+ def initialize(input_lines, filespec = nil)
25
+ @filespec = filespec
23
26
  @accounts = []
24
- input_lines.each { |line| parse_line(line) }
27
+ parse_lines(input_lines)
25
28
  # TODO: Add validation for required fields.
29
+ check_for_missing_fields
30
+ end
31
+
32
+
33
+ def check_for_missing_fields
34
+ missing_fields = REQUIRED_FIELDS.select do |field|
35
+ instance_variable_get("@#{field}").nil?
36
+ end
37
+
38
+ unless missing_fields.empty?
39
+ raise Error.new("Chart of accounts lacks required fields: #{missing_fields.join(', ')}")
40
+ end
41
+ end
42
+
43
+
44
+ def parse_lines(input_lines)
45
+ input_lines.each_with_index do |line, line_num|
46
+ begin
47
+ parse_line(line)
48
+ rescue => e
49
+ file_message_fragment = (@filespec ? " in file '#{@filespec}'" : '')
50
+ puts "Error parsing chart of accounts#{file_message_fragment}. Bad line is line ##{line_num}, text is:\n#{line}\n\n"
51
+ raise
52
+ end
53
+ end
26
54
  end
27
55
 
28
56
 
@@ -36,36 +64,40 @@ class ChartOfAccounts
36
64
  end
37
65
 
38
66
  def parse_line(line)
39
- case line.strip
40
- when /^@doc_type:/
41
- @doc_type = line.split('@doc_type:').last.strip
42
- when /^@entity:/
43
- @entity ||= line.split('@entity:').last.strip
44
- when /^@title:/
45
- @title = line.split('@title:').last.strip
46
- when /^@start_date:/
47
- @start_date = parse_date(line.split('@start_date:').last.strip)
48
- when /^@end_date:/
49
- @end_date = parse_date(line.split('@end_date:').last.strip)
50
- when /^$/
51
- # ignore empty line
52
- when /^#/
53
- # ignore comment line
54
- else
55
- # this is an account line in the form: 101 Asset First National City Bank
56
- # The regex below gets everything before the first whitespace in token 1, and the rest in token 2.
57
- matcher = line.match(/^(\S+)\s+(.*)$/)
58
- code = matcher[1]
59
- rest = matcher[2]
60
-
61
- matcher = rest.match(/^(\S+)\s+(.*)$/)
62
- account_type_token = matcher[1]
63
- account_type = AccountType.to_type(account_type_token).symbol
64
-
65
- name = matcher[2]
66
-
67
- accounts << Account.new(code, account_type, name)
67
+ begin
68
+ case line.strip
69
+ when /^@doc_type:/
70
+ @doc_type = line.split('@doc_type:').last.strip
71
+ when /^@entity:/
72
+ @entity ||= line.split('@entity:').last.strip
73
+ when /^@title:/
74
+ @title = line.split('@title:').last.strip
75
+ when /^@start_date:/
76
+ @start_date = parse_date(line.split('@start_date:').last.strip)
77
+ when /^@end_date:/
78
+ @end_date = parse_date(line.split('@end_date:').last.strip)
79
+ when /^$/
80
+ # ignore empty line
81
+ when /^#/
82
+ # ignore comment line
83
+ else
84
+ # this is an account line in the form: 101 Asset First National City Bank
85
+ # The regex below gets everything before the first whitespace in token 1, and the rest in token 2.
86
+ matcher = line.match(/^(\S+)\s+(.*)$/)
87
+ code = matcher[1]
88
+ rest = matcher[2]
89
+
90
+ matcher = rest.match(/^(\S+)\s+(.*)$/)
91
+
92
+ account_type_token = matcher[1]
93
+ account_type = AccountType.letter_to_type(account_type_token)
94
+
95
+ name = matcher[2]
96
+
97
+ accounts << Account.new(code, account_type.symbol, name)
98
+ end
68
99
  end
100
+
69
101
  end
70
102
 
71
103
 
@@ -90,7 +122,7 @@ class ChartOfAccounts
90
122
 
91
123
 
92
124
  def report_string
93
- result = ''
125
+ result = StringIO.new
94
126
 
95
127
  if title
96
128
  result << title << "\n\n"
@@ -98,9 +130,9 @@ class ChartOfAccounts
98
130
 
99
131
  code_width = @accounts.inject(0) { |width, a| width = [width, a.code.length].max }
100
132
  format_string = "%-#{code_width}s %-10.10s %s\n"
101
- accounts.each { |a| result << (format_string % [a.code, a.type.to_s, a.name]) }
133
+ accounts.each { |a| result << sprintf(format_string, a.code, a.type.to_s, a.name) }
102
134
 
103
- result
135
+ result.string
104
136
  end
105
137
 
106
138
 
@@ -7,7 +7,7 @@ require_relative '../types/acct_amount'
7
7
  require_relative '../types/journal_entry_context'
8
8
  require_relative 'journal_entry'
9
9
  require_relative 'journal_entry_builder'
10
- require_relative '../reports/reporter'
10
+ require_relative '../reports/helpers/reporter'
11
11
 
12
12
  module RockBooks
13
13
 
@@ -45,9 +45,7 @@ class Journal
45
45
  end
46
46
 
47
47
 
48
-
49
-
50
- def self.acct_amounts_in_documents(documents, entries_filter = nil, acct_amounts_filter = nil)
48
+ def self.acct_amounts_in_documents(documents, entries_filter = nil, acct_amounts_filter = nil)
51
49
  entries = entries_in_documents(documents, entries_filter)
52
50
 
53
51
  acct_amounts = entries.each_with_object([]) do |entry, acct_amounts|
@@ -72,7 +70,6 @@ class Journal
72
70
  @short_name = short_name
73
71
  @entries = []
74
72
  @date_prefix = ''
75
- @title = ''
76
73
  input_lines.each_with_index do |line, linenum|
77
74
  context = JournalEntryContext.new(self, linenum + 1, line)
78
75
  parse_line(context)
@@ -81,52 +78,56 @@ class Journal
81
78
 
82
79
 
83
80
  def parse_line(journal_entry_context)
84
- line = journal_entry_context.line
85
- case line.strip
86
- when /^@doc_type:/
87
- @doc_type = line.split(/^@doc_type:/).last.strip
88
- when /^@account_code:/
89
- @account_code = line.split(/^@account_code:/).last.strip
90
-
91
- unless chart_of_accounts.include?(@account_code)
92
- raise AccountNotFoundError.new(@account_code, journal_entry_context)
93
- end
94
-
95
- # if debit or credit has not yet been specified, inherit the setting from the account:
96
- unless @debit_or_credit
97
- @debit_or_credit = chart_of_accounts.debit_or_credit_for_code(@account_code)
98
- end
99
-
100
- when /^@title:/
101
- @title = line.split(/^@title:/).last.strip
102
- when /^@short_name:/
103
- @short_name = line.split(/^@short_name:/).last.strip
104
- when /^@date_prefix:/
105
- @date_prefix = line.split(/^@date_prefix:/).last.strip
106
- when /^@debit_or_credit:/
107
- data = line.split(/^@debit_or_credit:/).last.strip
108
- @debit_or_credit = data.to_sym
109
- when /^$/
110
- # ignore empty line
111
- when /^#/
112
- # ignore comment line
113
- when /^\d/ # a date/acct/amount line starting with a number
114
- entries << JournalEntryBuilder.new(journal_entry_context).build
115
- else # Text line(s) to be attached to the most recently parsed transaction
116
- unless entries.last
117
- raise Error.new("Entry for this description cannot be found: #{line}")
118
- end
119
- entries.last.description << line << "\n"
120
-
121
- if /^Receipt:/.match(line)
122
- receipt_spec = line.split(/^Receipt:/).last.strip
123
- entries.last.receipts << receipt_spec
81
+ begin
82
+ line = journal_entry_context.line
83
+ case line.strip
84
+ when /^@doc_type:/
85
+ @doc_type = line.split(/^@doc_type:/).last.strip
86
+ when /^@account_code:/
87
+ @account_code = line.split(/^@account_code:/).last.strip
88
+
89
+ unless chart_of_accounts.include?(@account_code)
90
+ raise AccountNotFoundError.new(@account_code, journal_entry_context)
91
+ end
92
+
93
+ # if debit or credit has not yet been specified, inherit the setting from the account:
94
+ unless @debit_or_credit
95
+ @debit_or_credit = chart_of_accounts.debit_or_credit_for_code(@account_code)
96
+ end
97
+
98
+ when /^@title:/
99
+ @title = line.split(/^@title:/).last.strip
100
+ when /^@short_name:/
101
+ @short_name = line.split(/^@short_name:/).last.strip
102
+ when /^@date_prefix:/
103
+ @date_prefix = line.split(/^@date_prefix:/).last.strip
104
+ when /^@debit_or_credit:/
105
+ data = line.split(/^@debit_or_credit:/).last.strip
106
+ @debit_or_credit = data.to_sym
107
+ when /^$/
108
+ # ignore empty line
109
+ when /^#/
110
+ # ignore comment line
111
+ when /^\d/ # a date/acct/amount line starting with a number
112
+ entries << JournalEntryBuilder.new(journal_entry_context).build
113
+ else # Text line(s) to be attached to the most recently parsed transaction
114
+ unless entries.last
115
+ raise Error.new("Entry for this description cannot be found: #{line}")
116
+ end
117
+ entries.last.description << line << "\n"
118
+
119
+ if /^Receipt:/.match(line)
120
+ receipt_spec = line.split(/^Receipt:/).last.strip
121
+ entries.last.receipts << receipt_spec
122
+ end
124
123
  end
124
+ rescue => e
125
+ puts "Error occurred parsing:\n#{journal_entry_context}\n\n"
126
+ raise
125
127
  end
126
128
  end
127
129
 
128
130
 
129
-
130
131
  def acct_amounts
131
132
  entries.each_with_object([]) { |entry, acct_amounts| acct_amounts << entry.acct_amounts }.flatten
132
133
  end
@@ -32,12 +32,17 @@ class JournalEntry < Struct.new(:date, :acct_amounts, :doc_short_name, :descript
32
32
 
33
33
  def self.sort_entries_by_amount_descending!(entries)
34
34
  entries.sort_by! do |entry|
35
- [entry.total_absolute_value, entry.doc_short_name]
35
+ [-entry.total_absolute_value, entry.doc_short_name]
36
36
  end
37
- entries.reverse!
38
37
  end
39
38
 
40
39
 
40
+ def self.sort_entries_by_date!(entries)
41
+ entries.sort_by! { |entry| [entry.date, entry.doc_short_name] }
42
+ end
43
+
44
+
45
+
41
46
  def total_for_code(account_code)
42
47
  acct_amounts_with_code(account_code).map(&:amount).sum
43
48
  end
@@ -14,6 +14,10 @@ class JournalEntryBuilder < Struct.new(:journal_entry_context)
14
14
  def chart_of_accounts; journal_entry_context.chart_of_accounts; end
15
15
 
16
16
 
17
+ # A "token" in this context means an account name and account amount pair, e.g. "pnc.checking 1234.56".
18
+ # @param tokens the account name/amount pairs found in the source text
19
+ # @date transaction date
20
+ # @return array of AcctAmount instances
17
21
  def acct_amounts_from_tokens(tokens, date)
18
22
  acct_amounts = []
19
23
 
@@ -7,6 +7,7 @@ module RockBooks
7
7
 
8
8
  module_function
9
9
 
10
+ # @return a hash whose keys are the filespecs and values are the document types
10
11
  def get_files_with_types(directory)
11
12
  files = Dir[File.join(directory, '*.txt')]
12
13
  files.each_with_object({}) do |filespec, files_with_types|
@@ -42,7 +43,7 @@ module RockBooks
42
43
 
43
44
  # Uses all *.txt files in the specified directory; uses @doc_type to determine which
44
45
  # is the chart of accounts and which are journals.
45
- # To exclude a file, make the extension other than .rdt.
46
+ # To exclude a file, make the extension something other than .txt.
46
47
  def load(run_options)
47
48
 
48
49
  files_with_types = get_files_with_types(run_options.input_dir)
@@ -54,9 +55,8 @@ module RockBooks
54
55
  validate_journal_file_count(journal_files)
55
56
 
56
57
  chart_of_accounts = ChartOfAccounts.from_file(chart_of_account_files.first)
57
- journals = journal_files.map { |fs| Journal.from_file(chart_of_accounts, fs) }
58
+ journals = journal_files.map { |filespec| Journal.from_file(chart_of_accounts, filespec) }
58
59
  BookSet.new(run_options, chart_of_accounts, journals)
59
60
  end
60
-
61
61
  end
62
62
  end
@@ -0,0 +1,35 @@
1
+ module RockBooks
2
+ module HtmlHelper
3
+
4
+ def self.convert_receipts_to_hyperlinks(original_html_string, html_filespec)
5
+ html_lines = original_html_string.split("\n")
6
+ content_changed = false
7
+
8
+ # If the HTML file being created is in DATA_DIR/rockbooks-reports/html/single-account, then
9
+ # the processed link should be '../../../receipts/[receipt_filespec]'
10
+ # else it's in DATA_DIR/rockbooks-reports/html, and
11
+ # the processed link should be '../../receipts/[receipt_filespec]'
12
+ processed_receipt_filespec = ->(listed_receipt_filespec) do
13
+ num_dirs_up = html_filespec.include?('/single-account/') ? 3 : 2
14
+ File.join(('../' * num_dirs_up), 'receipts', listed_receipt_filespec)
15
+ end
16
+
17
+ receipt_anchor_line = ->(line, listed_receipt_filespec) do
18
+ line.gsub( \
19
+ /Receipt:\s*#{listed_receipt_filespec}/, \
20
+ %Q{Receipt: <a href="#{processed_receipt_filespec.(listed_receipt_filespec)}">#{listed_receipt_filespec}</a>})
21
+ end
22
+
23
+ html_lines.each_with_index do |line, index|
24
+ matches = /Receipt:\s*(\S*)/.match(line)
25
+ if matches
26
+ listed_receipt_filespec = matches[1]
27
+ html_lines[index] = receipt_anchor_line.(line, listed_receipt_filespec)
28
+ content_changed = true
29
+ end
30
+ end
31
+
32
+ content_changed ? html_lines.join("\n") : original_html_string
33
+ end
34
+ end
35
+ end
@@ -1,6 +1,5 @@
1
- require_relative '../filters/journal_entry_filters'
2
- require_relative '../documents/journal'
3
- require_relative 'report_context'
1
+ require_relative 'helpers/erb_helper'
2
+ require_relative 'helpers/reporter'
4
3
 
5
4
  module RockBooks
6
5
 
@@ -10,51 +9,18 @@ module RockBooks
10
9
  class BalanceSheet
11
10
 
12
11
  include Reporter
12
+ include ErbHelper
13
13
 
14
- attr_accessor :context
14
+ attr_accessor :context, :data
15
15
 
16
- def initialize(report_context)
16
+ def initialize(report_context, data)
17
17
  @context = report_context
18
+ @data = data
18
19
  end
19
20
 
20
21
 
21
- def end_date
22
- context.chart_of_accounts.end_date
22
+ def generate
23
+ ErbHelper.render_hashes('text/balance_sheet.txt.erb', data, template_presentation_context)
23
24
  end
24
-
25
-
26
- def generate_header
27
- lines = [banner_line]
28
- lines << center(context.entity || 'Unspecified Entity')
29
- lines << center("Balance Sheet for Period Ending #{end_date}")
30
- lines << banner_line
31
- lines << ''
32
- lines << ''
33
- lines << ''
34
- lines.join("\n")
35
- end
36
-
37
-
38
- def generate_report
39
- filter = RockBooks::JournalEntryFilters.date_on_or_before(end_date)
40
- acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
41
- totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
42
- output = generate_header
43
-
44
- asset_output, asset_total = generate_account_type_section('Assets', totals, :asset, false)
45
- liab_output, liab_total = generate_account_type_section('Liabilities', totals, :liability, true)
46
- equity_output, equity_total = generate_account_type_section('Equity', totals, :equity, true)
47
-
48
- output << [asset_output, liab_output, equity_output].join("\n\n")
49
-
50
- grand_total = asset_total - (liab_total + equity_total)
51
-
52
- output << "\n#{"%12.2f Assets - (Liabilities + Equity)" % grand_total}\n============\n"
53
- output
54
- end
55
-
56
- alias_method :to_s, :generate_report
57
- alias_method :call, :generate_report
58
-
59
25
  end
60
26
  end
@@ -0,0 +1,218 @@
1
+ require_relative '../documents/book_set'
2
+
3
+ require_relative 'balance_sheet'
4
+ require_relative 'data/bs_is_data'
5
+ require_relative 'data/receipts_report_data'
6
+ require_relative 'income_statement'
7
+ require_relative 'multidoc_txn_report'
8
+ require_relative 'receipts_report'
9
+ require_relative 'report_context'
10
+ require_relative 'journal_report'
11
+ require_relative 'multidoc_txn_by_account_report'
12
+ require_relative 'tx_one_account'
13
+ require_relative 'helpers/erb_helper'
14
+ require_relative 'helpers/reporter'
15
+
16
+ require 'prawn'
17
+
18
+ module RockBooks
19
+ class BookSetReporter
20
+
21
+ extend Forwardable
22
+
23
+ attr_reader :book_set, :output_dir, :filter, :context
24
+
25
+ def_delegator :book_set, :all_entries
26
+ def_delegator :book_set, :journals
27
+ def_delegator :book_set, :chart_of_accounts
28
+ def_delegator :book_set, :run_options
29
+
30
+ FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
31
+
32
+
33
+ def initialize(book_set, output_dir, filter = nil)
34
+ @book_set = book_set
35
+ @output_dir = output_dir
36
+ @filter = filter
37
+ @context = ReportContext.new(book_set.chart_of_accounts, book_set.journals, 80)
38
+ end
39
+
40
+
41
+ def generate
42
+ create_directories
43
+ create_index_html
44
+
45
+ do_statements
46
+ do_journals
47
+ do_transaction_reports
48
+ do_single_account_reports
49
+ do_receipts_report
50
+ end
51
+
52
+
53
+ # All methods after this point are private.
54
+
55
+ private def do_statements
56
+ bs_is_data = BsIsData.new(context)
57
+
58
+ bal_sheet_text_report = BalanceSheet.new(context, bs_is_data.bal_sheet_data).generate
59
+ write_report(:balance_sheet, bal_sheet_text_report)
60
+
61
+ inc_stat_text_report = IncomeStatement.new(context, bs_is_data.inc_stat_data).generate
62
+ write_report(:income_statement, inc_stat_text_report)
63
+ end
64
+
65
+
66
+ private def do_journals
67
+ journals.each do |journal|
68
+ report_data = JournalData.new(journal, context, filter).fetch
69
+ text_report = JournalReport.new(report_data, context, filter).generate
70
+ write_report(journal.short_name, text_report)
71
+ end
72
+ end
73
+
74
+
75
+ private def do_transaction_reports
76
+
77
+ do_date_or_amount_report = ->(sort_field, short_name) do
78
+ data = MultidocTxnReportData.new(context, sort_field, filter).fetch
79
+ text_report = MultidocTransactionReport.new(data, context).generate
80
+ write_report(short_name, text_report)
81
+ end
82
+
83
+ do_acct_report = -> do
84
+ data = MultidocTxnByAccountData.new(context).fetch
85
+ text_report = MultidocTransactionByAccountReport.new(data, context).generate
86
+ write_report(:all_txns_by_acct, text_report)
87
+ end
88
+
89
+ do_date_or_amount_report.(:date, :all_txns_by_date)
90
+ do_date_or_amount_report.(:amount, :all_txns_by_amount)
91
+ do_acct_report.()
92
+ end
93
+
94
+
95
+ private def do_single_account_reports
96
+ chart_of_accounts.accounts.each do |account|
97
+ short_name = ('acct_' + account.code).to_sym
98
+ data = TxOneAccountData.new(context, account.code).fetch
99
+ text_report = TxOneAccount.new(data, context).generate
100
+ write_report(short_name, text_report)
101
+ end
102
+ end
103
+
104
+
105
+ private def do_receipts_report
106
+ data = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
107
+ text_report = ReceiptsReport.new(context, data).generate
108
+ write_report(:receipts, text_report)
109
+ end
110
+
111
+
112
+ private def run_command(command)
113
+ puts "\n----\nRunning command: #{command}"
114
+ stdout, stderr, status = Open3.capture3(command)
115
+ puts "Exit code was #{status.exitstatus}."
116
+ puts "\nStdout was:\n\n#{stdout}" unless stdout.size == 0
117
+ puts "\nStderr was:\n\n#{stderr}" unless stderr.size == 0
118
+ puts
119
+ end
120
+
121
+
122
+ private def create_directories
123
+ %w(txt pdf html).each do |format|
124
+ dir = File.join(output_dir, format, SINGLE_ACCT_SUBDIR)
125
+ FileUtils.mkdir_p(dir)
126
+ end
127
+ end
128
+
129
+
130
+ # "./pdf/short_name.pdf" or "./pdf/single_account/short_name.pdf"
131
+ private def build_filespec(directory, short_name, file_format)
132
+ fragments = [directory, file_format, "#{short_name}.#{file_format}"]
133
+ is_acct_report = /^acct_/.match(short_name)
134
+ if is_acct_report
135
+ fragments.insert(2, SINGLE_ACCT_SUBDIR)
136
+ end
137
+ File.join(*fragments)
138
+ end
139
+
140
+
141
+ private def create_index_html
142
+ filespec = build_filespec(output_dir, 'index', 'html')
143
+ File.write(filespec, index_html_content)
144
+ puts "Created index.html"
145
+ end
146
+
147
+
148
+ private def prawn_create_document(pdf_filespec, text)
149
+ Prawn::Document.generate(pdf_filespec) do
150
+ font(FONT_FILESPEC, size: 10)
151
+
152
+ utf8_nonbreaking_space = "\uC2A0"
153
+ unicode_nonbreaking_space = "\u00A0"
154
+ text(text.gsub(' ', unicode_nonbreaking_space))
155
+ end
156
+ puts "Finished generating #{pdf_filespec} with prawn."
157
+ end
158
+
159
+
160
+ private def write_report(short_name, text_report)
161
+
162
+ txt_filespec = build_filespec(output_dir, short_name, 'txt')
163
+ html_filespec = build_filespec(output_dir, short_name, 'html')
164
+ pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
165
+
166
+ create_text_report = -> { File.write(txt_filespec, text_report) }
167
+
168
+ create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report) }
169
+
170
+ create_html_report = -> do
171
+ data = { report_body: text_report }
172
+ html_raw_report = ErbHelper.render_hashes("html/report_page.html.erb", data, {})
173
+ html_report = HtmlHelper.convert_receipts_to_hyperlinks(html_raw_report, html_filespec)
174
+ File.write(html_filespec, html_report)
175
+ end
176
+
177
+ create_text_report.()
178
+ create_pdf_report.()
179
+ create_html_report.()
180
+
181
+
182
+ puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
183
+ end
184
+
185
+
186
+ private def missing_existing_unused_receipts
187
+ missing_receipts = []
188
+ existing_receipts = []
189
+ receipt_full_filespec = ->(receipt_filespec) { File.join(run_options.receipt_dir, receipt_filespec) }
190
+
191
+ # We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
192
+ unused_receipt_filespecs = Dir['receipts/**/*'].select { |s| File.file?(s) } \
193
+ .sort \
194
+ .map { |s| "./" + s } # Prepend './' to match the data
195
+
196
+ all_entries.each do |entry|
197
+ entry.receipts.each do |receipt|
198
+ filespec = receipt_full_filespec.(receipt)
199
+ unused_receipt_filespecs.delete(filespec)
200
+ file_exists = File.file?(filespec)
201
+ list = (file_exists ? existing_receipts : missing_receipts)
202
+ list << { receipt: receipt, journal: entry.doc_short_name }
203
+ end
204
+ end
205
+ [missing_receipts, existing_receipts, unused_receipt_filespecs]
206
+ end
207
+
208
+
209
+ private def index_html_content
210
+ erb_filespec = File.join(File.dirname(__FILE__), 'templates', 'html', 'index.html.erb')
211
+ erb = ERB.new(File.read(erb_filespec))
212
+ erb.result_with_hash(
213
+ journals: journals,
214
+ chart_of_accounts: chart_of_accounts,
215
+ run_options: run_options)
216
+ end
217
+ end
218
+ end