rock_books 0.7.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/{manual.md → MANUAL.md} +26 -26
  3. data/README.md +20 -12
  4. data/RELEASE_NOTES.md +55 -10
  5. data/REPORTS.md +81 -0
  6. data/assets/doc-images/sample-index-html.png +0 -0
  7. data/lib/rock_books/cmd_line/command_line_interface.rb +12 -21
  8. data/lib/rock_books/documents/book_set.rb +4 -1
  9. data/lib/rock_books/documents/journal.rb +2 -3
  10. data/lib/rock_books/reports/balance_sheet.rb +2 -2
  11. data/lib/rock_books/reports/book_set_reporter.rb +69 -55
  12. data/lib/rock_books/reports/data/bs_is_section_data.rb +3 -1
  13. data/lib/rock_books/reports/data/journal_data.rb +1 -0
  14. data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +1 -1
  15. data/lib/rock_books/reports/data/tx_one_account_data.rb +1 -1
  16. data/lib/rock_books/reports/helpers/erb_helper.rb +1 -6
  17. data/lib/rock_books/reports/helpers/receipts_hyperlink_converter.rb +59 -0
  18. data/lib/rock_books/reports/helpers/{reporter.rb → text_report_helper.rb} +11 -1
  19. data/lib/rock_books/reports/income_statement.rb +2 -2
  20. data/lib/rock_books/reports/index_html_page.rb +27 -0
  21. data/lib/rock_books/reports/journal_report.rb +18 -8
  22. data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +2 -2
  23. data/lib/rock_books/reports/multidoc_txn_report.rb +2 -2
  24. data/lib/rock_books/reports/receipts_report.rb +1 -1
  25. data/lib/rock_books/reports/templates/html/index.html.erb +45 -6
  26. data/lib/rock_books/reports/templates/html/report_page.html.erb +14 -1
  27. data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +2 -0
  28. data/lib/rock_books/reports/templates/text/income_statement.txt.erb +3 -1
  29. data/lib/rock_books/reports/templates/text/journal.txt.erb +5 -2
  30. data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +3 -0
  31. data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +3 -0
  32. data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +3 -0
  33. data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +3 -0
  34. data/lib/rock_books/reports/tx_one_account.rb +2 -2
  35. data/lib/rock_books/version.rb +2 -1
  36. data/rock_books.gemspec +1 -0
  37. metadata +22 -5
  38. data/lib/rock_books/helpers/html_helper.rb +0 -35
@@ -4,7 +4,8 @@ require 'ostruct'
4
4
 
5
5
  require_relative '../../rock_books'
6
6
  require_relative '../version'
7
- require_relative '../reports/helpers/reporter'
7
+ require_relative '../reports/data/receipts_report_data'
8
+ require_relative '../reports/helpers/text_report_helper'
8
9
  require_relative '../helpers/book_set_loader'
9
10
 
10
11
  module RockBooks
@@ -51,12 +52,11 @@ Commands:
51
52
 
52
53
  rec[eipts] - receipts: a/:a all, m/:m missing, e/:e existing, u/:u unused
53
54
  rep[orts] - return an OpenStruct containing all reports (interactive shell mode only)
54
- d[isplay_reports] - display all reports on stdout
55
55
  w[rite_reports] - write all reports to the output directory (see -o option)
56
56
  c[hart_of_accounts] - chart of accounts
57
57
  h[elp] - prints this help
58
58
  jo[urnals] - list of the journals' short names
59
- proj[ect_page] - open the RockBooks Github project page in a browser
59
+ proj[ect_page] - prints the RockBooks Github project page URL
60
60
  rel[oad_data] - reload data from input files
61
61
  q[uit] - exits this program (interactive shell mode only) (see also 'x')
62
62
  x[it] - exits this program (interactive shell mode only) (see also 'q')
@@ -281,7 +281,7 @@ When in interactive shell mode:
281
281
  # All reports as Ruby objects; only makes sense in shell mode.
282
282
  def cmd_rep
283
283
  unless run_options.interactive_mode
