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,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
|