rock_books 0.6.0 → 0.9.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/RELEASE_NOTES.md +49 -10
  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 -2
  8. data/lib/rock_books/documents/chart_of_accounts.rb +29 -12
  9. data/lib/rock_books/documents/journal.rb +3 -8
  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/reports/balance_sheet.rb +9 -43
  14. data/lib/rock_books/reports/book_set_reporter.rb +115 -98
  15. data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
  16. data/lib/rock_books/reports/data/bs_is_section_data.rb +30 -0
  17. data/lib/rock_books/reports/data/journal_data.rb +38 -0
  18. data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +40 -0
  19. data/lib/rock_books/reports/data/multidoc_txn_report_data.rb +39 -0
  20. data/lib/rock_books/reports/data/receipts_report_data.rb +47 -0
  21. data/lib/rock_books/reports/data/tx_one_account_data.rb +37 -0
  22. data/lib/rock_books/reports/helpers/erb_helper.rb +21 -0
  23. data/lib/rock_books/reports/helpers/receipts_hyperlink_converter.rb +47 -0
  24. data/lib/rock_books/reports/helpers/text_report_helper.rb +144 -0
  25. data/lib/rock_books/reports/income_statement.rb +9 -47
  26. data/lib/rock_books/reports/index_html_page.rb +27 -0
  27. data/lib/rock_books/reports/journal_report.rb +82 -0
  28. data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +32 -0
  29. data/lib/rock_books/reports/multidoc_txn_report.rb +25 -0
  30. data/lib/rock_books/reports/receipts_report.rb +6 -55
  31. data/lib/rock_books/reports/templates/html/index.html.erb +158 -0
  32. data/lib/rock_books/reports/templates/html/report_page.html.erb +13 -0
  33. data/lib/rock_books/reports/templates/text/_receipt_section.txt.erb +17 -0
  34. data/lib/rock_books/reports/templates/text/_totals.txt.erb +8 -0
  35. data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +23 -0
  36. data/lib/rock_books/reports/templates/text/income_statement.txt.erb +23 -0
  37. data/lib/rock_books/reports/templates/text/journal.txt.erb +23 -0
  38. data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +31 -0
  39. data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +25 -0
  40. data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +16 -0
  41. data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +21 -0
  42. data/lib/rock_books/reports/tx_one_account.rb +10 -45
  43. data/lib/rock_books/types/account.rb +13 -1
  44. data/lib/rock_books/types/account_type.rb +18 -7
  45. data/lib/rock_books/version.rb +2 -1
  46. data/rock_books.gemspec +1 -0
  47. metadata +43 -10
  48. data/lib/rock_books/helpers/html_helper.rb +0 -29
  49. data/lib/rock_books/reports/index.html.erb +0 -156
  50. data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
  51. data/lib/rock_books/reports/receipts.html.erb +0 -54
  52. data/lib/rock_books/reports/reporter.rb +0 -118
  53. data/lib/rock_books/reports/transaction_report.rb +0 -105
  54. data/lib/rock_books/reports/tx_by_account.rb +0 -82
@@ -1,26 +1,36 @@
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 'index_html_page'
8
+ require_relative 'multidoc_txn_report'
6
9
  require_relative 'receipts_report'
7
10
  require_relative 'report_context'
8
- require_relative 'transaction_report'
9
- require_relative 'tx_by_account'
11
+ require_relative 'journal_report'
12
+ require_relative 'multidoc_txn_by_account_report'
10
13
  require_relative 'tx_one_account'
14
+ require_relative 'helpers/erb_helper'
15
+ require_relative 'helpers/text_report_helper'
16
+ require_relative 'helpers/receipts_hyperlink_converter'
17
+
18
+ require 'prawn'
11
19
 
12
20
  module RockBooks
13
21
  class BookSetReporter
14
22
 
15
23
  extend Forwardable
16
24
 
17
- attr_reader :book_set, :output_dir, :filter, :context
25
+ attr_reader :book_set, :context, :filter, :output_dir
18
26
 
19
27
  def_delegator :book_set, :all_entries
20
28
  def_delegator :book_set, :journals
21
29
  def_delegator :book_set, :chart_of_accounts
22
30
  def_delegator :book_set, :run_options
23
31
 
32
+ FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
33
+
24
34
 
25
35
  def initialize(book_set, output_dir, filter = nil)
26
36
  @book_set = book_set
@@ -30,72 +40,74 @@ class BookSetReporter
30
40
  end
31
41
 
32
42
 
33
- def call
34
- check_prequisite_executables
35
- reports = all_reports(filter)
43
+ def generate
36
44
  create_directories
37
45
  create_index_html
