rock_books 0.4.0 → 0.7.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/RELEASE_NOTES.md +33 -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 +4 -136
  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/reports/balance_sheet.rb +9 -43
  14. data/lib/rock_books/reports/book_set_reporter.rb +207 -0
  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 +28 -0
  17. data/lib/rock_books/reports/data/journal_data.rb +37 -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/html_report_helper.rb +35 -0
  24. data/lib/rock_books/reports/helpers/text_report_helper.rb +134 -0
  25. data/lib/rock_books/reports/income_statement.rb +9 -47
  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 +6 -55
  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 +10 -45
  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 +5 -3
  46. metadata +64 -16
  47. data/lib/rock_books/documents/index.html.erb +0 -156
  48. data/lib/rock_books/documents/receipts.html.erb +0 -54
  49. data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
  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
@@ -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,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/text_report_helper'
4
3
 
5
4
  module RockBooks
6
5
 
@@ -9,52 +8,19 @@ module RockBooks
9
8
  # in order to calculate the correct balances, so we ignore the global $filter.
10
9
  class BalanceSheet
11
10
 
12
- include Reporter
11
+ include TextReportHelper
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
@@ -0,0 +1,207 @@
1
+ require_relative '../documents/book_set'
2
+
3
+ require_relative 'balance_sheet'
4
+ require_relative 'data/bs_is_data'
5
+ require_relative 'data/receipts_report_data'
6
+ require_relative 'income_statement'
7
+ require_relative 'multidoc_txn_report'
8
+ require_relative 'receipts_report'
9
+ require_relative 'report_context'
10
+ require_relative 'journal_report'
11
+ require_relative 'multidoc_txn_by_account_report'
12
+ require_relative 'tx_one_account'
13
+ require_relative 'helpers/erb_helper'
14
+ require_relative 'helpers/text_report_helper'
15
+ require_relative 'helpers/html_report_helper'
16
+
17
+ require 'prawn'
18
+
19
+ module RockBooks
20
+ class BookSetReporter
21
+
22
+ extend Forwardable
23
+
24
+ attr_reader :book_set, :output_dir, :filter, :context
25
+
26
+ def_delegator :book_set, :all_entries
27
+ def_delegator :book_set, :journals
28
+ def_delegator :book_set, :chart_of_accounts
29
+ def_delegator :book_set, :run_options
30
+
31
+ FONT_FILESPEC = File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'fonts', 'JetBrainsMono-Medium.ttf'))
32
+
33
+
34
+ def initialize(book_set, output_dir, filter = nil)
35
+ @book_set = book_set
36
+ @output_dir = output_dir
37
+ @filter = filter
38
+ @context = ReportContext.new(book_set.chart_of_accounts, book_set.journals, 80)
39
+ end
40
+
41
+
42
+ def generate
43
+ create_directories
44
+ create_index_html
45
+
46
+ do_statements
47
+ do_journals
48
+ do_transaction_reports
49
+ do_single_account_reports
50
+ do_receipts_report
51
+ end
52
+
53
+
54
+ # All methods after this point are private.
55
+
56
+ private def do_statements
57
+ bs_is_data = BsIsData.new(context)
58
+
59
+ bal_sheet_text_report = BalanceSheet.new(context, bs_is_data.bal_sheet_data).generate
60
+ write_report(:balance_sheet, bal_sheet_text_report)
61
+
62
+ inc_stat_text_report = IncomeStatement.new(context, bs_is_data.inc_stat_data).generate
63
+ write_report(:income_statement, inc_stat_text_report)
64
+ end
65
+
66
+
67
+ private def do_journals
68
+ journals.each do |journal|
69
+ report_data = JournalData.new(journal, context, filter).fetch
70
+ text_report = JournalReport.new(report_data, context, filter).generate
71
+ write_report(journal.short_name, text_report)
72
+ end
73
+ end
74
+
75
+
76
+ private def do_transaction_reports
77
+
78
+ do_date_or_amount_report = ->(sort_field, short_name) do
79
+ data = MultidocTxnReportData.new(context, sort_field, filter).fetch
80
+ text_report = MultidocTransactionReport.new(data, context).generate
81
+ write_report(short_name, text_report)
82
+ end
83
+
84
+ do_acct_report = -> do
85
+ data = MultidocTxnByAccountData.new(context).fetch
86
+ text_report = MultidocTransactionByAccountReport.new(data, context).generate
87
+ write_report(:all_txns_by_acct, text_report)
88
+ end
89
+
90
+ do_date_or_amount_report.(:date, :all_txns_by_date)
91
+ do_date_or_amount_report.(:amount, :all_txns_by_amount)
92
+ do_acct_report.()
93
+ end
94
+
95
+
96
+ private def do_single_account_reports
97
+ chart_of_accounts.accounts.each do |account|
98
+ short_name = ('acct_' + account.code).to_sym
99
+ data = TxOneAccountData.new(context, account.code).fetch
100
+ text_report = TxOneAccount.new(data, context).generate
101
+ write_report(short_name, text_report)
102
+ end
103
+ end
104
+
105
+
106
+ private def do_receipts_report
107
+ data = ReceiptsReportData.new(book_set.all_entries, run_options.receipt_dir).fetch
108
+ text_report = ReceiptsReport.new(context, data).generate
109
+ write_report(:receipts, text_report)
110
+ end
111
+
112
+
113
+ private def create_directories
114
+ %w(txt pdf html).each do |format|
115
+ dir = File.join(output_dir, format, SINGLE_ACCT_SUBDIR)
116
+ FileUtils.mkdir_p(dir)
117
+ end
118
+ end
119
+
120
+
121
+ # "./pdf/short_name.pdf" or "./pdf/single_account/short_name.pdf"
122
+ private def build_filespec(directory, short_name, file_format)
123
+ fragments = [directory, file_format, "#{short_name}.#{file_format}"]
124
+ is_acct_report = /^acct_/.match(short_name)
125
+ if is_acct_report
126
+ fragments.insert(2, SINGLE_ACCT_SUBDIR)
127
+ end
128
+ File.join(*fragments)
129
+ end
130
+
131
+
132
+ private def create_index_html
133
+ filespec = build_filespec(output_dir, 'index', 'html')
134
+ File.write(filespec, index_html_content)
135
+ puts "Created index.html"
136
+ end
137
+
138
+
139
+ private def prawn_create_document(pdf_filespec, text)
140
+ Prawn::Document.generate(pdf_filespec) do
141
+ font(FONT_FILESPEC, size: 10)
142
+
143
+ utf8_nonbreaking_space = "\uC2A0"
144
+ unicode_nonbreaking_space = "\u00A0"
145
+ text(text.gsub(' ', unicode_nonbreaking_space))
146
+ end
147
+ end
148
+
149
+
150
+ private def write_report(short_name, text_report)
151
+
152
+ txt_filespec = build_filespec(output_dir, short_name, 'txt')
153
+ html_filespec = build_filespec(output_dir, short_name, 'html')
154
+ pdf_filespec = build_filespec(output_dir, short_name, 'pdf')
155
+
156
+ create_text_report = -> { File.write(txt_filespec, text_report) }
157
+
158
+ create_pdf_report = -> { prawn_create_document(pdf_filespec, text_report) }
159
+
160
+ create_html_report = -> do
161
+ data = { report_body: text_report, title: "#{short_name} Report -- RockBooks" }
162
+ html_raw_report = ErbHelper.render_hashes("html/report_page.html.erb", data, {})
163
+ html_report = HtmlReportHelper.convert_receipts_to_hyperlinks(html_raw_report, html_filespec)
164
+ File.write(html_filespec, html_report)
165
+ end
166
+
167
+ create_text_report.()
168
+ create_pdf_report.()
169
+ create_html_report.()
170
+
171
+ puts "Created text, PDF, and HTML reports for #{short_name}."
172
+ end
173
+
174
+
175
+ private def missing_existing_unused_receipts
176
+ missing_receipts = []
177
+ existing_receipts = []
178
+ receipt_full_filespec = ->(receipt_filespec) { File.join(run_options.receipt_dir, receipt_filespec) }
179
+
180
+ # We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
181
+ unused_receipt_filespecs = Dir['receipts/**/*'].select { |s| File.file?(s) } \
182
+ .sort \
183
+ .map { |s| "./" + s } # Prepend './' to match the data
184
+
185
+ all_entries.each do |entry|
186
+ entry.receipts.each do |receipt|
187
+ filespec = receipt_full_filespec.(receipt)
188
+ unused_receipt_filespecs.delete(filespec)
189
+ file_exists = File.file?(filespec)
190
+ list = (file_exists ? existing_receipts : missing_receipts)
191
+ list << { receipt: receipt, journal: entry.doc_short_name }
192
+ end
193
+ end
194
+ [missing_receipts, existing_receipts, unused_receipt_filespecs]
195
+ end
196
+
197
+
198
+ private def index_html_content
199
+ erb_filespec = File.join(File.dirname(__FILE__), 'templates', 'html', 'index.html.erb')
200
+ erb = ERB.new(File.read(erb_filespec))
201
+ erb.result_with_hash(
202
+ journals: journals,
203
+ chart_of_accounts: chart_of_accounts,
204
+ run_options: run_options)
205
+ end
206
+ end
207
+ 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,28 @@
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 { |code| totals[code] = -totals[code] }
23
+ end
24
+ totals
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,37 @@
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
+ start_date: context.chart_of_accounts.start_date,
29
+ end_date: context.chart_of_accounts.end_date,
30
+ entries: entries,
31
+ totals: totals,
32
+ grand_total: totals.values.sum.round(2),
33
+ max_acct_code_len: context.chart_of_accounts.max_account_code_length
34
+ }
35
+ end
36
+ end
37
+ 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