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.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/RELEASE_NOTES.md +33 -0
- data/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
- data/lib/rock_books/cmd_line/command_line_interface.rb +6 -6
- data/lib/rock_books/cmd_line/main.rb +1 -9
- data/lib/rock_books/documents/book_set.rb +4 -136
- data/lib/rock_books/documents/chart_of_accounts.rb +29 -12
- data/lib/rock_books/documents/journal.rb +2 -6
- data/lib/rock_books/documents/journal_entry.rb +7 -2
- data/lib/rock_books/documents/journal_entry_builder.rb +4 -0
- data/lib/rock_books/helpers/book_set_loader.rb +3 -3
- data/lib/rock_books/reports/balance_sheet.rb +9 -43
- data/lib/rock_books/reports/book_set_reporter.rb +207 -0
- data/lib/rock_books/reports/data/bs_is_data.rb +61 -0
- data/lib/rock_books/reports/data/bs_is_section_data.rb +28 -0
- data/lib/rock_books/reports/data/journal_data.rb +37 -0
- data/lib/rock_books/reports/data/multidoc_txn_by_account_data.rb +40 -0
- data/lib/rock_books/reports/data/multidoc_txn_report_data.rb +39 -0
- data/lib/rock_books/reports/data/receipts_report_data.rb +47 -0
- data/lib/rock_books/reports/data/tx_one_account_data.rb +37 -0
- data/lib/rock_books/reports/helpers/erb_helper.rb +21 -0
- data/lib/rock_books/reports/helpers/html_report_helper.rb +35 -0
- data/lib/rock_books/reports/helpers/text_report_helper.rb +134 -0
- data/lib/rock_books/reports/income_statement.rb +9 -47
- data/lib/rock_books/reports/journal_report.rb +72 -0
- data/lib/rock_books/reports/multidoc_txn_by_account_report.rb +32 -0
- data/lib/rock_books/reports/multidoc_txn_report.rb +25 -0
- data/lib/rock_books/reports/receipts_report.rb +6 -55
- data/lib/rock_books/reports/templates/html/index.html.erb +141 -0
- data/lib/rock_books/reports/templates/html/report_page.html.erb +12 -0
- data/lib/rock_books/reports/templates/text/_receipt_section.txt.erb +17 -0
- data/lib/rock_books/reports/templates/text/_totals.txt.erb +8 -0
- data/lib/rock_books/reports/templates/text/balance_sheet.txt.erb +21 -0
- data/lib/rock_books/reports/templates/text/income_statement.txt.erb +21 -0
- data/lib/rock_books/reports/templates/text/journal.txt.erb +20 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_by_account_report.txt.erb +28 -0
- data/lib/rock_books/reports/templates/text/multidoc_txn_report.txt.erb +22 -0
- data/lib/rock_books/reports/templates/text/receipts_report.txt.erb +13 -0
- data/lib/rock_books/reports/templates/text/tx_one_account.txt.erb +18 -0
- data/lib/rock_books/reports/tx_one_account.rb +10 -45
- data/lib/rock_books/types/account.rb +13 -1
- data/lib/rock_books/types/account_type.rb +18 -7
- data/lib/rock_books/version.rb +1 -1
- data/rock_books.gemspec +5 -3
- metadata +64 -16
- data/lib/rock_books/documents/index.html.erb +0 -156
- data/lib/rock_books/documents/receipts.html.erb +0 -54
- data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
- data/lib/rock_books/reports/reporter.rb +0 -118
- data/lib/rock_books/reports/transaction_report.rb +0 -105
- 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 '
|
2
|
-
require_relative '
|
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
|
9
|
+
include TextReportHelper
|
10
|
+
include ErbHelper
|
10
11
|
|
11
|
-
|
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
|
20
|
-
|
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
|