38
- write_reports(reports)
46
+
47
+ do_statements
48
+ do_journals
49
+ do_transaction_reports
50
+ do_single_account_reports
51
+ do_receipts_report
39
52
  end
40
53
 
41
54
 
42
55
  # All methods after this point are private.
43
56
 
44
- private def all_reports(filter = nil)
57
+ private def do_statements
58
+ bs_is_data = BsIsData.new(context)
45
59
 
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
60
+ bal_sheet_text_report = BalanceSheet.new(context, bs_is_data.bal_sheet_data).generate
61
+ write_report(:balance_sheet, bal_sheet_text_report)
50
62
 
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
63
+ inc_stat_text_report = IncomeStatement.new(context, bs_is_data.inc_stat_data).generate
64
+ write_report(:income_statement, inc_stat_text_report)
65
+ end
56
66
 
57
- if run_options.do_receipts
58
- report_hash[:receipts] = ReceiptsReport.new(context, *missing_existing_unused_receipts).call
59
- end
60
67
 
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
68
+ private def do_journals
69
+ journals.each do |journal|
70
+ report_data = JournalData.new(journal, context, filter).fetch
71
+ text_report = JournalReport.new(report_data, context, filter).generate
72
+ write_report(journal.short_name, text_report)
65
73
  end
66
-
67
- report_hash
68
74
  end
69
75
 
70
76
 
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
+ private def do_transaction_reports
78
+
79
+ do_date_or_amount_report = ->(sort_field, short_name) do
80
+ data = MultidocTxnReportData.new(context, sort_field, filter).fetch
81
+ text_report = MultidocTransactionReport.new(data, context).generate
82
+ write_report(short_name, text_report)
77
83
  end
78
- unless stderr.size == 0
79
- puts "\nStderr was:\n\n#{stderr}"
84
+
85
+ do_acct_report = -> do
86
+ data = MultidocTxnByAccountData.new(context).fetch
87
+ text_report = MultidocTransactionByAccountReport.new(data, context).generate
88
+ write_report(:all_txns_by_acct, text_report)
80
89
  end
81
- puts
82
- stdout
90
+
91
+ do_date_or_amount_report.(:date, :all_txns_by_date)
92
+ do_date_or_amount_report.(:amount, :all_txns_by_amount)
93
+ do_acct_report.()
83
94
  end
84
95
 
85
96
 
86
- private def executable_exists?(name)
87
- `which #{name}`
88
- $?.success?
97
+ private def do_single_account_reports
98
+ chart_of_accounts.accounts.each do |account|
99
+ short_name = ('acct_' + account.code).to_sym
100
+ data = TxOneAccountData.new(context, account.code).fetch
101
+ text_report = TxOneAccount.new(data, context).generate
102
+ write_report(short_name, text_report)
103
+ end
89
104
  end
90
105
 
91
106
 
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 cupsfilter)
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(', ')}"
98
- end
107
+ private def do_receipts_report
108
+ data = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
109
+ text_report = ReceiptsReport.new(context, data).generate
110
+ write_report(:receipts, text_report)
99
111
  end
100
112
 
101
113
 
@@ -118,84 +130,89 @@ class BookSetReporter
118
130
  end
119
131
 
120
132
 
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"
133
+ private def report_metadata(doc_short_name)
134
+ {
135
+ RBCreator: "RockBooks v#{VERSION} (#{PROJECT_URL})",
136
+ RBEntity: context.entity,
137
+ RBCreated: Time.now.to_s,
138
+ RBDocumentCode: doc_short_name.to_s,
139
+ }
125
140
  end
126
141
 
127
142
 
128
- private def write_reports(reports)
143
+ private def prawn_create_document(pdf_filespec, report_text, doc_short_name)
144
+ Prawn::Document.generate(pdf_filespec, info: report_metadata(doc_short_name)) do
145
+ font(FONT_FILESPEC, size: 10)
129
146
 
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')
147
+ utf8_nonbreaking_space = "\uC2A0"
148
+ unicode_nonbreaking_space = "\u00A0"
149
+ text(report_text.gsub(' ', unicode_nonbreaking_space))
150
+ end
151
+ end
134
152
 
135
- File.write(txt_filespec, report_text)
136
153
 
137
- # Linux & Mac OS
138
- cupsfilter = -> { run_command("cupsfilter #{txt_filespec} > #{pdf_filespec}") }
154
+ private def html_metadata_comment(doc_short_name)
155
+ "\n" + report_metadata(doc_short_name).ai(plain: true) + "\n"
156
+ end
139
157
 
140
- # Mac OS
141
- textutil = ->(font_size) do
142
- run_command("textutil -convert html -font 'Courier New Bold' -fontsize #{font_size} #{txt_filespec} -output #{html_filespec}")
143
- end
144
158
 
