rock_books 0.6.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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