rock_books 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/RELEASE_NOTES.md +12 -0
  4. data/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
  5. data/lib/rock_books/cmd_line/command_line_interface.rb +6 -6
  6. data/lib/rock_books/cmd_line/main.rb +1 -9
  7. data/lib/rock_books/documents/book_set.rb +1 -1
  8. data/lib/rock_books/documents/chart_of_accounts.rb +29 -12
  9. data/lib/rock_books/documents/journal.rb +2 -6
  10. data/lib/rock_books/documents/journal_entry.rb +7 -2
  11. data/lib/rock_books/documents/journal_entry_builder.rb +4 -0
  12. data/lib/rock_books/helpers/book_set_loader.rb +3 -3
  13. data/lib/rock_books/helpers/html_helper.rb +22 -16
  14. data/lib/rock_books/reports/balance_sheet.rb +8 -42
  15. data/lib/rock_books/reports/book_set_reporter.rb +104 -86
  16. data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
  17. data/lib/rock_books/reports/data/bs_is_section_data.rb +28 -0
  18. data/lib/rock_books/reports/data/journal_data.rb +37 -0
  19. data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +40 -0
  20. data/lib/rock_books/reports/data/multidoc_txn_report_data.rb +39 -0
  21. data/lib/rock_books/reports/data/receipts_report_data.rb +47 -0
  22. data/lib/rock_books/reports/data/tx_one_account_data.rb +37 -0
  23. data/lib/rock_books/reports/helpers/erb_helper.rb +26 -0
  24. data/lib/rock_books/reports/helpers/reporter.rb +134 -0
  25. data/lib/rock_books/reports/income_statement.rb +8 -46
  26. data/lib/rock_books/reports/journal_report.rb +72 -0
  27. data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +32 -0
  28. data/lib/rock_books/reports/multidoc_txn_report.rb +25 -0
  29. data/lib/rock_books/reports/receipts_report.rb +5 -54
  30. data/lib/rock_books/reports/templates/html/index.html.erb +141 -0
  31. data/lib/rock_books/reports/templates/html/report_page.html.erb +12 -0
  32. data/lib/rock_books/reports/templates/text/_receipt_section.txt.erb +17 -0
  33. data/lib/rock_books/reports/templates/text/_totals.txt.erb +8 -0
  34. data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +21 -0
  35. data/lib/rock_books/reports/templates/text/income_statement.txt.erb +21 -0
  36. data/lib/rock_books/reports/templates/text/journal.txt.erb +20 -0
  37. data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +28 -0
  38. data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +22 -0
  39. data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +13 -0
  40. data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +18 -0
  41. data/lib/rock_books/reports/tx_one_account.rb +9 -44
  42. data/lib/rock_books/types/account.rb +13 -1
  43. data/lib/rock_books/types/account_type.rb +18 -7
  44. data/lib/rock_books/version.rb +1 -1
  45. data/rock_books.gemspec +1 -0
  46. metadata +41 -9
  47. data/lib/rock_books/reports/index.html.erb +0 -156
  48. data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
  49. data/lib/rock_books/reports/receipts.html.erb +0 -54
  50. data/lib/rock_books/reports/reporter.rb +0 -118
  51. data/lib/rock_books/reports/transaction_report.rb +0 -105
  52. data/lib/rock_books/reports/tx_by_account.rb +0 -82
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81be281ee187aa03b43808391235de30c85e05b7983e243a5df887ec6eb12b2b
4
- data.tar.gz: 9069620c1ad95d22e35358caa4ed473899c7a897ad741f679047def6b6fee1e7
3
+ metadata.gz: c30673728b99315fc841b99b96081751f91459663c736758b7340a317407cc56
4
+ data.tar.gz: 751adad9c6038a85a696deb4076bbc93bbfb8a8650d53159aca691b281388d9f
5
5
  SHA512:
6
- metadata.gz: e67e7be616f654e5ed67e7f32836b03f56cdfc18821a7d0ee50570aadbb5cb65d0f288d0c4def2999ce2f32b150490e93aa0a6107a1995297b69d178e4ab9de9
7
- data.tar.gz: 38819a0cfa909e3171705f11d3151bc2d128d0b946d6a31ff3cb1e6a7278e60670cb820c2fceb8e66be9891891a91dcd317079050675c1b2678b099d26b76676
6
+ metadata.gz: 84ff76b2001eed18b4f4ec69f9949720a4dbca892f41a4fa6b96ddda9e19dde5660b34c514568f5410a117df8afddf84c33bf23ba31256efc2bceccbdb06bd01
7
+ data.tar.gz: 2214c3b52b24bf4b9b822059c4f41c67d7d3fc3a6fc666b8a073be79a794c769d1403d8f164085e382e1714320d71cbacd0673f591b26dd958e43f7b4cb1ab50
data/README.md CHANGED
@@ -33,10 +33,8 @@ To simplify its implementation, RockBooks assumes some conventions:
33
33
 
