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,108 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
require 'optparse'
|
3
|
+
require 'pry'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
require_relative '../../rock_books'
|
7
|
+
require_relative '../documents/book_set'
|
8
|
+
require_relative 'command_line_interface'
|
9
|
+
|
10
|
+
module RockBooks
|
11
|
+
|
12
|
+
class Main
|
13
|
+
|
14
|
+
|
15
|
+
def options_with_defaults
|
16
|
+
options = OpenStruct.new
|
17
|
+
options.input_dir = DEFAULT_INPUT_DIR
|
18
|
+
options.output_dir = DEFAULT_OUTPUT_DIR
|
19
|
+
options.receipt_dir = DEFAULT_RECEIPT_DIR
|
20
|
+
options.do_receipts = true
|
21
|
+
options
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def prepend_environment_options
|
26
|
+
env_opt_string = ENV['ROCKBOOKS_OPTIONS']
|
27
|
+
if env_opt_string
|
28
|
+
args_to_prepend = Shellwords.shellsplit(env_opt_string)
|
29
|
+
ARGV.unshift(args_to_prepend).flatten!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Parses the command line with Ruby's internal 'optparse'.
|
35
|
+
# OptionParser#parse! removes what it processes from ARGV, which simplifies our command parsing.
|
36
|
+
def parse_command_line
|
37
|
+
prepend_environment_options
|
38
|
+
options = options_with_defaults
|
39
|
+
|
40
|
+
OptionParser.new do |parser|
|
41
|
+
|
42
|
+
parser.on('-i', '--input_dir DIR',
|
43
|
+
"Input directory containing source data files, default: '#{DEFAULT_INPUT_DIR}'") do |v|
|
44
|
+
options.input_dir = File.expand_path(v)
|
45
|
+
end
|
46
|
+
|
47
|
+
parser.on('-o', '--output_dir DIR',
|
48
|
+
"Output directory to which report files will be written, default: '#{DEFAULT_OUTPUT_DIR}'") do |v|
|
49
|
+
options.output_dir = File.expand_path(v)
|
50
|
+
end
|
51
|
+
|
52
|
+
parser.on('-r', '--receipt_dir DIR',
|
53
|
+
"Directory root from which to find receipt filespecs, default: '#{DEFAULT_RECEIPT_DIR}'") do |v|
|
54
|
+
options.receipt_dir = File.expand_path(v)
|
55
|
+
end
|
56
|
+
|
57
|
+
parser.on('-s', '--shell', 'Start interactive shell') do |v|
|
58
|
+
options.interactive_mode = true
|
59
|
+
end
|
60
|
+
|
61
|
+
parser.on('-v', '--[no-]verbose', 'Verbose mode') do |v|
|
62
|
+
options.verbose_mode = v
|
63
|
+
end
|
64
|
+
|
65
|
+
parser.on('-y', '--[no-]say', 'Say error messages.') do |v|
|
66
|
+
options.say = v
|
67
|
+
end
|
68
|
+
|
69
|
+
parser.on('', '--[no-]receipts', 'Include report on existing and missing receipts.') do |v|
|
70
|
+
options.do_receipts = v
|
71
|
+
end
|
72
|
+
end.parse!
|
73
|
+
|
74
|
+
if options.verbose_mode
|
75
|
+
puts "Run Options:"
|
76
|
+
ap options.to_h
|
77
|
+
end
|
78
|
+
|
79
|
+
options
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Arg is a directory containing 'chart_of_accounts.rbd' and '*journal*.rbd' for input,
|
84
|
+
# and reports (*.rpt) will be output to this directory as well.
|
85
|
+
def call
|
86
|
+
begin
|
87
|
+
run_options = parse_command_line
|
88
|
+
CommandLineInterface.new(run_options).call
|
89
|
+
rescue => error
|
90
|
+
$stderr.puts \
|
91
|
+
<<~HEREDOC
|
92
|
+
#{error.backtrace.join("\n")}
|
93
|
+
|
94
|
+
#{error}
|
95
|
+
HEREDOC
|
96
|
+
|
97
|
+
if run_options.say
|
98
|
+
`say #{error}`
|
99
|
+
end
|
100
|
+
|
101
|
+
exit(-1)
|
102
|
+
binding.pry
|
103
|
+
raise error
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
|
3
|
+
require_relative 'chart_of_accounts'
|
4
|
+
require_relative 'journal'
|
5
|
+
require_relative '../filters/journal_entry_filters' # for shell mode
|
6
|
+
require_relative '../helpers/parse_helper'
|
7
|
+
require_relative '../reports/balance_sheet'
|
8
|
+
require_relative '../reports/income_statement'
|
9
|
+
require_relative '../reports/multidoc_transaction_report'
|
10
|
+
require_relative '../reports/receipts_report'
|
11
|
+
require_relative '../reports/report_context'
|
12
|
+
require_relative '../reports/transaction_report'
|
13
|
+
require_relative '../reports/tx_by_account'
|
14
|
+
require_relative '../reports/tx_one_account'
|
15
|
+
|
16
|
+
module RockBooks
|
17
|
+
|
18
|
+
class BookSet < Struct.new(:run_options, :chart_of_accounts, :journals)
|
19
|
+
|
20
|
+
FILTERS = JournalEntryFilters
|
21
|
+
|
22
|
+
|
23
|
+
def initialize(run_options, chart_of_accounts, journals)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def report_context
|
29
|
+
@report_context ||= ReportContext.new(chart_of_accounts, journals, nil, nil, 80)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def all_reports(filter = nil)
|
34
|
+
context = report_context
|
35
|
+
report_hash = context.journals.each_with_object({}) do |journal, report_hash|
|
36
|
+
report_hash[journal.short_name] = TransactionReport.new(journal, context).call(filter)
|
37
|
+
end
|
38
|
+
report_hash['all_txns_by_date'] = MultidocTransactionReport.new(context).call(filter)
|
39
|
+
report_hash['all_txns_by_amount'] = MultidocTransactionReport.new(context).call(filter, :amount)
|
40
|
+
report_hash['all_txns_by_acct'] = TxByAccount.new(context).call
|
41
|
+
report_hash['balance_sheet'] = BalanceSheet.new(context).call
|
42
|
+
report_hash['income_statement'] = IncomeStatement.new(context).call
|
43
|
+
|
44
|
+
if run_options.do_receipts
|
45
|
+
report_hash['receipts'] = ReceiptsReport.new(context, *missing_and_existing_receipts).call
|
46
|
+
end
|
47
|
+
|
48
|
+
chart_of_accounts.accounts.each do |account|
|
49
|
+
key = 'acct_' + account.code
|
50
|
+
report = TxOneAccount.new(context, account.code).call
|
51
|
+
report_hash[key] = report
|
52
|
+
end
|
53
|
+
report_hash
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def run_command(command)
|
58
|
+
command = command + ' 2>&1'
|
59
|
+
puts command
|
60
|
+
`#{command}`
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def all_reports_to_files(directory = '.', filter = nil)
|
65
|
+
reports = all_reports(filter)
|
66
|
+
reports.each do |short_name, report_text|
|
67
|
+
report_directory = /^acct_/.match(short_name) ? File.join(directory, SINGLE_ACCT_SUBDIR) : directory
|
68
|
+
txt_filespec = File.join(report_directory, "#{short_name}.txt")
|
69
|
+
html_filespec = File.join(report_directory, "#{short_name}.html")
|
70
|
+
pdf_filespec = File.join(report_directory, "#{short_name}.pdf")
|
71
|
+
File.write(txt_filespec, report_text)
|
72
|
+
run_command("textutil -convert html -font 'Menlo Regular' #{txt_filespec} -output #{html_filespec}")
|
73
|
+
run_command("cupsfilter #{html_filespec} > #{pdf_filespec}")
|
74
|
+
puts "Created reports in txt, html, and pdf for #{"%-20s" % short_name} at #{File.dirname(txt_filespec)}.\n\n\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def journal_names
|
80
|
+
journals.map(&:short_name)
|
81
|
+
end
|
82
|
+
alias_method :jnames, :journal_names
|
83
|
+
|
84
|
+
|
85
|
+
# Note: Unfiltered!
|
86
|
+
def all_acct_amounts
|
87
|
+
@all_acct_amounts ||= Journal.acct_amounts_in_documents(journals)
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def all_entries
|
92
|
+
@all_entries ||= Journal.entries_in_documents(journals)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def receipt_full_filespec(receipt_filespec)
|
97
|
+
File.join(run_options.receipt_dir, receipt_filespec)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def missing_and_existing_receipts
|
102
|
+
missing = []; existing = []
|
103
|
+
all_entries.each do |entry|
|
104
|
+
entry.receipts.each do |receipt|
|
105
|
+
file_exists = File.file?(receipt_full_filespec(receipt))
|
106
|
+
list = (file_exists ? existing : missing)
|
107
|
+
list << { receipt: receipt, journal: entry.doc_short_name }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
[missing, existing]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative '../types/account'
|
2
|
+
require_relative '../types/account_type'
|
3
|
+
require_relative '../errors/error'
|
4
|
+
|
5
|
+
module RockBooks
|
6
|
+
class ChartOfAccounts
|
7
|
+
|
8
|
+
attr_reader :doc_type, :title, :accounts, :entity
|
9
|
+
|
10
|
+
|
11
|
+
def self.from_file(file)
|
12
|
+
self.new(File.read(file))
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(input_string)
|
17
|
+
@accounts = []
|
18
|
+
lines = input_string.split("\n")
|
19
|
+
lines.each { |line| parse_line(line) }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def parse_line(line)
|
24
|
+
case line.strip
|
25
|
+
when /^@doc_type:/
|
26
|
+
@doc_type = line.split('@doc_type:').last.strip
|
27
|
+
when /^@entity:/
|
28
|
+
@entity ||= line.split('@entity:').last.strip
|
29
|
+
when /^@title:/
|
30
|
+
@title = line.split('@title:').last.strip
|
31
|
+
when /^$/
|
32
|
+
# ignore empty line
|
33
|
+
when /^#/
|
34
|
+
# ignore comment line
|
35
|
+
else
|
36
|
+
# this is an account line in the form: 101 Asset First National City Bank
|
37
|
+
# The regex below gets everything before the first whitespace in token 1, and the rest in token 2.
|
38
|
+
matcher = line.match(/^(\S+)\s+(.*)$/)
|
39
|
+
code = matcher[1]
|
40
|
+
rest = matcher[2]
|
41
|
+
|
42
|
+
matcher = rest.match(/^(\S+)\s+(.*)$/)
|
43
|
+
account_type_token = matcher[1]
|
44
|
+
account_type = AccountType.to_type(account_type_token).symbol
|
45
|
+
|
46
|
+
name = matcher[2]
|
47
|
+
|
48
|
+
accounts << Account.new(code, account_type, name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def accounts_of_type(type)
|
53
|
+
accounts.select { |account| account.type == type }
|
54
|
+
end
|
55
|
+
|
56
|
+
def account_codes_of_type(type)
|
57
|
+
accounts_of_type(type).map(&:code)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def include?(candidate_code)
|
62
|
+
accounts.any? { |account| account.code == candidate_code }
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def report_string
|
67
|
+
result = ''
|
68
|
+
|
69
|
+
if title
|
70
|
+
result << title << "\n\n"
|
71
|
+
end
|
72
|
+
|
73
|
+
code_width = @accounts.inject(0) { |width, a| width = [width, a.code.length].max }
|
74
|
+
format_string = "%-#{code_width}s %-10.10s %s\n"
|
75
|
+
accounts.each { |a| result << (format_string % [a.code, a.type.to_s, a.name]) }
|
76
|
+
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def account_for_code(code)
|
82
|
+
accounts.detect { |a| a.code == code }
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def type_for_code(code)
|
87
|
+
found = account_for_code(code)
|
88
|
+
found ? found.type : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def name_for_code(code)
|
92
|
+
found = account_for_code(code)
|
93
|
+
found ? found.name : nil
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def max_account_code_length
|
98
|
+
@max_account_code_length ||= accounts.map { |a| a.code.length }.max
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def debit_or_credit_for_code(code)
|
103
|
+
type = type_for_code(code)
|
104
|
+
if %i(asset expense).include?(type)
|
105
|
+
:debit
|
106
|
+
elsif %i(liability equity income).include?(type)
|
107
|
+
:credit
|
108
|
+
else
|
109
|
+
raise "Unexpected type #{type} for code #{code}."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require_relative '../errors/account_not_found_error'
|
6
|
+
require_relative '../types/acct_amount'
|
7
|
+
require_relative 'journal_entry'
|
8
|
+
require_relative 'journal_entry_builder'
|
9
|
+
require_relative '../reports/reporter'
|
10
|
+
|
11
|
+
module RockBooks
|
12
|
+
|
13
|
+
# The journal will create journal entries, each of which containing an array of account/amount objects,
|
14
|
+
# copying the entry date to them.
|
15
|
+
#
|
16
|
+
# Warning: Any line beginning with a number will be assumed to be the date of a data line for an entry,
|
17
|
+
# so descriptions cannot begin with a number.
|
18
|
+
class Journal
|
19
|
+
|
20
|
+
def self.from_file(chart_of_accounts, file)
|
21
|
+
self.new(chart_of_accounts, File.read(file))
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Returns the entries in the specified documents, sorted by date and document short name,
|
26
|
+
# optionally filtered with the specified filter.
|
27
|
+
def self.entries_in_documents(documents, filter = nil)
|
28
|
+
entries = documents.each_with_object([]) do |document, entries|
|
29
|
+
entries << document.entries
|
30
|
+
end.flatten
|
31
|
+
|
32
|
+
if filter
|
33
|
+
entries = entries.select {|entry| filter.(entry) }
|
34
|
+
end
|
35
|
+
|
36
|
+
entries.sort_by do |entry|
|
37
|
+
[entry.date, entry.doc_short_name]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
def self.acct_amounts_in_documents(documents, entries_filter = nil, acct_amounts_filter = nil)
|
45
|
+
entries = entries_in_documents(documents, entries_filter)
|
46
|
+
|
47
|
+
acct_amounts = entries.each_with_object([]) do |entry, acct_amounts|
|
48
|
+
acct_amounts << entry.acct_amounts
|
49
|
+
end.flatten
|
50
|
+
|
51
|
+
if acct_amounts_filter
|
52
|
+
acct_amounts = AcctAmount.filter(acct_amounts, filter)
|
53
|
+
end
|
54
|
+
|
55
|
+
acct_amounts
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class Entry < Struct.new(:date, :amount, :acct_amounts, :description); end
|
60
|
+
|
61
|
+
attr_reader :short_name, :account_code, :chart_of_accounts, :date_prefix, :debit_or_credit, :doc_type, :title, :entries
|
62
|
+
|
63
|
+
# short_name is a name that will appear on reports identifying the journal from which a transaction comes
|
64
|
+
def initialize(chart_of_accounts, input_string, short_name = nil)
|
65
|
+
@chart_of_accounts = chart_of_accounts
|
66
|
+
@short_name = short_name
|
67
|
+
@entries = []
|
68
|
+
@date_prefix = ''
|
69
|
+
@title = ''
|
70
|
+
lines = input_string.split("\n")
|
71
|
+
lines.each { |line| parse_line(line) }
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def parse_line(line)
|
76
|
+
case line.strip
|
77
|
+
when /^@doc_type:/
|
78
|
+
@doc_type = line.split(/^@doc_type:/).last.strip
|
79
|
+
when /^@account_code:/
|
80
|
+
@account_code = line.split(/^@account_code:/).last.strip
|
81
|
+
|
82
|
+
unless chart_of_accounts.include?(@account_code)
|
83
|
+
raise AccountNotFoundError.new(@account_code)
|
84
|
+
end
|
85
|
+
|
86
|
+
# if debit or credit has not yet been specified, inherit the setting from the account:
|
87
|
+
unless @debit_or_credit
|
88
|
+
@debit_or_credit = chart_of_accounts.debit_or_credit_for_code(@account_code)
|
89
|
+
end
|
90
|
+
|
91
|
+
when /^@title:/
|
92
|
+
@title = line.split(/^@title:/).last.strip
|
93
|
+
when /^@short_name:/
|
94
|
+
@short_name = line.split(/^@short_name:/).last.strip
|
95
|
+
when /^@date_prefix:/
|
96
|
+
@date_prefix = line.split(/^@date_prefix:/).last.strip
|
97
|
+
when /^@debit_or_credit:/
|
98
|
+
data = line.split(/^@debit_or_credit:/).last.strip
|
99
|
+
@debit_or_credit = data.to_sym
|
100
|
+
when /^$/
|
101
|
+
# ignore empty line
|
102
|
+
when /^#/
|
103
|
+
# ignore comment line
|
104
|
+
when /^\d/ # a date/acct/amount line starting with a number
|
105
|
+
entries << JournalEntryBuilder.new(line, self).build
|
106
|
+
else # Text line(s) to be attached to the most recently parsed transaction
|
107
|
+
unless entries.last
|
108
|
+
raise Error.new("Entry for this description cannot be found: #{line}")
|
109
|
+
end
|
110
|
+
entries.last.description << line << "\n"
|
111
|
+
|
112
|
+
if /^Receipt:/.match(line)
|
113
|
+
receipt_spec = line.split(/^Receipt:/).last.strip
|
114
|
+
entries.last.receipts << receipt_spec
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
def acct_amounts
|
122
|
+
entries.each_with_object([]) { |entry, acct_amounts| acct_amounts << entry.acct_amounts }.flatten
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def totals_by_account
|
127
|
+
acct_amounts.each_with_object(Hash.new(0)) { |aa, totals| totals[aa.code] += aa.amount }
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def total_amount
|
132
|
+
AcctAmount.total_amount(acct_amounts)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
def to_s
|
137
|
+
super.to_s + ': ' + \
|
138
|
+
{
|
139
|
+
account_code: account_code,
|
140
|
+
debit_or_credit: debit_or_credit,
|
141
|
+
title: title
|
142
|
+
}.to_s
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def to_h
|
147
|
+
{
|
148
|
+
title: title,
|
149
|
+
account_code: account_code,
|
150
|
+
debit_or_credit: debit_or_credit,
|
151
|
+
doc_type: doc_type,
|
152
|
+
date_prefix: date_prefix,
|
153
|
+
entries: entries
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def to_json; to_h.to_json; end
|
159
|
+
def to_yaml; to_h.to_yaml; end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require_relative '../types/acct_amount'
|
2
|
+
require_relative '../filters/acct_amount_filters'
|
3
|
+
|
4
|
+
module RockBooks
|
5
|
+
|
6
|
+
class JournalEntry < Struct.new(:date, :acct_amounts, :doc_short_name, :description, :receipts)
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(date, acct_amounts = [], doc_short_name = nil, description = '', receipts = [])
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def self.entries_acct_amounts(entries)
|
15
|
+
acct_amounts = entries.each_with_object([]) do |entry, acct_amounts|
|
16
|
+
acct_amounts << entry.acct_amounts
|
17
|
+
end
|
18
|
+
acct_amounts.flatten!
|
19
|
+
acct_amounts
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def self.entries_containing_account_code(entries, account_code)
|
24
|
+
entries.select { |entry| entry.contains_account?(account_code) }
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def self.total_for_code(entries, account_code)
|
29
|
+
entries.map { |entry| entry.total_for_code(account_code)}.sum
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def self.sort_entries_by_amount_descending!(entries)
|
34
|
+
entries.sort_by! do |entry|
|
35
|
+
[entry.total_absolute_value, entry.doc_short_name]
|
36
|
+
end
|
37
|
+
entries.reverse!
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def total_for_code(account_code)
|
42
|
+
acct_amounts_with_code(account_code).map(&:amount).sum
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def acct_amounts_with_code(account_code)
|
47
|
+
AcctAmount.filter(acct_amounts, AcctAmountFilters.account_code(account_code))
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def total_amount
|
52
|
+
acct_amounts.inject(0) { |sum, aa| sum + aa.amount }
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Gets the absolute value of the positive (or negative) amounts in this entry.
|
57
|
+
# This is used to sort by transaction amount, since total of all amounts will always be zero.
|
58
|
+
def total_absolute_value
|
59
|
+
acct_amounts.map(&:amount).select { |n| n.positive? }.sum
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def balanced?
|
64
|
+
total_amount == 0.0
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def contains_account?(account_code)
|
69
|
+
acct_amounts.any? { |acct_amount| acct_amount.code == account_code }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|