rock_books 0.2.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/LICENSE.txt +201 -21
  4. data/README.md +6 -2
  5. data/RELEASE_NOTES.md +34 -0
  6. data/lib/rock_books/cmd_line/command_line_interface.rb +19 -8
  7. data/lib/rock_books/documents/book_set.rb +4 -130
  8. data/lib/rock_books/documents/chart_of_accounts.rb +45 -30
  9. data/lib/rock_books/documents/journal.rb +45 -40
  10. data/lib/rock_books/helpers/html_helper.rb +29 -0
  11. data/lib/rock_books/reports/book_set_reporter.rb +200 -0
  12. data/lib/rock_books/{documents → reports}/index.html.erb +0 -0
  13. data/lib/rock_books/{documents → reports}/receipts.html.erb +0 -0
  14. data/lib/rock_books/reports/receipts_report.rb +23 -10
  15. data/lib/rock_books/reports/report_context.rb +1 -4
  16. data/lib/rock_books/reports/reporter.rb +1 -1
  17. data/lib/rock_books/reports/transaction_report.rb +4 -2
  18. data/lib/rock_books/version.rb +1 -1
  19. data/rock_books.gemspec +4 -3
  20. data/sample_data/minimal/rockbooks-reports/html/ck_hsbc_disb.html +40 -0
  21. data/sample_data/minimal/rockbooks-reports/html/index.html +271 -0
  22. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_accts_rec.html +27 -0
  23. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_bank_fees.html +27 -0
  24. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_books_refs.html +27 -0
  25. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cc_hsbc_visa.html +74 -0
  26. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cc_proc.html +27 -0
  27. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ck_hsbc.html +45 -0
  28. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_conf_fees.html +36 -0
  29. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_cowork_fees.html +42 -0
  30. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_govt_fees.html +27 -0
  31. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_inet_fees.html +27 -0
  32. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_int_exp.html +27 -0
  33. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_loan_to_sh.html +47 -0
  34. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_meals_ent.html +27 -0
  35. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_misc_exp.html +27 -0
  36. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_mktng_exp.html +27 -0
  37. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_own_equity.html +35 -0
  38. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_prof_fees.html +27 -0
  39. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_repair_maint.html +27 -0
  40. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ret_earn.html +27 -0
  41. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_ship_exp.html +27 -0
  42. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_sls_cons.html +35 -0
  43. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_sw_exp.html +27 -0
  44. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_airfare.html +35 -0
  45. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_autorent.html +27 -0
  46. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_gas_etc.html +27 -0
  47. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_govt.html +27 -0
  48. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_lodging.html +36 -0
  49. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_m_e.html +27 -0
  50. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_m_i.html +27 -0
  51. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_mileage.html +35 -0
  52. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_misc.html +27 -0
  53. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_parking.html +27 -0
  54. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_perdiem_mi.html +35 -0
  55. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_taxi.html +27 -0
  56. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_trainfare.html +27 -0
  57. data/sample_data/minimal/rockbooks-reports/html/single-account/acct_tr_unclass.html +27 -0
  58. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_acct.pdf +0 -0
  59. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_amount.pdf +0 -0
  60. data/sample_data/minimal/rockbooks-reports/pdf/all_txns_by_date.pdf +0 -0
  61. data/sample_data/minimal/rockbooks-reports/pdf/balance_sheet.pdf +0 -0
  62. data/sample_data/minimal/rockbooks-reports/pdf/ck_hsbc_disb.pdf +0 -0
  63. data/sample_data/minimal/rockbooks-reports/pdf/general.pdf +0 -0
  64. data/sample_data/minimal/rockbooks-reports/pdf/hsbc_visa.pdf +0 -0
  65. data/sample_data/minimal/rockbooks-reports/pdf/income_statement.pdf +0 -0
  66. data/sample_data/minimal/rockbooks-reports/pdf/receipts.pdf +0 -0
  67. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_accts_rec.pdf +0 -0
  68. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_bank_fees.pdf +0 -0
  69. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_books_refs.pdf +0 -0
  70. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cc_hsbc_visa.pdf +0 -0
  71. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cc_proc.pdf +0 -0
  72. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ck_hsbc.pdf +0 -0
  73. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_conf_fees.pdf +0 -0
  74. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_cowork_fees.pdf +0 -0
  75. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_govt_fees.pdf +0 -0
  76. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_inet_fees.pdf +0 -0
  77. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_insurance.pdf +0 -0
  78. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_int_exp.pdf +0 -0
  79. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_loan_to_sh.pdf +0 -0
  80. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_meals_ent.pdf +0 -0
  81. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_misc_exp.pdf +0 -0
  82. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_mktng_exp.pdf +0 -0
  83. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_own_equity.pdf +0 -0
  84. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_paypal.pdf +0 -0
  85. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_prof_fees.pdf +0 -0
  86. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_repair_maint.pdf +0 -0
  87. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ret_earn.pdf +0 -0
  88. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_ship_exp.pdf +0 -0
  89. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_sls_cons.pdf +0 -0
  90. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_supplies.pdf +0 -0
  91. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_sw_exp.pdf +0 -0
  92. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_airfare.pdf +0 -0
  93. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_autorent.pdf +0 -0
  94. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_gas_etc.pdf +0 -0
  95. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_govt.pdf +0 -0
  96. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_lodging.pdf +0 -0
  97. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_m_e.pdf +0 -0
  98. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_m_i.pdf +0 -0
  99. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_mileage.pdf +0 -0
  100. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_misc.pdf +0 -0
  101. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_parking.pdf +0 -0
  102. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_perdiem_mi.pdf +0 -0
  103. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_taxi.pdf +0 -0
  104. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_trainfare.pdf +0 -0
  105. data/sample_data/minimal/rockbooks-reports/pdf/single-account/acct_tr_unclass.pdf +0 -0
  106. data/sample_data/minimal/rockbooks-reports/txt/ck_hsbc_disb.txt +24 -0
  107. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_accts_rec.txt +11 -0
  108. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_bank_fees.txt +11 -0
  109. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_books_refs.txt +11 -0
  110. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cc_hsbc_visa.txt +58 -0
  111. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cc_proc.txt +11 -0
  112. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ck_hsbc.txt +29 -0
  113. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_conf_fees.txt +20 -0
  114. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_cowork_fees.txt +26 -0
  115. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_govt_fees.txt +11 -0
  116. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_inet_fees.txt +11 -0
  117. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_int_exp.txt +11 -0
  118. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_loan_to_sh.txt +31 -0
  119. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_meals_ent.txt +11 -0
  120. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_misc_exp.txt +11 -0
  121. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_mktng_exp.txt +11 -0
  122. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_own_equity.txt +19 -0
  123. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_prof_fees.txt +11 -0
  124. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_repair_maint.txt +11 -0
  125. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ret_earn.txt +11 -0
  126. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_ship_exp.txt +11 -0
  127. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_sls_cons.txt +19 -0
  128. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_sw_exp.txt +11 -0
  129. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_airfare.txt +19 -0
  130. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_autorent.txt +11 -0
  131. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_gas_etc.txt +11 -0
  132. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_govt.txt +11 -0
  133. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_lodging.txt +20 -0
  134. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_m_e.txt +11 -0
  135. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_m_i.txt +11 -0
  136. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_mileage.txt +19 -0
  137. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_misc.txt +11 -0
  138. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_parking.txt +11 -0
  139. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_perdiem_mi.txt +19 -0
  140. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_taxi.txt +11 -0
  141. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_trainfare.txt +11 -0
  142. data/sample_data/minimal/rockbooks-reports/txt/single-account/acct_tr_unclass.txt +11 -0
  143. metadata +140 -12
