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