rock_books 0.6.1 → 0.10.0

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