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
@@ -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,35 @@
1
+ module RockBooks
2
+ module HtmlReportHelper
3
+
4
+ def self.convert_receipts_to_hyperlinks(original_html_string, html_filespec)
5
+ html_lines = original_html_string.split("\n")
6
+ content_changed = false
7
+
8
+ # If the HTML file being created is in DATA_DIR/rockbooks-reports/html/single-account, then
9
+ # the processed link should be '../../../receipts/[receipt_filespec]'
10
+ # else it's in DATA_DIR/rockbooks-reports/html, and
11
+ # the processed link should be '../../receipts/[receipt_filespec]'
12
+ processed_receipt_filespec = ->(listed_receipt_filespec) do
13
+ num_dirs_up = html_filespec.include?('/single-account/') ? 3 : 2
14
+ File.join(('../' * num_dirs_up), 'receipts', listed_receipt_filespec)
15
+ end
16
+
17
+ receipt_anchor_line = ->(line, listed_receipt_filespec) do
18
+ line.gsub( \
19
+ /Receipt:\s*#{listed_receipt_filespec}/, \
20
+ %Q{Receipt: <a href="#{processed_receipt_filespec.(listed_receipt_filespec)}">#{listed_receipt_filespec}</a>})
21
+ end
22
+
23
+ html_lines.each_with_index do |line, index|
24
+ matches = /Receipt:\s*(\S*)/.match(line)
25
+ if matches
26
+ listed_receipt_filespec = matches[1]
27
+ html_lines[index] = receipt_anchor_line.(line, listed_receipt_filespec)
28
+ content_changed = true
29
+ end
30
+ end
31
+
32
+ content_changed ? html_lines.join("\n") : original_html_string
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,134 @@
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
+ def template_presentation_context
114
+ {
115
+ banner_line: banner_line,
116
+ end_date: end_date,
117
+ entity: context.entity,
118
+ fn_acct_name: method(:acct_name),
119
+ fn_account_code_name_type_string_for_code: method(:account_code_name_type_string_for_code),
120
+ fn_center: method(:center),
121
+ fn_erb_render_binding: ErbHelper.method(:render_binding),
122
+ fn_erb_render_hashes: ErbHelper.method(:render_hashes),
123
+ fn_format_multidoc_entry: method(:format_multidoc_entry),
124
+ fn_section_heading: method(:section_heading),
125
+ fn_total_with_ok_or_discrepancy: method(:total_with_ok_or_discrepancy),
126
+ line_item_format_string: line_item_format_string,
127
+ short_name_format_string: SHORT_NAME_FORMAT_STRING,
128
+ start_date: start_date,
129
+ }
130
+ end
131
+ end
132
+ end
133
+
134
+
@@ -1,63 +1,25 @@
1
- require_relative '../documents/journal'
2
- require_relative 'report_context'
1
+ require_relative 'helpers/erb_helper'
2
+ require_relative 'helpers/text_report_helper'
3
3
 
4
4
  module RockBooks
5
5
 
6
6
 
7
7
  class IncomeStatement
8
8
 
9
- include Reporter
9
+ include TextReportHelper
10
+ include ErbHelper
10
11
 
11
- attr_accessor :context
12
+ attr_reader :data, :context
12
13
 
13
14
 
14
- def initialize(report_context)
15
+ def initialize(report_context, data)
15
16
  @context = report_context
17
+ @data = data
16
18
  end
17
19
 
18
20
 
19
- def start_date
20
- context.chart_of_accounts.start_date
21
+ def generate
22
+ ErbHelper.render_hashes('text/income_statement.txt.erb', data, template_presentation_context)
21
23
  end
22
-
23
-
24
- def end_date
25
- context.chart_of_accounts.end_date
26
- end
27
-
28
-
29
- def generate_header
30
- lines = [banner_line]
31
- lines << center(context.entity || 'Unspecified Entity')
32
- lines << "#{center("Income Statement -- #{start_date} to #{end_date}")}"
33
- lines << banner_line
34
- lines << ''
35
- lines << ''
36
- lines << ''
37
- lines.join("\n")
38
- end
39
-
40
-
41
- def generate_report
42
- filter = RockBooks::JournalEntryFilters.date_in_range(start_date, end_date)
43
- acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
44
- totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
45
- totals.each { |aa| aa[1] = -aa[1] } # income statement shows credits as positive, debits as negative
46
- output = generate_header
47
-
48
- income_output, income_total = generate_account_type_section('Income', totals, :income, true)
49
- expense_output, expense_total = generate_account_type_section('Expenses', totals, :expense, false)
50
-
51
- grand_total = income_total - expense_total
52
-
53
- output << [income_output, expense_output].join("\n\n")
54
- output << "\n#{"%12.2f Net Income" % grand_total}\n============\n"
55
- output
56
- end
57
-
58
- alias_method :to_s, :generate_report
59
- alias_method :call, :generate_report
60
-
61
-
62
24
  end
63
25
  end
@@ -0,0 +1,72 @@
1
+ require_relative 'data/journal_data'
2
+ require_relative 'helpers/erb_helper'
3
+ require_relative 'helpers/text_report_helper'
4
+
5
+ module RockBooks
6
+
7
+ class JournalReport
8
+
9
+ include TextReportHelper
10
+ include ErbHelper
11
+
12
+ attr_accessor :context, :report_data
13
+
14
+
15
+ def initialize(report_data, report_context, filter = nil)
16
+ @report_data = report_data
17
+ @context = report_context
18
+ end
19
+
20
+
21
+ def generate
22
+ presentation_context = template_presentation_context.merge(fn_format_entry: method(:format_entry))
23
+ ErbHelper.render_hashes('text/journal.txt.erb', report_data, presentation_context)
24
+ end
25
+
26
+
27
+ private def format_entry_first_acct_amount(entry)
28
+ entry.date.to_s \
29
+ + ' ' \
30
+ + format_acct_amount(entry.acct_amounts.last) \
31
+ + "\n"
32
+ end
33
+
34
+
35
+ # Formats an entry like this, with entry description added on additional line(s) if it exists:
36
+ # 2018-05-21 $120.00 701 Office Supplies
37
+ private def format_entry_no_split(entry)
38
+ output = format_entry_first_acct_amount(entry)
39
+
40
+ if entry.description && entry.description.length > 0
41
+ output += entry.description
42
+ end
43
+ output
44
+ end
45
+
46
+
47
+ # Formats an entry like this, with entry description added on additional line(s) if it exists::
48
+ # 2018-05-21 $120.00 95.00 701 Office Supplies
49
+ # 25.00 751 Gift to Customer
50
+ private def format_entry_with_split(entry)
51
+ output = format_entry_first_acct_amount(entry)
52
+ indent = ' ' * 12
53
+
54
+ entry.acct_amounts[1..-1].each do |acct_amount|
55
+ output << indent << format_acct_amount(acct_amount) << "\n"
56
+ end
57
+
58
+ if entry.description && entry.description.length > 0
59
+ output << entry.description
60
+ end
61
+ end
62
+
63
+
64
+ private def format_entry(entry)
65
+ if entry.acct_amounts.size > 2
66
+ format_entry_with_split(entry)
67
+ else
68
+ format_entry_no_split(entry)
69
+ end
70
+ end
71
+ end
72
+ end