@@ -6,7 +6,8 @@ require_relative '../errors/date_range_error'
6
6
  module RockBooks
7
7
  class ChartOfAccounts
8
8
 
9
- attr_reader :doc_type, :title, :accounts, :entity, :start_date, :end_date
9
+ REQUIRED_FIELDS = %i(doc_type title accounts entity start_date end_date)
10
+ REQUIRED_FIELDS.each { |field| attr_reader(field) }
10
11
 
11
12
 
12
13
  def self.from_file(file)
@@ -23,6 +24,14 @@ class ChartOfAccounts
23
24
  @accounts = []
24
25
  input_lines.each { |line| parse_line(line) }
25
26
  # TODO: Add validation for required fields.
27
+
28
+ missing_fields = REQUIRED_FIELDS.select do |field|
29
+ instance_variable_get("@#{field}").nil?
30
+ end
31
+
32
+ unless missing_fields.empty?
33
+ raise Error.new("Chart of accounts lacks required fields: #{missing_fields.join(', ')}")
34
+ end
26
35
  end
27
36
 
28
37
 
@@ -36,36 +45,42 @@ class ChartOfAccounts
36
45
  end
37
46
 
38
47
  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)
48
+ begin
49
+ case line.strip
50
+ when /^@doc_type:/
51
+ @doc_type = line.split('@doc_type:').last.strip
52
+ when /^@entity:/
53
+ @entity ||= line.split('@entity:').last.strip
54
+ when /^@title:/
55
+ @title = line.split('@title:').last.strip
56
+ when /^@start_date:/
57
+ @start_date = parse_date(line.split('@start_date:').last.strip)
58
+ when /^@end_date:/
59
+ @end_date = parse_date(line.split('@end_date:').last.strip)
60
+ when /^$/
61
+ # ignore empty line
62
+ when /^#/
63
+ # ignore comment line
64
+ else
65
+ # this is an account line in the form: 101 Asset First National City Bank
66
+ # The regex below gets everything before the first whitespace in token 1, and the rest in token 2.
67
+ matcher = line.match(/^(\S+)\s+(.*)$/)
68
+ code = matcher[1]
69
+ rest = matcher[2]
70
+
71
+ matcher = rest.match(/^(\S+)\s+(.*)$/)
72
+ account_type_token = matcher[1]
73
+ account_type = AccountType.to_type(account_type_token).symbol
74
+
75
+ name = matcher[2]
76
+
77
+ accounts << Account.new(code, account_type, name)
78
+ end
79
+ rescue => e
80
+ puts "Error parsing chart of accounts. Line text is:\n#{line}\n\n"
81
+ raise
68
82
  end
