rock_books 0.2.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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