284
- raise Error.new("Option 'all_reports' is only available in shell mode. Try 'display_reports' or 'write_reports'.")
284
+ raise Error.new("Option 'all_reports' is only available in shell mode. Try 'write_reports'.")
285
285
  end
286
286
 
287
287
  os = OpenStruct.new(book_set.all_reports($filter))
@@ -297,18 +297,8 @@ When in interactive shell mode:
297
297
  end
298
298
 
299
299
 
300
- def cmd_d
301
- book_set.all_reports($filter).each do |short_name, text_report|
302
- puts "#{short_name}:\n\n"
303
- puts text_report
304
- puts "\n\n\n"
305
- end
306
- nil
307
- end
308
-
309
-
310
300
  def cmd_proj
311
- `open https://github.com/keithrbennett/rock_books`
301
+ puts 'https://github.com/keithrbennett/rock_books'
312
302
  end
313
303
 
314
304
 
@@ -317,7 +307,9 @@ When in interactive shell mode:
317
307
  raise Error.new("Receipt processing was requested but has been disabled with --no-receipts.")
318
308
  end
319
309
 
320
- missing, existing, unused = book_set.missing_existing_unused_receipts
310
+ data = ReceiptsReportData.new(all_entries, run_options.receipt_dir).fetch
311
+
312
+ missing, existing, unused = data[:missing], data[:existing], data[:unused]
321
313
 
322
314
  print_missing = -> { puts "\n\nMissing Receipts:"; ap missing }
323
315
  print_existing = -> { puts "\n\nExisting Receipts:"; ap existing }
@@ -326,11 +318,11 @@ When in interactive shell mode:
326
318
  case options.first.to_s
327
319
  when 'a' # all
328
320
  if run_options.interactive_mode
329
- { missing: missing, existing: existing, unused: unused }
321
+ data
330
322
  else
331
- print_missing.()
332
- print_existing.()
333
- print_unused.()
323
+ print_missing.()
324
+ print_existing.()
325
+ print_unused.()
334
326
  end
335
327
 
336
328
  when 'm'