83
+
69
84
  end
70
85
 
71
86
 
@@ -81,47 +81,52 @@ class Journal
81
81
 
82
82
 
83
83
  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
84
+ begin
85
+ line = journal_entry_context.line
86
+ case line.strip
87
+ when /^@doc_type:/
88
+ @doc_type = line.split(/^@doc_type:/).last.strip
89
+ when /^@account_code:/
90
+ @account_code = line.split(/^@account_code:/).last.strip
91
+
92
+ unless chart_of_accounts.include?(@account_code)
93
+ raise AccountNotFoundError.new(@account_code, journal_entry_context)
94
+ end
95
+
96
+ # if debit or credit has not yet been specified, inherit the setting from the account:
97
+ unless @debit_or_credit
98
+ @debit_or_credit = chart_of_accounts.debit_or_credit_for_code(@account_code)
99
+ end
100
+
101
+ when /^@title:/
102
+ @title = line.split(/^@title:/).last.strip
103
+ when /^@short_name:/
104
+ @short_name = line.split(/^@short_name:/).last.strip
105
+ when /^@date_prefix:/
106
+ @date_prefix = line.split(/^@date_prefix:/).last.strip
107
+ when /^@debit_or_credit:/
108
+ data = line.split(/^@debit_or_credit:/).last.strip
109
+ @debit_or_credit = data.to_sym
110
+ when /^$/
111
+ # ignore empty line
112
+ when /^#/
113
+ # ignore comment line
114
+ when /^\d/ # a date/acct/amount line starting with a number
115
+ entries << JournalEntryBuilder.new(journal_entry_context).build
116
+ else # Text line(s) to be attached to the most recently parsed transaction
117
+ unless entries.last
118
+ raise Error.new("Entry for this description cannot be found: #{line}")
119
+ end
120
+ entries.last.description << line << "\n"
121
+
122
+ if /^Receipt:/.match(line)
123
+ receipt_spec = line.split(/^Receipt:/).last.strip
124
+ entries.last.receipts << receipt_spec
125
+ end
124
126
  end
