rock_books 0.7.0 → 0.11.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 (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
+