rock_books 0.6.1 → 0.10.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -10
  3. data/RELEASE_NOTES.md +57 -10
  4. data/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
  5. data/lib/rock_books/cmd_line/command_line_interface.rb +15 -24
  6. data/lib/rock_books/cmd_line/main.rb +1 -9
  7. data/lib/rock_books/documents/book_set.rb +5 -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 +138 -106
  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 +59 -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 +25 -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/manual.md +13 -16
  47. data/rock_books.gemspec +2 -0
  48. metadata +57 -10
  49. data/lib/rock_books/helpers/html_helper.rb +0 -29
  50. data/lib/rock_books/reports/index.html.erb +0 -156
  51. data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
  52. data/lib/rock_books/reports/receipts.html.erb +0 -54
  53. data/lib/rock_books/reports/reporter.rb +0 -118
  54. data/lib/rock_books/reports/transaction_report.rb +0 -105
  55. data/lib/rock_books/reports/tx_by_account.rb +0 -82
@@ -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
@@ -0,0 +1,39 @@
1
+ require_relative '../../documents/journal'
2
+ require_relative '../../documents/journal_entry'
3
+
4
+ module RockBooks
5
+ class MultidocTxnReportData
6
+
7
+ attr_reader :context, :entries, :totals
8
+
9
+ def initialize(context, sort_by, filter = nil)
10
+ @context = context
11
+ @entries = fetch_entries(sort_by, filter)
12
+ @totals = fetch_totals(filter)
13
+ end
14
+
15
+ def fetch
16
+ {
17
+ journals: context.journals,
18
+ entries: entries,
19
+ totals: totals,
20
+ grand_total: totals.values.sum.round(2),
21
+ }
22
+ end
23
+
24
+ private def fetch_entries(sort_by, filter)
25
+ entries = Journal.entries_in_documents(context.journals, filter)
26
+ if sort_by == :amount
27
+ JournalEntry.sort_entries_by_amount_descending!(entries)
28
+ else
29
+ JournalEntry.sort_entries_by_date!(entries)
30
+ end
31
+ entries
32
+ end
33
+
34
+ private def fetch_totals(filter)
35
+ acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
36
+ AcctAmount.aggregate_amounts_by_account(acct_amounts)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ module RockBooks
2
+ class ReceiptsReportData
3
+
4
+ attr_reader :all_entries, :receipt_dir
5
+
6
+ def initialize(all_entries, receipt_dir)
7
+ @all_entries = all_entries
8
+ @receipt_dir = receipt_dir
9
+ end
10
+
11
+
12
+ def fetch
13
+ missing_receipts = []
14
+ existing_receipts = []
15
+
16
+ # We will start out putting all filespecs in the unused array, and delete them as they are found in the transactions.
17
+ unused_receipt_filespecs = all_receipt_filespecs
18
+
19
+ all_entries.each do |entry|
20
+ entry.receipts.each do |receipt|
21
+ filespec = receipt_full_filespec(receipt)
22
+ unused_receipt_filespecs.delete(filespec)
23
+ list = (File.file?(filespec) ? existing_receipts : missing_receipts)
24
+ list << { receipt: receipt, journal: entry.doc_short_name }
25
+ end
26
+ end
27
+
28
+ {
29
+ missing: missing_receipts,
30
+ unused: unused_receipt_filespecs,
31
+ existing: existing_receipts
32
+ }
33
+ end
34
+
35
+
36
+ private def all_receipt_filespecs
37
+ Dir['receipts/**/*'].select { |s| File.file?(s) } \
38
+ .sort \
39
+ .map { |s| "./" + s } # Prepend './' to match the data
40
+ end
41
+
42
+
43
+ private def receipt_full_filespec(receipt_filespec)
44
+ File.join(receipt_dir, receipt_filespec)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ module RockBooks
2
+ class TxOneAccountData
3
+
4
+ include TextReportHelper
5
+
6
+ attr_reader :context, :account_code, :account, :entries, :account_total, :totals
7
+
8
+
9
+ def initialize(report_context, account_code)
10
+ @context = report_context
11
+ @account_code = account_code
12
+ end
13
+
14
+ def fetch
15
+
16
+ account = context.chart_of_accounts.account_for_code(account_code)
17
+ account_string = "#{account.code} -- #{account.name} (#{account.type.to_s.capitalize})"
18
+ entries = Journal.entries_in_documents(context.journals, JournalEntryFilters.account_code(account_code))
19
+ account_total = JournalEntry.total_for_code(entries, account_code)
20
+ totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(entries))
21
+
22
+
23
+ {
24
+ account_string: account_string,
25
+ entries: entries,
26
+ total: account_total,
27
+ totals: AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(entries)),
28
+ grand_total: totals.values.sum.round(2)
29
+ }
30
+ end
31
+
32
+ private def fetch_totals(filter)
33
+ acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
34
+ AcctAmount.aggregate_amounts_by_account(acct_amounts)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ module ErbHelper
2
+
3
+ def self.erb_template(erb_relative_filespec)
4
+ erb_filespec = File.absolute_path(File.join(File.dirname(__FILE__), '..', 'templates', erb_relative_filespec))
5
+ eoutvar = "@outvar_#{erb_relative_filespec.split('.').first.split('/').last}" # dots will be evaulated by `eval`, must remove them
6
+ ERB.new(File.read(erb_filespec), eoutvar: eoutvar, trim_mode: '-')
7
+ end
8
+
9
+
10
+ def self.render_binding(erb_relative_filespec, template_binding)
11
+ result = erb_template(erb_relative_filespec).result(template_binding)
12
+ result
13
+ end
14
+
15
+ # Takes 2 hashes, one with data, and the other with presentation functions/lambdas, and passes their union to ERB
16
+ # for rendering.
17
+ def self.render_hashes(erb_relative_filespec, data_hash, presentation_hash)
18
+ combined_hash = (data_hash || {}).merge(presentation_hash || {})
19
+ erb_template(erb_relative_filespec).result_with_hash(combined_hash)
20
+ end
21
+ end
@@ -0,0 +1,59 @@
1
+ module RockBooks
2
+ class ReceiptsHyperlinkConverter
3
+
4
+ def self.convert(html_string, html_filespec)
5
+ ReceiptsHyperlinkConverter.new(html_string, html_filespec).convert
6
+ end
7
+
8
+ RECEIPT_REGEX = /Receipt:\s*(\S*)/
9
+ INVOICE_REGEX = /Invoice:\s*(\S*)/
10
+
11
+ attr_reader :html_string, :num_dirs_up
12
+
13
+ def initialize(html_string, html_filespec)
14
+ @html_string = html_string
15
+ @num_dirs_up = html_filespec.include?('/single-account/') ? 3 : 2
16
+ end
17
+
18
+
19
+ def convert
20
+ process_link_type = ->(line, regex, dir_name) do
21
+ matches = regex.match(line)
22
+ if matches
23
+ listed_filespec = matches[1]
24
+ anchor_line(line, listed_filespec, dir_name)
25
+ else
26
+ line
27
+ end
28
+ end
29
+
30
+ html_string.split("\n").map do |line|
31
+ line = process_link_type.(line, RECEIPT_REGEX, 'receipts')
32
+ process_link_type.(line, INVOICE_REGEX, 'invoices')
33
+ end.join("\n")
34
+ end
35
+
36
+
37
+ # If the HTML file being created is in DATA_DIR/rockbooks-reports/html/single-account, then
38
+ # the processed link should be '../../../receipts/[receipt_filespec]'
39
+ # else it's in DATA_DIR/rockbooks-reports/html, and
40
+ # the processed link should be '../../receipts/[receipt_filespec]'
41
+ #
42
+ # `dir_name` will be 'receipts' or 'invoices'
43
+ private def dirized_filespec(listed_filespec, dir_name)
44
+ File.join(('../' * num_dirs_up), dir_name, listed_filespec)
45
+ end
46
+
47
+ private def anchor_line(line, listed_filespec, dir_name)
48
+ label = {
49
+ 'receipts' => 'Receipt',
50
+ 'invoices' => 'Invoice'
51
+ }.fetch(dir_name)
52
+
53
+ line.gsub( \
54
+ /#{label}:\s*#{listed_filespec}/, \
55
+ %Q{#{label}: <a href="#{dirized_filespec(listed_filespec, dir_name)}">#{listed_filespec}</a>})
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,144 @@
1
+ require_relative '../../documents/journal_entry'
2
+ require_relative 'erb_helper'
3
+
4
+ module RockBooks
5
+ module TextReportHelper
6
+
7
+ SHORT_NAME_MAX_LENGTH = 16
8
+
9
+ SHORT_NAME_FORMAT_STRING = "%#{SHORT_NAME_MAX_LENGTH}.#{SHORT_NAME_MAX_LENGTH}s"
10
+
11
+
12
+ def page_width
13
+ context.page_width || 80
14
+ end
15
+
16
+
17
+ def account_code_format
18
+ @account_code_format ||= "%#{max_account_code_length}.#{max_account_code_length}s"
19
+ end
20
+
21
+
22
+ def account_code_name_type_string(account)
23
+ "#{account.code} -- #{account.name} (#{account.type.to_s.capitalize})"
24
+ end
25
+
26
+
27
+ def account_code_name_type_string_for_code(account_code)
28
+ account = context.chart_of_accounts.account_for_code(account_code)
29
+ raise "Account for code #{account_code} not found" unless account
30
+ account_code_name_type_string(account)
31
+ end
32
+
33
+
34
+ # e.g. " 117.70 tr.mileage Travel - Mileage Allowance"
35
+ def format_acct_amount(acct_amount)
36
+ sprintf("%s %s %s",
37
+ sprintf("%9.2f", acct_amount.amount),
38
+ sprintf(account_code_format, acct_amount.code),
39
+ context.chart_of_accounts.name_for_code(acct_amount.code))
40
+ end
41
+
42
+
43
+ def banner_line
44
+ @banner_line ||= '-' * page_width
45
+ end
46
+
47
+
48
+ def center(string)
49
+ indent = [(page_width - string.length) / 2, 0].max
50
+ (' ' * indent) + string
51
+ end
52
+
53
+
54
+ def max_account_code_length
55
+ @max_account_code_length ||= context.chart_of_accounts.max_account_code_length
56
+ end
57
+
58
+
59
+ def total_with_ok_or_discrepancy(amount)
60
+ status_message = (amount == 0.0) ? '(Ok)' : '(Discrepancy)'
61
+ sprintf(line_item_format_string, amount, status_message, '')
62
+ end
63
+
64
+
65
+ def format_multidoc_entry(entry)
66
+ acct_amounts = entry.acct_amounts
67
+
68
+ # "2017-10-29 hsbc_visa":
69
+ output = entry.date.to_s << ' ' << (SHORT_NAME_FORMAT_STRING % entry.doc_short_name) << ' '
70
+
71
+ indent = ' ' * output.length
72
+
73
+ output << format_acct_amount(acct_amounts.first) << "\n"
74
+
75
+ acct_amounts[1..-1].each do |acct_amount|
76
+ output << indent << format_acct_amount(acct_amount) << "\n"
77
+ end
78
+
79
+ if entry.description && entry.description.length > 0
80
+ output << entry.description
81
+ end
82
+
83
+ output
84
+ end
85
+
86
+ def line_item_format_string
87
+ @line_item_format_string ||= "%12.2f %-#{context.chart_of_accounts.max_account_code_length}s %s"
88
+ end
89
+
90
+
91
+ # :asset => "Assets\n------"
92
+ def section_heading(section_type)
93
+ title = AccountType.symbol_to_type(section_type).plural_name
94
+ "\n\n" + title + "\n" + ('-' * title.length)
95
+ end
96
+
97
+
98
+ def acct_name(code)
99
+ context.chart_of_accounts.name_for_code(code)
100
+ end
101
+
102
+
103
+ def start_date
104
+ context.chart_of_accounts.start_date
105
+ end
106
+
107
+
108
+ def end_date
109
+ context.chart_of_accounts.end_date
110
+ end
111
+
112
+
113
+ # e.g. "Generated at 2021-01-09 18:22:18 by RockBooks version 0.7.1"
114
+ def generation_info_display_string
115
+ now = Time.now
116
+ timestamp = "#{now} (#{now.utc})"
117
+ center("Generated at #{timestamp}") + "\n" + center("by RockBooks version #{RockBooks::VERSION}")
118
+ end
119
+
120
+
121
+ def template_presentation_context
122
+ {
123
+ accounting_period: "#{start_date} to #{end_date}",
124
+ banner_line: banner_line,
125
+ end_date: end_date,
126
+ entity: context.entity,
127
+ fn_acct_name: method(:acct_name),
128
+ fn_account_code_name_type_string_for_code: method(:account_code_name_type_string_for_code),
129
+ fn_center: method(:center),
130
+ fn_erb_render_binding: ErbHelper.method(:render_binding),
131
+ fn_erb_render_hashes: ErbHelper.method(:render_hashes),
132
+ fn_format_multidoc_entry: method(:format_multidoc_entry),
133
+ fn_section_heading: method(:section_heading),
134
+ fn_total_with_ok_or_discrepancy: method(:total_with_ok_or_discrepancy),
135
+ generated: generation_info_display_string,
136
+ line_item_format_string: line_item_format_string,
137
+ short_name_format_string: SHORT_NAME_FORMAT_STRING,
138
+ start_date: start_date,
139
+ }
140
+ end
141
+ end
142
+ end
143
+
144
+