127
+ rescue => e
128
+ puts "Error occurred parsing:\n#{journal_entry_context}\n\n"
129
+ raise
125
130
  end
126
131
  end
127
132
 
@@ -0,0 +1,29 @@
1
+ module RockBooks
2
+ module HtmlHelper
3
+
4
+ module_function
5
+
6
+ def self.convert_receipts_to_hyperlinks(html_text)
7
+ html_lines = html_text.split("\n")
8
+ replacements_made = false
9
+
10
+ html_lines.each_with_index do |line, index|
11
+ matches = /Receipt:\s*(.*?)</.match(line)
12
+ if matches
13
+ receipt_filespec = matches[1]
14
+ line_with_hyperlink = line.gsub( \
15
+ /Receipt:\s*#{receipt_filespec}/, \
16
+ %Q{Receipt: <a href="../../../receipts/#{receipt_filespec}">#{receipt_filespec}</a>})
17
+ html_lines[index] = line_with_hyperlink
18
+ replacements_made = true
19
+ end
20
+ end
21
+
22
+ if replacements_made
23
+ html_text = html_lines.join("\n")
24
+ end
25
+
26
+ [html_text, replacements_made]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,200 @@
1
+ require_relative '../documents/book_set'
2
+
3
+ require_relative 'balance_sheet'
4
+ require_relative 'income_statement'
5
+ require_relative 'multidoc_transaction_report'
6
+ require_relative 'receipts_report'
7
+ require_relative 'report_context'
8
+ require_relative 'transaction_report'
9
+ require_relative 'tx_by_account'
10
+ require_relative 'tx_one_account'
11
+
12
+ module RockBooks
13
+ class BookSetReporter
14
+
15
+ extend Forwardable
16
+
17
+ attr_reader :book_set, :output_dir, :filter, :context
18
+
19
+ def_delegator :book_set, :all_entries
20
+ def_delegator :book_set, :journals
21
+ def_delegator :book_set, :chart_of_accounts
22
+ def_delegator :book_set, :run_options
23
+
24
+
25
+ def initialize(book_set, output_dir, filter = nil)
26
+ @book_set = book_set
27
+ @output_dir = output_dir
28
+ @filter = filter
29
+ @context = ReportContext.new(book_set.chart_of_accounts, book_set.journals, 80)
30
+ end
31
+
32
+
33
+ def call
34
+ check_prequisite_executables
35
+ reports = all_reports(filter)
36
+ create_directories
37
+ create_index_html
38
+ write_reports(reports)
39
+ end
40
+
41
+
42
+ # All methods after this point are private.
43
+
44
+ private def all_reports(filter = nil)
45
+
46
+ report_hash = journals.each_with_object({}) do |journal, report_hash|
47
+ key = journal.short_name.to_sym
48
+ report_hash[key] = TransactionReport.new(journal, context).call(filter)
49
+ end
50
+
51
+ report_hash[:all_txns_by_date] = MultidocTransactionReport.new(context).call(filter)
52
+ report_hash[:all_txns_by_amount] = MultidocTransactionReport.new(context).call(filter, :amount)
53
+ report_hash[:all_txns_by_acct] = TxByAccount.new(context).call
54
+ report_hash[:balance_sheet] = BalanceSheet.new(context).call
55
+ report_hash[:income_statement] = IncomeStatement.new(context).call
56
+
57
+ if run_options.do_receipts
58
+ report_hash[:receipts] = ReceiptsReport.new(context, *missing_existing_unused_receipts).call
59
+ end
60
+
61
+ chart_of_accounts.accounts.each do |account|
62
+ key = ('acct_' + account.code).to_sym
63
+ report = TxOneAccount.new(context, account.code).call
64
+ report_hash[key] = report
65
+ end
66
+
67
+ report_hash
68
+ end
69
+
70
+
71
+ private def run_command(command)
72
+ puts "\n----\nRunning command: #{command}"
73
+ stdout, stderr, status = Open3.capture3(command)
74
+ puts "Status was #{status}."
75
+ unless stdout.size == 0
76
+ puts "\nStdout was:\n\n#{stdout}"
77
+ end
78
+ unless stderr.size == 0
79
+ puts "\nStderr was:\n\n#{stderr}"
80
+ end
81
+ puts
82
+ stdout
83
+ end
84
+
85
+
86
+ private def executable_exists?(name)
87
+ `which #{name}`
88
+ $?.success?
89
+ end
90
+
91
+
92
+ private def check_prequisite_executables
93
+ raise "Report generation is not currently supported in Windows." if OS.windows?
94
+ required_exes = OS.mac? ? %w(textutil cupsfilter) : %w(txt2html wkhtmltopdf)
95
+ missing_exes = required_exes.reject { |exe| executable_exists?(exe) }
96
+ if missing_exes.any?
97
+ raise "Missing required report generation executable(s): #{missing_exes.join(', ')}. Please install them with your system's package manager."
98
+ end
99
+ end
100
+
101
+
102
+ private def create_directories
103
+ %w(txt pdf html).each do |format|
104
+ dir = File.join(output_dir, format, SINGLE_ACCT_SUBDIR)
105
+ FileUtils.mkdir_p(dir)
106
+ end
107
+ end
108
+
109
+
110
+ # "./pdf/short_name.pdf" or "./pdf/single_account/short_name.pdf"
111
+ private def build_filespec(directory, short_name, file_format)
112
+ fragments = [directory, file_format, "#{short_name}.#{file_format}"]
113
+ is_acct_report = /^acct_/.match(short_name)
114
+ if is_acct_report
115
+ fragments.insert(2, SINGLE_ACCT_SUBDIR)
116
+ end
117
+ File.join(*fragments)
118
+ end
119
+
120
+
121
+ private def create_index_html
122
+ filespec = build_filespec(output_dir, 'index', 'html')
123
+ File.write(filespec, index_html_content)
124
+ puts "Created index.html"
125
+ end
126
+
127
+
128
+ private def write_reports(reports)
129
+
130
+ reports.each do |short_name, report_text|
131
+ txt_filespec = build_filespec(output_dir, short_name, 'txt')
132
+ html_filespec = build_filespec(output_dir, short_name, 'html')
133
+ pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
134
+
135
+ File.write(txt_filespec, report_text)
136
+
137
+ # Mac OS
138
+ textutil = ->(font_size) do
139
+ run_command("textutil -convert html -font 'Courier New Bold' -fontsize #{font_size} #{txt_filespec} -output #{html_filespec}")
140
+ end
141
+ cupsfilter = -> { run_command("cupsfilter #{txt_filespec} > #{pdf_filespec}") }
142
+
143
+ # Linux
144
+ txt2html = -> { run_command("txt2html --preformat_trigger_lines 0 #{txt_filespec} > #{html_filespec}") }
145
+ html2pdf = -> { run_command("wkhtmltopdf #{html_filespec} #{pdf_filespec}") }
146
+
147
+ # Use smaller size for the PDF but larger size for the web pages:
148
+ if OS.mac?
149
+ textutil.(11)
150
+ cupsfilter.()
151
+ textutil.(14)
152
+ else
153
+ txt2html.()
154
+ html2pdf.()
155
+ end
156
+
157
+ hyperlinkized_text, replacements_made = HtmlHelper.convert_receipts_to_hyperlinks(File.read(html_filespec))
158
+ if replacements_made
159
+ File.write(html_filespec, hyperlinkized_text)
160
+ end
161
+
162
+ puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
163
+ end
164
+ end
165
+
166
+
167
+ private def receipt_full_filespec(receipt_filespec)
168
+ File.join(run_options.receipt_dir, receipt_filespec)
169
+ end
170
+
171
+
172
+ private def missing_existing_unused_receipts
173
+ missing = []
174
+ existing = []
175
+ unused = Dir['receipts/**/*'].select { |s| File.file?(s) }.sort # Remove files as they are found
176
+ unused.map! { |s| "./" + s } # Prepend './' to match the data
177
+
178
+ all_entries.each do |entry|
179
+ entry.receipts.each do |receipt|
180
+ filespec = receipt_full_filespec(receipt)
181
+ unused.delete(filespec)
182
+ file_exists = File.file?(filespec)
183
+ list = (file_exists ? existing : missing)
184
+ list << { receipt: receipt, journal: entry.doc_short_name }
185
+ end
186
+ end
187
+ [missing, existing, unused]
188
+ end
189
+
190
+
191
+ private def index_html_content
192
+ erb_filespec = File.join(File.dirname(__FILE__), 'index.html.erb')
193
+ erb = ERB.new(File.read(erb_filespec))
194
+ erb.result_with_hash(
195
+ journals: journals,
196
+ chart_of_accounts: chart_of_accounts,
197
+ run_options: run_options)
198
+ end
199
+ end
200
+ end
@@ -6,13 +6,14 @@ class ReceiptsReport
6
6
 
7
7
  include Reporter
8
8
 
9
- attr_reader :context, :missing, :existing
9
+ attr_reader :context, :missing, :existing, :unused
10
10
 
11
11
 
12
- def initialize(report_context, missing, existing)
12
+ def initialize(report_context, missing, existing, unused)
13
13
  @context = report_context
14
14
  @missing = missing
15
15
  @existing = existing
16
+ @unused = unused
16
17
  end
17
18
 
18
19
 
@@ -21,9 +22,6 @@ class ReceiptsReport
21
22
  lines << center(context.entity || 'Unspecified Entity')
22
23
  lines << "#{center("Receipts Report")}"
23
24
  lines << banner_line
24
- lines << ''
25
- lines << ''
26
- lines << ''
27
25
  lines.join("\n")
28
26
  end
29
27
 
@@ -39,15 +37,30 @@ class ReceiptsReport
39
37
  end
40
38
 
41
39
 
40
+ def report_one_section(name, list)
41
+ output = ''
42
+ output << "\n\n\n#{name} Receipts:\n\n" << column_headings
43
+ if list.empty?
44
+ output << "[None]\n\n\n"
45
+ else
46
+ list.each { |receipt| output << receipt_info_line(receipt) }
47
+ end
48
+ output
49
+ end
50
+
51
+
42
52
  def generate_report
43
53
  output = generate_header
54
+ output << report_one_section('Missing', missing)
44
55
 
45
- output << "Missing Receipts:\n\n" << column_headings
46
- missing.each { |info| output << receipt_info_line(info) }
47
-
48
- output << "\n\n\nExisting Receipts:\n\n" << column_headings
49
- existing.each { |info| output << receipt_info_line(info) }
56
+ output << "\n\n\nUnused Receipts:\n\n"
57
+ if unused.empty?
58
+ output << "[None]\n\n\n"
59
+ else
60
+ unused.each { |filespec| output << filespec << "\n" }
61
+ end
50
62
 
63
+ output << report_one_section('Existing', existing)
51
64
  output
52
65
  end
53
66