145
- # Linux
146
- txt2html = -> { run_command("txt2html --preformat_trigger_lines 0 #{txt_filespec} > #{html_filespec}") }
147
-
148
- # Use smaller size for the PDF but larger size for the web pages:
149
- if OS.mac?
150
- textutil.(11)
151
- cupsfilter.()
152
- textutil.(14)
153
- else
154
- txt2html.()
155
- cupsfilter.()
156
- end
159
+ private def write_report(short_name, text_report)
157
160
 
158
- hyperlinkized_text, replacements_made = HtmlHelper.convert_receipts_to_hyperlinks(File.read(html_filespec))
159
- if replacements_made
160
- File.write(html_filespec, hyperlinkized_text)
161
- end
161
+ txt_filespec = build_filespec(output_dir, short_name, 'txt')
162
+ html_filespec = build_filespec(output_dir, short_name, 'html')
163
+ pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
162
164
 
163
- puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
165
+ create_text_report = -> { File.write(txt_filespec, text_report) }
166
+
167
+ create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report, short_name) }
168
+
169
+ create_html_report = -> do
170
+ data = {
171
+ report_body: text_report,
172
+ title: "#{short_name} Report -- RockBooks",
173
+ metadata_comment: html_metadata_comment(short_name)
174
+ }
175
+ html_raw_report = ErbHelper.render_hashes("html/report_page.html.erb", data, {})
176
+ html_report = ReceiptsHyperlinkConverter.convert(html_raw_report, html_filespec)
177
+ File.write(html_filespec, html_report)
164
178
  end
165
- end
166
179
 
180
+ create_text_report.()
181
+ create_pdf_report.()
182
+ create_html_report.()
167
183
 
168
- private def receipt_full_filespec(receipt_filespec)
169
- File.join(run_options.receipt_dir, receipt_filespec)
184
+ puts "Created text, PDF, and HTML reports for #{short_name}."
170
185
  end
171
186
 
172
187
 
173
188
  private def missing_existing_unused_receipts
174
- missing = []
175
- existing = []
176
- unused = Dir['receipts/**/*'].select { |s| File.file?(s) }.sort # Remove files as they are found
177
- unused.map! { |s| "./" + s } # Prepend './' to match the data
189
+ missing_receipts = []
190
+ existing_receipts = []
191
+ receipt_full_filespec = ->(receipt_filespec) { File.join(run_options.receipt_dir, receipt_filespec) }
192
+
193
+ # We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
194
+ unused_receipt_filespecs = Dir['receipts/**/*'].select { |s| File.file?(s) } \
195
+ .sort \
196
+ .map { |s| "./" + s } # Prepend './' to match the data
178
197
 
179
198
  all_entries.each do |entry|
180
199
  entry.receipts.each do |receipt|
181
- filespec = receipt_full_filespec(receipt)
182
- unused.delete(filespec)
200
+ filespec = receipt_full_filespec.(receipt)
201
+ unused_receipt_filespecs.delete(filespec)
183
202
  file_exists = File.file?(filespec)
184
- list = (file_exists ? existing : missing)
203
+ list = (file_exists ? existing_receipts : missing_receipts)
185
204
  list << { receipt: receipt, journal: entry.doc_short_name }
186
205
  end
187
206
  end
188
- [missing, existing, unused]
207
+ [missing_receipts, existing_receipts, unused_receipt_filespecs]
189
208
  end
190
209
 
191
210
 
192
- private def index_html_content
193
- erb_filespec = File.join(File.dirname(__FILE__), 'index.html.erb')
194
- erb = ERB.new(File.read(erb_filespec))
195
- erb.result_with_hash(
196
- journals: journals,
197
- chart_of_accounts: chart_of_accounts,
198
- run_options: run_options)
211
+ private def create_index_html
212
+ filespec = build_filespec(output_dir, 'index', 'html')
213
+ content = IndexHtmlPage.new(context, html_metadata_comment('index.html'), run_options).generate
214
+ File.write(filespec, content)
215
+ puts "Created index.html"
199
216
  end
200
217
  end
201
218
  end
