rock_books 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +1 -3
- data/RELEASE_NOTES.md +12 -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 +1 -1
- 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/helpers/html_helper.rb +22 -16
- data/lib/rock_books/reports/balance_sheet.rb +8 -42
- data/lib/rock_books/reports/book_set_reporter.rb +104 -86
- 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 +26 -0
- data/lib/rock_books/reports/helpers/reporter.rb +134 -0
- data/lib/rock_books/reports/income_statement.rb +8 -46
- 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 +5 -54
- 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 +9 -44
- 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 +1 -0
- metadata +41 -9
- data/lib/rock_books/reports/index.html.erb +0 -156
- data/lib/rock_books/reports/multidoc_transaction_report.rb +0 -66
- data/lib/rock_books/reports/receipts.html.erb +0 -54
- 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,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 Reporter
|
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 Reporter
|
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,26 @@
|
|
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
|
+
print "Rendering template #{erb_relative_filespec}..."
|
12
|
+
result = erb_template(erb_relative_filespec).result(template_binding)
|
13
|
+
puts 'done.'
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
# Takes 2 hashes, one with data, and the other with presentation functions/lambdas, and passes their union to ERB
|
18
|
+
# for rendering.
|
19
|
+
def self.render_hashes(erb_relative_filespec, data_hash, presentation_hash)
|
20
|
+
print "Rendering template #{erb_relative_filespec}..."
|
21
|
+
combined_hash = (data_hash || {}).merge(presentation_hash || {})
|
22
|
+
result = erb_template(erb_relative_filespec).result_with_hash(combined_hash)
|
23
|
+
puts 'done.'
|
24
|
+
result
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require_relative '../../documents/journal_entry'
|
2
|
+
require_relative 'erb_helper'
|
3
|
+
|
4
|
+
module RockBooks
|
5
|
+
module Reporter
|
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
|
+
|