rock_books 0.1.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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +200 -0
- data/RELEASE_NOTES.md +4 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/rock_books +5 -0
- data/lib/rock_books/cmd_line/command_line_interface.rb +391 -0
- data/lib/rock_books/cmd_line/main.rb +108 -0
- data/lib/rock_books/documents/book_set.rb +113 -0
- data/lib/rock_books/documents/chart_of_accounts.rb +113 -0
- data/lib/rock_books/documents/journal.rb +161 -0
- data/lib/rock_books/documents/journal_entry.rb +73 -0
- data/lib/rock_books/documents/journal_entry_builder.rb +148 -0
- data/lib/rock_books/errors/account_not_found_error.rb +20 -0
- data/lib/rock_books/errors/error.rb +10 -0
- data/lib/rock_books/filters/acct_amount_filters.rb +12 -0
- data/lib/rock_books/filters/journal_entry_filters.rb +84 -0
- data/lib/rock_books/helpers/book_set_loader.rb +62 -0
- data/lib/rock_books/helpers/parse_helper.rb +22 -0
- data/lib/rock_books/reports/balance_sheet.rb +60 -0
- data/lib/rock_books/reports/income_statement.rb +63 -0
- data/lib/rock_books/reports/multidoc_transaction_report.rb +66 -0
- data/lib/rock_books/reports/receipts_report.rb +57 -0
- data/lib/rock_books/reports/report_context.rb +15 -0
- data/lib/rock_books/reports/reporter.rb +118 -0
- data/lib/rock_books/reports/transaction_report.rb +103 -0
- data/lib/rock_books/reports/tx_by_account.rb +82 -0
- data/lib/rock_books/reports/tx_one_account.rb +63 -0
- data/lib/rock_books/types/account.rb +7 -0
- data/lib/rock_books/types/account_type.rb +33 -0
- data/lib/rock_books/types/acct_amount.rb +52 -0
- data/lib/rock_books/version.rb +3 -0
- data/lib/rock_books.rb +7 -0
- data/rock_books.gemspec +39 -0
- data/sample_data/minimal/rockbooks-inputs/2017-xyz-chart-of-accounts.rbt +62 -0
- data/sample_data/minimal/rockbooks-inputs/2017-xyz-checking-journal.rbt +17 -0
- data/sample_data/minimal/rockbooks-inputs/2017-xyz-general-journal.rbt +14 -0
- data/sample_data/minimal/rockbooks-inputs/2017-xyz-visa-journal.rbt +23 -0
- metadata +158 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative '../types/acct_amount'
|
2
|
+
require_relative 'chart_of_accounts'
|
3
|
+
require_relative 'journal'
|
4
|
+
|
5
|
+
module RockBooks
|
6
|
+
class JournalEntryBuilder < Struct.new(:line, :journal)
|
7
|
+
|
8
|
+
def acct_amounts_from_tokens(tokens, date, chart_of_accounts)
|
9
|
+
acct_amounts = []
|
10
|
+
|
11
|
+
tokens[0..-1].each_slice(2).each do |(account_code, amount)|
|
12
|
+
begin
|
13
|
+
acct_amount = AcctAmount.create_with_chart_validation(date, account_code, amount, chart_of_accounts)
|
14
|
+
rescue AccountNotFoundError => error
|
15
|
+
error.document_name = journal.short_name
|
16
|
+
error.line = line
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
|
20
|
+
acct_amounts << acct_amount
|
21
|
+
end
|
22
|
+
|
23
|
+
acct_amounts
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def validate_acct_amount_token_array_size(tokens)
|
28
|
+
if tokens.size.odd?
|
29
|
+
raise Error.new("Incorrect sequence of account codes and amounts: #{tokens}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def convert_amounts_to_floats(tokens)
|
35
|
+
(1...tokens.size).step(2) do |amount_index|
|
36
|
+
tokens[amount_index] = Float(tokens[amount_index])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def general_journal?
|
42
|
+
journal.doc_type == 'general_journal'
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# For regular journal only, not general journal.
|
47
|
+
# This converts the entered signs to the correct debit/credit signs.
|
48
|
+
def convert_signs_for_debit_credit(tokens)
|
49
|
+
|
50
|
+
# Adjust the sign of the amount for the main journal account (e.g. the checking account or credit card account)
|
51
|
+
# e.g. If it's a checking account, it is an asset, a debit account, and the transaction total
|
52
|
+
# will represent a credit to that checking account.
|
53
|
+
adjust_sign_for_main_account = ->(amount) do
|
54
|
+
(journal.debit_or_credit == :debit) ? -amount : amount
|
55
|
+
end
|
56
|
+
|
57
|
+
adjust_sign_for_other_accounts = ->(amount) do
|
58
|
+
- adjust_sign_for_main_account.(amount)
|
59
|
+
end
|
60
|
+
|
61
|
+
tokens[1] = adjust_sign_for_main_account.(tokens[1])
|
62
|
+
(3...tokens.size).step(2) do |amount_index|
|
63
|
+
tokens[amount_index] = adjust_sign_for_other_accounts.(tokens[amount_index])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Returns an array of AcctAmount instances for the array of tokens.
|
69
|
+
#
|
70
|
+
# The following applies only to regular (not general) journals:
|
71
|
+
#
|
72
|
+
# this token array will start with the transaction's total amount
|
73
|
+
# and be followed by account/amount pairs.
|
74
|
+
#
|
75
|
+
# Examples, assuming a line:
|
76
|
+
# 2018-05-20 5.79 701 1.23 702 4.56
|
77
|
+
#
|
78
|
+
# and the journal account is '101', 'D' 'My Checking Account',
|
79
|
+
# the following AcctAmoutns will be created:
|
80
|
+
# [AcctAmount code: '101', amount: -5.79, AcctAmount code: '701', 1.23, AcctAmount code: '702', 4.56, ]
|
81
|
+
#
|
82
|
+
# shortcut: if there is only 1 account (that is, it is not a split entry), give it the total amount
|
83
|
+
# ['5.79', '701'] --> [AcctAmount code: '101', amount: -5.79, AcctAmount code: '701', 5.79]
|
84
|
+
#
|
85
|
+
# If the account is a credit account, the signs will be reversed.
|
86
|
+
def build_acct_amount_array(date, tokens)
|
87
|
+
|
88
|
+
unless general_journal?
|
89
|
+
if journal.account_code.nil?
|
90
|
+
raise Error.new("An '@account_code: ' line has not yet been specified in this journal." )
|
91
|
+
end
|
92
|
+
|
93
|
+
# Prepend the array with the document account code so that total amount will be associated with it.
|
94
|
+
tokens.unshift(journal.account_code)
|
95
|
+
|
96
|
+
# For convenience in regular journals, when there is no split,
|
97
|
+
# we permit the user to omit the amount after the
|
98
|
+
# account code, since we know it will be equal to the total amount.
|
99
|
+
# We add it here, because we *will* need to include it in the data.
|
100
|
+
if tokens.size == 3
|
101
|
+
tokens << tokens[1] # copy the total amount to the sole account's amount
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
validate_acct_amount_token_array_size(tokens)
|
106
|
+
|
107
|
+
# Tokens in the odd numbered positions are dollar amounts that need to be converted from string to float.
|
108
|
+
begin
|
109
|
+
convert_amounts_to_floats(tokens)
|
110
|
+
rescue ArgumentError
|
111
|
+
raise Error.new("Float conversion or other parse error for #{date}, #{tokens}.")
|
112
|
+
end
|
113
|
+
|
114
|
+
unless general_journal?
|
115
|
+
# As a convenience, all normal journal amounts are entered as positive numbers.
|
116
|
+
# This code negates the amounts as necessary so that debits are + and credits are -.
|
117
|
+
# In general journals, the debit and credit amounts must be entered correctly already.
|
118
|
+
convert_signs_for_debit_credit(tokens)
|
119
|
+
end
|
120
|
+
|
121
|
+
acct_amounts_from_tokens(tokens, date, journal.chart_of_accounts)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def build
|
126
|
+
# this is an account line in the form: yyyy-mm-dd 101 blah blah blah
|
127
|
+
tokens = line.split
|
128
|
+
acct_amount_tokens = tokens[1..-1]
|
129
|
+
date_string = journal.date_prefix + tokens[0]
|
130
|
+
raise_error = -> do
|
131
|
+
raise Error.new("In journal '#{journal.short_name}', date string was '#{date_string}'" +
|
132
|
+
" but should be a valid date in the form YYYY-MM-DD.")
|
133
|
+
end
|
134
|
+
|
135
|
+
raise_error.() if date_string.length != 10
|
136
|
+
|
137
|
+
begin
|
138
|
+
date = Date.iso8601(date_string)
|
139
|
+
rescue ArgumentError
|
140
|
+
raise_error.()
|
141
|
+
end
|
142
|
+
|
143
|
+
acct_amounts = build_acct_amount_array(date, acct_amount_tokens)
|
144
|
+
JournalEntry.new(date, acct_amounts, journal.short_name)
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RockBooks
|
2
|
+
class AccountNotFoundError < RuntimeError
|
3
|
+
|
4
|
+
attr_accessor :bad_account_code, :document_name, :line
|
5
|
+
|
6
|
+
def initialize(bad_account_code, document_name = nil, line = nil)
|
7
|
+
self.bad_account_code = bad_account_code
|
8
|
+
super(to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
s = "Account code not found in chart of accounts: #{bad_account_code}"
|
14
|
+
if document_name && line
|
15
|
+
s << ", document: #{document_name}, line: #{line}"
|
16
|
+
end
|
17
|
+
s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# This error class is intended to differentiate errors from this library from other errors
|
2
|
+
# when this code is included in external code.
|
3
|
+
# In addition, more specific error classes in this library can subclass this one.
|
4
|
+
|
5
|
+
module RockBooks
|
6
|
+
|
7
|
+
class Error < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module RockBooks
|
4
|
+
module JournalEntryFilters
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
|
9
|
+
# Dates can be provided as a Ruby Date object, or as a string that will be converted to date (yyyy-mm-dd).
|
10
|
+
def to_date(string_or_date_object)
|
11
|
+
if string_or_date_object.is_a?(String)
|
12
|
+
Date.iso8601(string_or_date_object)
|
13
|
+
else
|
14
|
+
string_or_date_object
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def null_filter
|
20
|
+
->(entry) { true }
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def year(target_year)
|
25
|
+
->(entry) { entry.date.year == target_year }
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def month(target_year, target_month)
|
30
|
+
->(entry) do
|
31
|
+
entry.date.year == target_year && entry.date.month == target_month
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def day(target_year, target_month, target_day)
|
37
|
+
->(entry) do
|
38
|
+
entry.date.year == target_year && entry.date.month == target_month && entry.date.day == target_day
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def account_code_filter(account_code)
|
44
|
+
->(entry) do
|
45
|
+
entry.acct_amounts.map(&:code).detect { |code| code == account_code }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def date_on_or_after(date)
|
51
|
+
->(entry) { entry.date >= to_date(date) }
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def date_on_or_before(date)
|
57
|
+
date = to_date(date)
|
58
|
+
->(entry) { entry.date <= date }
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def date_in_range(start_date, end_date)
|
63
|
+
start_date = to_date(start_date)
|
64
|
+
end_date = to_date(end_date)
|
65
|
+
->(entry) { entry.date >= start_date && entry.date <= end_date }
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def all(*filters)
|
70
|
+
->(entry) { filters.all? { |filter| filter.(entry) } }
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def any(*filters)
|
75
|
+
->(entry) { filters.any? { |filter| filter.(entry) } }
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def none(*filters)
|
80
|
+
->(entry) { filters.none? { |filter| filter.(entry) } }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative '../documents/book_set'
|
2
|
+
|
3
|
+
module RockBooks
|
4
|
+
|
5
|
+
# Entry point is `load` method. Loads files in a directory to instantiate a BookSet.
|
6
|
+
module BookSetLoader
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def get_files_with_types(directory)
|
11
|
+
files = Dir[File.join(directory, '*.rbt')]
|
12
|
+
files.each_with_object({}) do |filespec, files_with_types|
|
13
|
+
files_with_types[filespec] = ParseHelper.find_document_type_in_file(filespec)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def validate_chart_account_count(chart_of_account_files)
|
19
|
+
size = chart_of_account_files.size
|
20
|
+
|
21
|
+
if size == 0
|
22
|
+
raise Error.new("Chart of accounts file not found in input directory.\n" +
|
23
|
+
" Does it have a '@doc_type: chart_of_accounts' line?")
|
24
|
+
elsif size > 1
|
25
|
+
raise Error.new("Expected only 1 chart of accounts file but found: #{chart_of_account_files}.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def validate_journal_file_count(journal_files)
|
31
|
+
if journal_files.size == 0
|
32
|
+
raise Error.new("No journal files found in directory #{directory}. " +
|
33
|
+
"A journal file must contain the line '@doc_type: journal'")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def select_files_of_type(files_with_types, target_doc_type_regex)
|
39
|
+
files_with_types.select { |filespec, doc_type| target_doc_type_regex === doc_type }.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Uses all *.rbt files in the specified directory; uses @doc_type to determine which
|
44
|
+
# is the chart of accounts and which are journals.
|
45
|
+
# To exclude a file, make the extension other than .rdt.
|
46
|
+
def load(run_options)
|
47
|
+
|
48
|
+
files_with_types = get_files_with_types(run_options.input_dir)
|
49
|
+
|
50
|
+
chart_of_account_files = select_files_of_type(files_with_types, 'chart_of_accounts')
|
51
|
+
validate_chart_account_count(chart_of_account_files)
|
52
|
+
|
53
|
+
journal_files = select_files_of_type(files_with_types, /journal/) # include 'journal' and 'general_journal'
|
54
|
+
validate_journal_file_count(journal_files)
|
55
|
+
|
56
|
+
chart_of_accounts = ChartOfAccounts.from_file(chart_of_account_files.first)
|
57
|
+
journals = journal_files.map { |fs| Journal.from_file(chart_of_accounts, fs) }
|
58
|
+
BookSet.new(run_options, chart_of_accounts, journals)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RockBooks
|
2
|
+
|
3
|
+
module ParseHelper
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
|
8
|
+
def find_document_type(document_lines)
|
9
|
+
doc_type_line = document_lines.detect { |line| /^@doc_type: /.match(line) }
|
10
|
+
if doc_type_line.nil?
|
11
|
+
nil
|
12
|
+
else
|
13
|
+
doc_type_line.split(/^@doc_type: /).last.strip
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def find_document_type_in_file(filespec)
|
19
|
+
find_document_type(File.readlines(filespec))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../filters/journal_entry_filters'
|
2
|
+
require_relative '../documents/journal'
|
3
|
+
require_relative 'report_context'
|
4
|
+
|
5
|
+
module RockBooks
|
6
|
+
|
7
|
+
# Reports the balance sheet as of the specified date.
|
8
|
+
# Unlike other reports, we need to process transactions from the beginning of time
|
9
|
+
# in order to calculate the correct balances, so we ignore the global $filter.
|
10
|
+
class BalanceSheet
|
11
|
+
|
12
|
+
include Reporter
|
13
|
+
|
14
|
+
attr_accessor :context
|
15
|
+
|
16
|
+
def initialize(report_context)
|
17
|
+
@context = report_context
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def end_date
|
22
|
+
context.end_date || Time.new.to_date
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def generate_header
|
27
|
+
lines = [banner_line]
|
28
|
+
lines << center(context.entity || 'Unspecified Entity')
|
29
|
+
lines << center("Balance Sheet -- #{end_date}")
|
30
|
+
lines << banner_line
|
31
|
+
lines << ''
|
32
|
+
lines << ''
|
33
|
+
lines << ''
|
34
|
+
lines.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def generate_report
|
39
|
+
filter = RockBooks::JournalEntryFilters.date_on_or_before(end_date)
|
40
|
+
acct_amounts = Journal.acct_amounts_in_documents(context.journals, filter)
|
41
|
+
totals = AcctAmount.aggregate_amounts_by_account(acct_amounts)
|
42
|
+
output = generate_header
|
43
|
+
|
44
|
+
asset_output, asset_total = generate_account_type_section('Assets', totals, :asset, false)
|
45
|
+
liab_output, liab_total = generate_account_type_section('Liabilities', totals, :liability, true)
|
46
|
+
equity_output, equity_total = generate_account_type_section('Equity', totals, :equity, true)
|
47
|
+
|
48
|
+
output << [asset_output, liab_output, equity_output].join("\n\n")
|
49
|
+
|
50
|
+
grand_total = asset_total - (liab_total + equity_total)
|
51
|
+
|
52
|
+
output << "\n#{"%12.2f Assets - (Liabilities + Equity)" % grand_total}\n============\n"
|
53
|
+
output
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :to_s, :generate_report
|
57
|
+
alias_method :call, :generate_report
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative '../documents/journal'
|
2
|
+
require_relative 'report_context'
|
3
|
+
|
4
|
+
module RockBooks
|
5
|
+
|
6
|
+
|
7
|
+
class IncomeStatement
|
8
|
+
|
9
|
+
include Reporter
|
10
|
+
|
11
|
+
attr_accessor :context
|
12
|
+
|
13
|
+
|
14
|
+
def initialize(report_context)
|
15
|
+
@context = report_context
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def start_date
|
20
|
+
context.start_date || Date.new(1900, 1, 1)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def end_date
|
25
|
+
context.end_date || Date.new(2100, 1, 1)
|
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
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative '../documents/journal'
|
2
|
+
require_relative 'reporter'
|
3
|
+
require_relative 'report_context'
|
4
|
+
|
5
|
+
module RockBooks
|
6
|
+
|
7
|
+
class MultidocTransactionReport
|
8
|
+
|
9
|
+
include Reporter
|
10
|
+
|
11
|
+
attr_accessor :context
|
12
|
+
|
13
|
+
SORT_BY_VALID_OPTIONS = %i(date_and_account amount)
|
14
|
+
|
15
|
+
def initialize(report_context)
|
16
|
+
@context = report_context
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def generate_header(sort_by)
|
21
|
+
lines = [banner_line]
|
22
|
+
lines << center(context.entity || 'Unspecified Entity')
|
23
|
+
lines << center('Multi Document Transaction Report')
|
24
|
+
lines << center('Sorted by Amount Descending') if sort_by == :amount
|
25
|
+
lines << ''
|
26
|
+
lines << center('Source Documents:')
|
27
|
+
lines << ''
|
28
|
+
context.journals.each do |document|
|
29
|
+
short_name = SHORT_NAME_FORMAT_STRING % document.short_name
|
30
|
+
lines << center("#{short_name} -- #{document.title}")
|
31
|
+
end
|
32
|
+
lines << banner_line
|
33
|
+
lines << ''
|
34
|
+
lines << ' Date Document Amount Account'
|
35
|
+
lines << ' ---- -------- ------ -------'
|
36
|
+
lines.join("\n") << "\n\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def generate_report(filter = nil, sort_by = :date_and_account)
|
41
|
+
unless SORT_BY_VALID_OPTIONS.include?(sort_by)
|
42
|
+
raise Error.new("sort_by option '#{sort_by}' not in valid choices of #{SORT_BY_VALID_OPTIONS}.")
|
43
|
+
end
|
44
|
+
|
45
|
+
entries = Journal.entries_in_documents(context.journals, filter)
|
46
|
+
|
47
|
+
if sort_by == :amount
|
48
|
+
JournalEntry.sort_entries_by_amount_descending!(entries)
|
49
|
+
end
|
50
|
+
|
51
|
+
sio = StringIO.new
|
52
|
+
sio << generate_header(sort_by)
|
53
|
+
entries.each { |entry| sio << format_multidoc_entry(entry) << "\n" }
|
54
|
+
|
55
|
+
totals = AcctAmount.aggregate_amounts_by_account(JournalEntry.entries_acct_amounts(entries))
|
56
|
+
sio << generate_and_format_totals('Totals', totals)
|
57
|
+
|
58
|
+
sio << "\n"
|
59
|
+
sio.string
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
alias_method :to_s, :generate_report
|
64
|
+
alias_method :call, :generate_report
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'report_context'
|
2
|
+
|
3
|
+
|
4
|
+
module RockBooks
|
5
|
+
class ReceiptsReport
|
6
|
+
|
7
|
+
include Reporter
|
8
|
+
|
9
|
+
attr_reader :context, :missing, :existing
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(report_context, missing, existing)
|
13
|
+
@context = report_context
|
14
|
+
@missing = missing
|
15
|
+
@existing = existing
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def generate_header
|
20
|
+
lines = [banner_line]
|
21
|
+
lines << center(context.entity || 'Unspecified Entity')
|
22
|
+
lines << "#{center("Receipts Report")}"
|
23
|
+
lines << banner_line
|
24
|
+
lines << ''
|
25
|
+
lines << ''
|
26
|
+
lines << ''
|
27
|
+
lines.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def receipt_info_line(info)
|
32
|
+
"%-16.16s %s\n" % [info[:journal], info[:receipt]]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def column_headings
|
37
|
+
format_string = "%-16.16s %s\n"
|
38
|
+
(format_string % ['Journal', 'Receipt Filespec']) << (format_string % %w(------- ----------------)) << "\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def generate_report
|
43
|
+
output = generate_header
|
44
|
+
|
45
|
+
output << "Missing Receipts:\n\n" << column_headings
|
46
|
+
missing.each { |info| output << receipt_info_line(info) }
|
47
|
+
|
48
|
+
output << "\n\n\nExisting Receipts:\n\n" << column_headings
|
49
|
+
existing.each { |info| output << receipt_info_line(info) }
|
50
|
+
|
51
|
+
output
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method :to_s, :generate_report
|
55
|
+
alias_method :call, :generate_report
|
56
|
+
end
|
57
|
+
end
|