@@ -0,0 +1,61 @@
1
+ require_relative 'bs_is_section_data'
2
+ require_relative '../../filters/journal_entry_filters'
3
+ require_relative '../../documents/journal'
4
+ require_relative '../report_context'
5
+
6
+ module RockBooks
7
+
8
+ class BsIsData
9
+
10
+ attr_reader :journals_acct_totals, :context, :start_date, :end_date, :totals
11
+
12
+ def initialize(context)
13
+ @context = context
14
+ @start_date = context.chart_of_accounts.start_date
15
+ @end_date = context.chart_of_accounts.end_date
16
+ filter = JournalEntryFilters.date_on_or_before(end_date)
17
+ acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
18
+ @journals_acct_totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
19
+ end
20
+
21
+
22
+ def section_data(type)
23
+ BsIsSectionData.new(type, context, journals_acct_totals).fetch
24
+ end
25
+
26
+
27
+ def bal_sheet_data
28
+ {
29
+ end_date: end_date,
30
+ entity: context.entity,
31
+ sections: {
32
+ asset: section_data(:asset),
33
+ liability: section_data(:liability),
34
+ equity: section_data(:equity),
35
+ },
36
+ grand_total: journals_acct_totals.values.sum.round(2)
37
+ }
38
+ end
39
+
40
+
41
+ def inc_stat_data
42
+ income_section_data = section_data(:income)
43
+ expense_section_data = section_data(:expense)
44
+ net_income = (income_section_data[:acct_totals].values.sum.round(2) -
45
+ expense_section_data[:acct_totals].values.sum.round(2)
46
+ ).round(2)
47
+
48
+ {
49
+ start_date: start_date,
50
+ end_date: end_date,
51
+ entity: context.entity,
52
+ sections: {
53
+ income: income_section_data,
54
+ expense: expense_section_data,
55
+ },
56
+ net_income: net_income
57
+ }
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,30 @@
1
+ module RockBooks
2
+
3
+ BsIsSectionData = Struct.new(:type, :context, :journals_acct_totals)
4
+ class BsIsSectionData
5
+
6
+ def fetch
7
+ {
8
+ acct_totals: totals,
9
+ total: totals.map(&:last).sum.round(2)
10
+ }
11
+ end
12
+
13
+ private def totals
14
+ @totals ||= calc_section_acct_totals
15
+ end
16
+
17
+ private def calc_section_acct_totals
18
+ codes = context.chart_of_accounts.account_codes_of_type(type)
19
+ totals = journals_acct_totals.select { |code, _amount| codes.include?(code) }
20
+ need_to_reverse_sign = %i{liability equity income}.include?(type)
21
+ if need_to_reverse_sign
22
+ totals.keys.each do |code|
23
+ totals[code] = -totals[code] unless totals[code] == 0.0
24
+ end
25
+ end
26
+ totals
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../../types/acct_amount'
2
+
3
+ module RockBooks
4
+ class JournalData
5
+
6
+ attr_reader :journal, :context, :filter
7
+
8
+ def initialize(journal, report_context, filter = nil)
9
+ @journal = journal
10
+ @context = report_context
11
+ @filter = filter
12
+ end
13
+
14
+ def entries
15
+ return @entries if @entries
16
+ @entries = journal.entries
17
+ @entries = @entries.select { |entry| filter.(entry) } if filter
18
+ @entries
19
+ end
20
+
21
+ def fetch
22
+ totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(entries))
23
+ {
24
+ code: journal.account_code,
25
+ name: journal.chart_of_accounts.name_for_code(journal.account_code),
26
+ title: journal.title,
27
+ short_name: journal.short_name,
28
+ debit_or_credit: journal.debit_or_credit,
29
+ start_date: context.chart_of_accounts.start_date,
30
+ end_date: context.chart_of_accounts.end_date,
31
+ entries: entries,
32
+ totals: totals,
33
+ grand_total: totals.values.sum.round(2),
34
+ max_acct_code_len: context.chart_of_accounts.max_account_code_length
35
+ }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ module RockBooks
2
+ class MultidocTxnByAccountData
3
+
4
+ include TextReportHelper
5
+
6
+ attr_reader :context, :account_code
7
+
8
+
9
+ def initialize(report_context)
10
+ @context = report_context
11
+ end
12
+
13
+
14
+ def fetch
15
+ all_journal_entries = Journal.entries_in_documents(context.journals)
16
+ totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(all_journal_entries))
17
+ {
18
+ journals: context.journals,
19
+ entries: all_journal_entries,
20
+ totals: totals,
21
+ grand_total: totals.values.sum.round(2),
22
+ acct_sections: fetch_acct_sections(all_journal_entries),
23
+ }
24
+ end
25
+
26
+
27
+ private def fetch_acct_sections(all_journal_entries)
28
+ context.chart_of_accounts.accounts.map do |account|
29
+ code = account.code
30
+ acct_entries = JournalEntry.entries_containing_account_code(all_journal_entries, code)
31
+ total = JournalEntry.total_for_code(acct_entries, code)
32
+ {
33
+ code: code,
34
+ entries: acct_entries,
35
+ total: total
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end