rock_books 0.4.0 → 0.7.1

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 +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