34
34
  #### Supported Operating Systems
35
35
 
36
- At this time, RockBooks is supported only on Mac OS and Linux. (It's really the external tools that is the issue, not the Ruby code.) It should probably work with Windows Subsystem for Linux (WSL) but it hasn't been tested. If you're unsuccessful trying to use RockBooks with WSL, give me as much information as possible and I'll try to resolve the issue.
36
+ At this time, RockBooks is tested only on Mac OS and Linux. However, it will probably work fine on Windows.
37
37
 
38
- If you get an error message saying that an external command is missing, install that command using your system's package manager (e.g. `sudo apt install txt2html`). For `wkhtmltopdf` on Linux, see [https://wkhtmltopdf.org/](https://wkhtmltopdf.org/).
39
-
40
38
  #### Text Files as Input
41
39
 
42
40
  Instead of a web interface, data input is done in plain text files. This isn't as fancy but has the following advantages:
@@ -1,3 +1,15 @@
1
+ ### v0.7.0
2
+
3
+ * Dependencies on external commands in Linux and Mac OS for generating PDF and HTML files has been eliminated,
4
+ using the prawn gem for PDF and simple ERB templating for HTML.
5
+ * Massive refactoring of reports to separate data generation from presentation.
6
+ * Reports are now computed and then written one at a time. Previously they were all computed, then all written.
7
+ * ERB is now used for generating text reports.
8
+ * Fix receipt hyperlinks in HTML output.
9
+ * Improve some error output.
10
+ * Various minor improvements and bug fixes.
11
+
12
+
1
13
  ### v0.6.1
2
14
 
3
15
  * Linux PDF generation fixed by using wkhtmltopdf instead of cupsfilter.
@@ -4,7 +4,7 @@ require 'ostruct'
4
4
 
5
5
  require_relative '../../rock_books'
6
6
  require_relative '../version'
7
- require_relative '../reports/reporter'
7
+ require_relative '../reports/helpers/reporter'
8
8
  require_relative '../helpers/book_set_loader'
9
9
 
10
10
  module RockBooks
@@ -105,7 +105,7 @@ When in interactive shell mode:
105
105
  end
106
106
  end
107
107
 
108
- validate_receipts_dir = -> do
108
+ validate_receipt_dir = -> do
109
109
  File.directory?(options.receipt_dir) ? nil : \
110
110
  "Receipts directory '#{options.receipt_dir}' does not exist. "
111
111
  end
@@ -114,7 +114,7 @@ When in interactive shell mode:
114
114
  output << validate_input_dir.()
115
115
  output << validate_output_dir.()
116
116
  if run_options.do_receipts
117
- output << validate_receipts_dir.()
117
+ output << validate_receipt_dir.()
118
118
  end
119
119
 
120
120
  output.compact!
@@ -298,9 +298,9 @@ When in interactive shell mode:
298
298
 
299
299
 
300
300
  def cmd_d
301
- book_set.all_reports($filter).each do |short_name, report_text|
301
+ book_set.all_reports($filter).each do |short_name, text_report|
302
302
  puts "#{short_name}:\n\n"
303
- puts report_text
303
+ puts text_report
304
304
  puts "\n\n\n"
305
305
  end
306
306
  nil
@@ -358,7 +358,7 @@ When in interactive shell mode:
358
358
  end
359
359
 
360
360
  def cmd_w
361
- BookSetReporter.new(book_set, run_options.output_dir, $filter).call
361
+ BookSetReporter.new(book_set, run_options.output_dir, $filter).generate
362
362
  nil
363
363
  end
364
364
 
@@ -1,4 +1,4 @@
1
- require 'awesome_print'
1
+ require 'amazing_print'
2
2
  require 'optparse'
3
3
  require 'pry'
4
4
  require 'shellwords'
@@ -67,10 +67,6 @@ class Main
67
67
  options.verbose_mode = v
68
68
  end
69
69
 
70
- parser.on('-y', '--[no-]say', 'Say error messages.') do |v|
71
- options.say = v
72
- end
73
-
74
70
  parser.on('', '--[no-]receipts', 'Include report on existing and missing receipts.') do |v|
75
71
  options.do_receipts = v
76
72
  end
@@ -101,10 +97,6 @@ class Main
101
97
 
102
98
  HEREDOC
103
99
 
104
- if run_options.say
105
- `say #{error}`
106
- end
107
-
108
100
  exit(-1)
109
101
  binding.pry
110
102
  raise error
@@ -1,4 +1,4 @@
1
- require 'awesome_print'
1
+ require 'amazing_print'
2
2
  require 'os'
3
3
 
4
4
  require_relative 'chart_of_accounts'
@@ -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'
@@ -10,8 +11,8 @@ class ChartOfAccounts
10
11
  REQUIRED_FIELDS.each { |field| attr_reader(field) }
11
12
 
12
13
 
13
- def self.from_file(file)
14
- self.new(File.readlines(file).map(&:chomp))
14
+ def self.from_file(filespec)
15
+ self.new(File.readlines(filespec).map(&:chomp), filespec)
15
16
  end
16
17
 
17
18
 
@@ -20,11 +21,16 @@ class ChartOfAccounts
20
21
  end
21
22
 
22
23
 
23
- def initialize(input_lines)
24
+ def initialize(input_lines, filespec = nil)
25
+ @filespec = filespec
24
26
  @accounts = []
25
- input_lines.each { |line| parse_line(line) }
27
+ parse_lines(input_lines)
26
28
  # TODO: Add validation for required fields.
29
+ check_for_missing_fields
30
+ end
31
+
27
32
 
33
+ def check_for_missing_fields
28
34
  missing_fields = REQUIRED_FIELDS.select do |field|
29
35
  instance_variable_get("@#{field}").nil?
30
36
  end
@@ -35,6 +41,19 @@ class ChartOfAccounts
35
41
  end
36
42
 
37
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
54
+ end
55
+
56
+
38
57
  def parse_date(date_string)
39
58
  # TODO: Add better handling for this error.
40
59
  # begin
@@ -69,16 +88,14 @@ class ChartOfAccounts
69
88
  rest = matcher[2]
70
89
 
71
90
  matcher = rest.match(/^(\S+)\s+(.*)$/)
91
+
72
92
  account_type_token = matcher[1]
73
- account_type = AccountType.to_type(account_type_token).symbol
93
+ account_type = AccountType.letter_to_type(account_type_token)
74
94
 
75
95
  name = matcher[2]
76
96
 
77
- accounts << Account.new(code, account_type, name)
97
+ accounts << Account.new(code, account_type.symbol, name)
78
98
  end
79
- rescue => e
80
- puts "Error parsing chart of accounts. Line text is:\n#{line}\n\n"
81
- raise
82
99
  end
83
100
 
84
101
  end
@@ -105,7 +122,7 @@ class ChartOfAccounts
105
122
 
106
123
 
107
124
  def report_string
108
- result = ''
125
+ result = StringIO.new
109
126
 
110
127
  if title
111
128
  result << title << "\n\n"
@@ -113,9 +130,9 @@ class ChartOfAccounts
113
130
 
114
131
  code_width = @accounts.inject(0) { |width, a| width = [width, a.code.length].max }
115
132
  format_string = "%-#{code_width}s %-10.10s %s\n"
116
- 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) }
117
134
 