@@ -372,7 +364,6 @@ When in interactive shell mode:
372
364
  @commands_ ||= [
373
365
  Command.new('rec', 'receipts', -> (*options) { cmd_rec(options) }),
374
366
  Command.new('rep', 'reports', -> (*_options) { cmd_rep }),
375
- Command.new('d', 'display_reports', -> (*_options) { cmd_d }),
376
367
  Command.new('w', 'write_reports', -> (*_options) { cmd_w }),
377
368
  Command.new('c', 'chart_of_accounts', -> (*_options) { cmd_c }),
378
369
  Command.new('jo', 'journals', -> (*_options) { cmd_j }),
@@ -4,7 +4,6 @@ require 'os'
4
4
  require_relative 'chart_of_accounts'
5
5
  require_relative 'journal'
6
6
  require_relative '../filters/journal_entry_filters' # for shell mode
7
- require_relative '../helpers/html_helper'
8
7
  require_relative '../helpers/parse_helper'
9
8
  require_relative '../reports/book_set_reporter'
10
9
 
@@ -39,6 +38,10 @@ module RockBooks
39
38
  @all_entries ||= Journal.entries_in_documents(journals)
40
39
  end
41
40
 
41
+
42
+ def all_reports(filter = nil)
43
+ BookSetReporter.new(self, nil, filter).get_all_report_data
44
+ end
42
45
  end
43
46
  end
44
47
 
@@ -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/helpers/reporter'
10
+ require_relative '../reports/helpers/text_report_helper'
11
11
 
12
12
  module RockBooks
13
13
 
@@ -65,9 +65,8 @@ class Journal
65
65
  attr_reader :short_name, :account_code, :chart_of_accounts, :date_prefix, :debit_or_credit, :doc_type, :title, :entries
66
66
 
67
67
  # short_name is a name that will appear on reports identifying the journal from which a transaction comes
68
- def initialize(chart_of_accounts, input_lines, short_name = nil)
68
+ def initialize(chart_of_accounts, input_lines)
69
69
  @chart_of_accounts = chart_of_accounts
70
- @short_name = short_name
71
70
  @entries = []
72
71
  @date_prefix = ''
73
72
  input_lines.each_with_index do |line, linenum|
@@ -1,5 +1,5 @@
1
1
  require_relative 'helpers/erb_helper'
2
- require_relative 'helpers/reporter'
2
+ require_relative 'helpers/text_report_helper'
3
3
 
4
4
  module RockBooks
5
5
 
@@ -8,7 +8,7 @@ module RockBooks
8
8
  # in order to calculate the correct balances, so we ignore the global $filter.
9
9
  class BalanceSheet
10
10
 
11
- include Reporter
11
+ include TextReportHelper
12
12
  include ErbHelper
13
13
 
14
14
  attr_accessor :context, :data
@@ -4,6 +4,7 @@ require_relative 'balance_sheet'
4
4
  require_relative 'data/bs_is_data'
5
5
  require_relative 'data/receipts_report_data'
6
6
  require_relative 'income_statement'
7
+ require_relative 'index_html_page'
7
8
  require_relative 'multidoc_txn_report'
8
9
  require_relative 'receipts_report'
9
10
  require_relative 'report_context'
@@ -11,16 +12,18 @@ require_relative 'journal_report'
11
12
  require_relative 'multidoc_txn_by_account_report'
12
13
  require_relative 'tx_one_account'
13
14
  require_relative 'helpers/erb_helper'
14
- require_relative 'helpers/reporter'
15
+ require_relative 'helpers/text_report_helper'
16
+ require_relative 'helpers/receipts_hyperlink_converter'
15
17
 
16
18
  require 'prawn'
19
+ require 'tty-progressbar'
17
20
 
18
21
  module RockBooks
19
22
  class BookSetReporter
20
23
 
21
24
  extend Forwardable
22
25
 
23
- attr_reader :book_set, :output_dir, :filter, :context
26
+ attr_reader :book_set, :context, :filter, :output_dir, :progress_bar
24
27
 
25
28
  def_delegator :book_set, :all_entries
26
29
  def_delegator :book_set, :journals
@@ -35,6 +38,7 @@ class BookSetReporter
35
38
  @output_dir = output_dir
36
39
  @filter = filter
37
40
  @context = ReportContext.new(book_set.chart_of_accounts, book_set.journals, 80)
41
+ @progress_bar = TTY::ProgressBar.new("[:bar] :caption", total: report_count + 10)
38
42
  end
39
43
 
40
44
 
@@ -47,9 +51,44 @@ class BookSetReporter
47
51
  do_transaction_reports
48
52
  do_single_account_reports
49
53
  do_receipts_report
54
+ progress_bar.advance(caption: 'Finished generating reports.')
55
+ progress_bar.finish
50
56
  end
51
57
 
52
58
 
59
+ def report_count
60
+ bal_sheet_income_statement = 2
61
+ journal_count = journals.size
62
+ txn_report_count = 3
63
+ single_account_count = chart_of_accounts.accounts.size
64
+ receipt_report_count = 1
65
+ bal_sheet_income_statement + journal_count + txn_report_count + single_account_count + receipt_report_count
66
+ end
67
+
68
+
69
+ def get_all_report_data
70
+ reports = {}
71
+
72
+ reports[:bs_is] = BsIsData.new(context)
73
+
74
+ reports[:journals] = journals.each_with_object({}) do |journal, journals|
75
+ journals[journal.short_name] = JournalData.new(journal, context, filter).fetch
76
+ end
77
+
78
+ reports[:txn_reports] = {
79
+ by_account: MultidocTxnByAccountData.new(context).fetch,
80
+ by_date: MultidocTxnReportData.new(context, :date, filter).fetch,
81
+ by_amount: MultidocTxnReportData.new(context, :amount, filter).fetch
82
+ }
83
+
84
+ reports[:single_accounts] = chart_of_accounts.accounts.each_with_object({}) do |account, single_accts|
85
+ single_accts[account.code.to_sym] = TxOneAccountData.new(context, account.code).fetch
86
+ end
87
+
88
+ reports[:receipts] = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
89
+ reports
90
+ end
91
+
53
92
  # All methods after this point are private.
54
93
 
55
94
  private def do_statements
@@ -109,16 +148,6 @@ class BookSetReporter
109
148
  end
110
149
 
111
150
 
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
151
  private def create_directories
123
152
  %w(txt pdf html).each do |format|
124
153
  dir = File.join(output_dir, format, SINGLE_ACCT_SUBDIR)
@@ -138,22 +167,29 @@ class BookSetReporter
138
167
  end
139
168
 
140
169
 
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"
170
+ private def report_metadata(doc_short_name)
171
+ {
172
+ RBCreator: "RockBooks v#{VERSION} (#{PROJECT_URL})",
173
+ RBEntity: context.entity,
174
+ RBCreated: Time.now.to_s,
175
+ RBDocumentCode: doc_short_name.to_s,
176
+ }
145
177
  end
146
178
 
147
179
 
148
- private def prawn_create_document(pdf_filespec, text)
149
- Prawn::Document.generate(pdf_filespec) do
180
+ private def prawn_create_document(pdf_filespec, report_text, doc_short_name)
181
+ Prawn::Document.generate(pdf_filespec, info: report_metadata(doc_short_name)) do
150
182
  font(FONT_FILESPEC, size: 10)
151
183
 
152
184
  utf8_nonbreaking_space = "\uC2A0"
153
185
  unicode_nonbreaking_space = "\u00A0"
154
- text(text.gsub(' ', unicode_nonbreaking_space))
186
+ text(report_text.gsub(' ', unicode_nonbreaking_space))
155
187
  end
156
- puts "Finished generating #{pdf_filespec} with prawn."
188
+ end
189
+
190
+
191
+ private def html_metadata_comment(doc_short_name)
192
+ "\n" + report_metadata(doc_short_name).ai(plain: true) + "\n"
157
193
  end
158
194
 
159
195
 
@@ -165,12 +201,16 @@ class BookSetReporter
165
201
 
166
202
  create_text_report = -> { File.write(txt_filespec, text_report) }
167
203
 
168
- create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report) }
204
+ create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report, short_name) }
169
205
 