118
- result
135
+ result.string
119
136
  end
120
137
 
121
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)
@@ -131,7 +128,6 @@ class Journal
131
128
  end
132
129
 
133
130
 
134
-
135
131
  def acct_amounts
136
132
  entries.each_with_object([]) { |entry, acct_amounts| acct_amounts << entry.acct_amounts }.flatten
137
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
@@ -1,29 +1,35 @@
1
1
  module RockBooks
2
2
  module HtmlHelper
3
3
 
4
- module_function
4
+ def self.convert_receipts_to_hyperlinks(original_html_string, html_filespec)
5
+ html_lines = original_html_string.split("\n")
6
+ content_changed = false
5
7
 
6
- def self.convert_receipts_to_hyperlinks(html_text)
7
- html_lines = html_text.split("\n")
8
- replacements_made = false
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
9
22
 
10
23
  html_lines.each_with_index do |line, index|
11
- matches = /Receipt:\s*(.*?)</.match(line)
24
+ matches = /Receipt:\s*(\S*)/.match(line)
12
25
  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
26
+ listed_receipt_filespec = matches[1]
27
+ html_lines[index] = receipt_anchor_line.(line, listed_receipt_filespec)
28
+ content_changed = true
19
29
  end
20
30
  end
21
31
 
22
- if replacements_made
23
- html_text = html_lines.join("\n")
24
- end
25
-
26
- [html_text, replacements_made]
32
+ content_changed ? html_lines.join("\n") : original_html_string
27
33
  end
28
34
  end
29
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
@@ -1,13 +1,19 @@
1
1
  require_relative '../documents/book_set'