170
206
  create_html_report = -> do
171
- data = { report_body: text_report }
207
+ data = {
208
+ report_body: text_report,
209
+ title: "#{short_name} Report -- RockBooks",
210
+ metadata_comment: html_metadata_comment(short_name)
211
+ }
172
212
  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)
213
+ html_report = ReceiptsHyperlinkConverter.convert(html_raw_report, html_filespec)
174
214
  File.write(html_filespec, html_report)
175
215
  end
176
216
 
@@ -178,41 +218,15 @@ class BookSetReporter
178
218
  create_pdf_report.()
179
219
  create_html_report.()
180
220
 
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]
221
+ progress_bar.advance(caption: "Generating report: #{short_name}")
206
222
  end
207
223
 
208
224
 
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)
225
+ private def create_index_html
226
+ progress_bar.advance(caption: 'Generating index.html')
227
+ filespec = build_filespec(output_dir, 'index', 'html')
228
+ content = IndexHtmlPage.new(context, html_metadata_comment('index.html'), run_options).generate
229
+ File.write(filespec, content)
216
230
  end
217
231
  end
218
232
  end
@@ -19,7 +19,9 @@ class BsIsSectionData
19
19
  totals = journals_acct_totals.select { |code, _amount| codes.include?(code) }
20
20
  need_to_reverse_sign = %i{liability equity income}.include?(type)
21
21
  if need_to_reverse_sign
22
- totals.keys.each { |code| totals[code] = -totals[code] }
22
+ totals.keys.each do |code|
23
+ totals[code] = -totals[code] unless totals[code] == 0.0
24
+ end
23
25
  end
24
26
  totals
25
27
  end
@@ -25,6 +25,7 @@ class JournalData
25
25
  name: journal.chart_of_accounts.name_for_code(journal.account_code),
26
26
  title: journal.title,
27
27
  short_name: journal.short_name,
28
+ debit_or_credit: journal.debit_or_credit,
28
29
  start_date: context.chart_of_accounts.start_date,
29
30
  end_date: context.chart_of_accounts.end_date,
30
31
  entries: entries,
@@ -1,7 +1,7 @@
1
1
  module RockBooks
2
2
  class MultidocTxnByAccountData
3
3
 
4
- include Reporter
4
+ include TextReportHelper
5
5
 
6
6
  attr_reader :context, :account_code
7
7
 
@@ -1,7 +1,7 @@
1
1
  module RockBooks
2
2
  class TxOneAccountData
3
3
 
4
- include Reporter
4
+ include TextReportHelper
5
5
 
6
6
  attr_reader :context, :account_code, :account, :entries, :account_total, :totals
7
7
 
@@ -8,19 +8,14 @@ module ErbHelper
8
8
 
9
9
 
10
10
  def self.render_binding(erb_relative_filespec, template_binding)
11
- print "Rendering template #{erb_relative_filespec}..."
12
11
  result = erb_template(erb_relative_filespec).result(template_binding)
13
- puts 'done.'
14
12
  result
15
13
  end
16
14
 
17
15
  # Takes 2 hashes, one with data, and the other with presentation functions/lambdas, and passes their union to ERB
18
16
  # for rendering.
19
17
  def self.render_hashes(erb_relative_filespec, data_hash, presentation_hash)
20
- print "Rendering template #{erb_relative_filespec}..."
21
18
  combined_hash = (data_hash || {}).merge(presentation_hash || {})
22
- result = erb_template(erb_relative_filespec).result_with_hash(combined_hash)
23
- puts 'done.'
24
- result
19
+ erb_template(erb_relative_filespec).result_with_hash(combined_hash)
25
20
  end
26
21
  end
@@ -0,0 +1,59 @@
1
+ module RockBooks
2
+ class ReceiptsHyperlinkConverter
3
+
4
+ def self.convert(html_string, html_filespec)
5
+ ReceiptsHyperlinkConverter.new(html_string, html_filespec).convert
6
+ end
7
+
8
+ RECEIPT_REGEX = /Receipt:\s*(\S*)/
9
+ INVOICE_REGEX = /Invoice:\s*(\S*)/
10
+
11
+ attr_reader :html_string, :num_dirs_up
12
+
13
+ def initialize(html_string, html_filespec)
14
+ @html_string = html_string
15
+ @num_dirs_up = html_filespec.include?('/single-account/') ? 3 : 2
16
+ end
17
+
18
+
19
+ def convert
20
+ process_link_type = ->(line, regex, dir_name) do
21
+ matches = regex.match(line)
22
+ if matches
23
+ listed_filespec = matches[1]
24
+ anchor_line(line, listed_filespec, dir_name)
25
+ else
26
+ line
27
+ end
28
+ end
29
+
30
+ html_string.split("\n").map do |line|
31
+ line = process_link_type.(line, RECEIPT_REGEX, 'receipts')
32
+ process_link_type.(line, INVOICE_REGEX, 'invoices')
33
+ end.join("\n")
34
+ end
35
+
36
+
37
+ # If the HTML file being created is in DATA_DIR/rockbooks-reports/html/single-account, then
38
+ # the processed link should be '../../../receipts/[receipt_filespec]'
39
+ # else it's in DATA_DIR/rockbooks-reports/html, and
40
+ # the processed link should be '../../receipts/[receipt_filespec]'
41
+ #
42
+ # `dir_name` will be 'receipts' or 'invoices'
43
+ private def dirized_filespec(listed_filespec, dir_name)
44
+ File.join(('../' * num_dirs_up), dir_name, listed_filespec)
45
+ end
46
+
47
+ private def anchor_line(line, listed_filespec, dir_name)
48
+ label = {
49
+ 'receipts' => 'Receipt',
50
+ 'invoices' => 'Invoice'
51
+ }.fetch(dir_name)
52
+
53
+ line.gsub( \
54
+ /#{label}:\s*#{listed_filespec}/, \
55
+ %Q{#{label}: <a href="#{dirized_filespec(listed_filespec, dir_name)}">#{listed_filespec}</a>})
56
+ end
57
+ end
58
+ end
59
+