2
2
 
3
3
  require_relative 'balance_sheet'
4
+ require_relative 'data/bs_is_data'
5
+ require_relative 'data/receipts_report_data'
4
6
  require_relative 'income_statement'
5
- require_relative 'multidoc_transaction_report'
7
+ require_relative 'multidoc_txn_report'
6
8
  require_relative 'receipts_report'
7
9
  require_relative 'report_context'
8
- require_relative 'transaction_report'
9
- require_relative 'tx_by_account'
10
+ require_relative 'journal_report'
11
+ require_relative 'multidoc_txn_by_account_report'
10
12
  require_relative 'tx_one_account'
13
+ require_relative 'helpers/erb_helper'
14
+ require_relative 'helpers/reporter'
15
+
16
+ require 'prawn'
11
17
 
12
18
  module RockBooks
13
19
  class BookSetReporter
@@ -21,6 +27,8 @@ class BookSetReporter
21
27
  def_delegator :book_set, :chart_of_accounts
22
28
  def_delegator :book_set, :run_options
23
29
 
30
+ FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
31
+
24
32
 
25
33
  def initialize(book_set, output_dir, filter = nil)
26
34
  @book_set = book_set
@@ -30,72 +38,84 @@ class BookSetReporter
30
38
  end
31
39
 
32
40
 
33
- def call
34
- check_prequisite_executables
35
- reports = all_reports(filter)
41
+ def generate
36
42
  create_directories
37
43
  create_index_html
38
- write_reports(reports)
44
+
45
+ do_statements
46
+ do_journals
47
+ do_transaction_reports
48
+ do_single_account_reports
49
+ do_receipts_report
39
50
  end
40
51
 
41
52
 
42
53
  # All methods after this point are private.
43
54
 
44
- private def all_reports(filter = nil)
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)
45
60
 
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)
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)
49
71
  end
72
+ end
50
73
 
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
74
 
57
- if run_options.do_receipts
58
- report_hash[:receipts] = ReceiptsReport.new(context, *missing_existing_unused_receipts).call
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)
59
81
  end
60
82
 
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
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)
65
87
  end
66
88
 
67
- report_hash
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.()
68
92
  end
69
93
 
70
94
 
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}"
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)
80
101
  end
81
- puts
82
- stdout
83
102
  end
84
103
 
85
104
 
86
- private def executable_exists?(name)
87
- `which #{name}`
88
- $?.success?
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)
89
109
  end
90
110
 
91
111
 
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
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
99
119
  end
100
120
 
101
121
 
@@ -125,71 +145,69 @@ class BookSetReporter
125
145
  end
126
146
 
127
147
 
128
- private def write_reports(reports)
148
+ private def prawn_create_document(pdf_filespec, text)
149
+ Prawn::Document.generate(pdf_filespec) do
150
+ font(FONT_FILESPEC, size: 10)
129
151
 
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')
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
134
158
 
135
- File.write(txt_filespec, report_text)
136
159
 
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
160
+ private def write_report(short_name, text_report)
156
161
 
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
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) }
161
169
 
162
- puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
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)
163
175
  end
164
- end
176
+
177
+ create_text_report.()
178
+ create_pdf_report.()
179
+ create_html_report.()
165
180
 
166
181
 
167
- private def receipt_full_filespec(receipt_filespec)
168
- File.join(run_options.receipt_dir, receipt_filespec)
182
+ puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
169
183
  end
170
184
 
171
185
 
172
186
  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
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
177
195
 
178
196
  all_entries.each do |entry|
179
197
  entry.receipts.each do |receipt|
180
- filespec = receipt_full_filespec(receipt)
181
- unused.delete(filespec)
198
+ filespec = receipt_full_filespec.(receipt)
199
+ unused_receipt_filespecs.delete(filespec)
182
200
  file_exists = File.file?(filespec)
183
- list = (file_exists ? existing : missing)
201
+ list = (file_exists ? existing_receipts : missing_receipts)
184
202
  list << { receipt: receipt, journal: entry.doc_short_name }
185
203
  end
186
204
  end
187
- [missing, existing, unused]
205
+ [missing_receipts, existing_receipts, unused_receipt_filespecs]
188
206
  end
189
207
 
190
208
 
191
209
  private def index_html_content
192
- erb_filespec = File.join(File.dirname(__FILE__), 'index.html.erb')
210
+ erb_filespec = File.join(File.dirname(__FILE__), 'templates', 'html', 'index.html.erb')
193
211
  erb = ERB.new(File.read(erb_filespec))
194
212
  erb.result_with_hash(
195
213
  journals: journals,