rock_books 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +200 -0
  8. data/RELEASE_NOTES.md +4 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/exe/rock_books +5 -0
  13. data/lib/rock_books/cmd_line/command_line_interface.rb +391 -0
  14. data/lib/rock_books/cmd_line/main.rb +108 -0
  15. data/lib/rock_books/documents/book_set.rb +113 -0
  16. data/lib/rock_books/documents/chart_of_accounts.rb +113 -0
  17. data/lib/rock_books/documents/journal.rb +161 -0
  18. data/lib/rock_books/documents/journal_entry.rb +73 -0
  19. data/lib/rock_books/documents/journal_entry_builder.rb +148 -0
  20. data/lib/rock_books/errors/account_not_found_error.rb +20 -0
  21. data/lib/rock_books/errors/error.rb +10 -0
  22. data/lib/rock_books/filters/acct_amount_filters.rb +12 -0
  23. data/lib/rock_books/filters/journal_entry_filters.rb +84 -0
  24. data/lib/rock_books/helpers/book_set_loader.rb +62 -0
  25. data/lib/rock_books/helpers/parse_helper.rb +22 -0
  26. data/lib/rock_books/reports/balance_sheet.rb +60 -0
  27. data/lib/rock_books/reports/income_statement.rb +63 -0
  28. data/lib/rock_books/reports/multidoc_transaction_report.rb +66 -0
  29. data/lib/rock_books/reports/receipts_report.rb +57 -0
  30. data/lib/rock_books/reports/report_context.rb +15 -0
  31. data/lib/rock_books/reports/reporter.rb +118 -0
  32. data/lib/rock_books/reports/transaction_report.rb +103 -0
  33. data/lib/rock_books/reports/tx_by_account.rb +82 -0
  34. data/lib/rock_books/reports/tx_one_account.rb +63 -0
  35. data/lib/rock_books/types/account.rb +7 -0
  36. data/lib/rock_books/types/account_type.rb +33 -0
  37. data/lib/rock_books/types/acct_amount.rb +52 -0
  38. data/lib/rock_books/version.rb +3 -0
  39. data/lib/rock_books.rb +7 -0
  40. data/rock_books.gemspec +39 -0
  41. data/sample_data/minimal/rockbooks-inputs/2017-xyz-chart-of-accounts.rbt +62 -0
  42. data/sample_data/minimal/rockbooks-inputs/2017-xyz-checking-journal.rbt +17 -0
  43. data/sample_data/minimal/rockbooks-inputs/2017-xyz-general-journal.rbt +14 -0
  44. data/sample_data/minimal/rockbooks-inputs/2017-xyz-visa-journal.rbt +23 -0
  45. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 42c5b49d705d543e5fbf5cf1f311e7b809a7511b898c83b372ee00b0922a8233
4
+ data.tar.gz: '0090982673c619217d19858dcad25bf05971e6c44de006fc778ed544a74b46e5'
5
+ SHA512:
6
+ metadata.gz: 60037d15787e0b3559e38050a4e26538ef111de98dbd24ea04e59869d33468869c279e924e91347f8214c168d3a758a64823b2d58f1bc3af18ac60b2115dfa5b
7
+ data.tar.gz: e96e3afde04b586a3692c4bed2e6cb854aadea35fcbe1a6a5a09b7b80585c85cdc22fb70dc76aa8ca5c316f640e602426a74d3be50eb6dd16b9f3b73654f3706
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in rock_books.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Keith Bennett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # RockBooks
2
+
3
+ A super primitive bookkeeping system using text files as input documents and console output
4
+ for reporting.
5
+
6
+ A supreme goal of this project is to give _you_ control over your data.
7
+ Want to serialize it to YAML, JSON, CSV, or manipulate it in your custom code?
8
+ No problem!
9
+
10
+ It assumes the traditional double entry bookkeeping system, with debits and credits.
11
+ In general, assets and expenses are debit balance accounts, and income, liabilities and equity
12
+ are credit balance accounts.
13
+
14
+ So, to really have this software make sense to you, you should probably understand
15
+ the double entry bookkeeping paradigm pretty well.
16
+
17
+ # Terminology Usage
18
+
19
+ * document - a RockBooks logical document such as a chart of accounts, a journal, etc.,
20
+ usually containing information parsed from a data file
21
+
22
+ * data file - a RockBooks data file, which is a text file, which
23
+ by convention has the extension `.rbt`
24
+
25
+
26
+ ## Data File Format
27
+
28
+ Lines beginning with `#` will be ignored.
29
+
30
+ Data lines that contain the value of document properties,
31
+ as opposed to transactions, etc., will be expressed as lines beginning with `@`:
32
+
33
+ ```
34
+ @doc_type: journal
35
+ @title: "ABC Bank Checking Account Disbursements Journal"
36
+ @account: ck_abc
37
+ ```
38
+
39
+ Repeating data types such as entries in journals, and accounts in the chart of accounts,
40
+ should in general be input after the properties.
41
+
42
+ Data lines will contain fields that an be separated with an arbitrary number of spaces, e.g.:
43
+
44
+ ```
45
+ 2018-05-18 123.45 703
46
+ ```
47
+
48
+ In journals, all entries will begin with dates, and all dates begin with numerals, so the
49
+ presence of a numeral in the first column will be interpreted as the beginning of a new
50
+ transaction (entry). Any lines following it not beginning with a `#` or number will be
51
+ assumed to be the textual description of the transaction, and will be saved along with
52
+ its other data.
53
+
54
+ In order to make the entry of dates more convenient, many documents will support
55
+ a `@date_prefix` property that will be prepended to dates. For example, if this prefix
56
+ contains `2018-`, then subsequent dates must exclude that prefix since it will be
57
+ automatically prepended. So, for example, a journal might contain the following lines:
58
+
59
+ ```
60
+ @date_prefix: 2018-
61
+ # ...more lines...
62
+ 05-29 37.50 ofc.spls
63
+ 05-30 22.20 tr.taxi
64
+ ```
65
+
66
+ All date strings must use the format `YYYY-MM-DD`, because that's what will be expected
67
+ by the application when it converts the date strings into numeric dates.
68
+
69
+
70
+
71
+ ### Chart of Accounts
72
+
73
+ Pretty much everything in this application assumes the presence of a chart of accounts
74
+ listing the accounts, including their codes, types, and names.
75
+
76
+ You'll need to provide a chart of accounts file that includes the following line in the header:
77
+
78
+ `@document_type: chart_of_accounts`
79
+
80
+ This file should contain the accounts
81
+ that will be used. Each account should contain the following fields:
82
+
83
+ | Property Name | Description |
84
+ | ------------- | ------------- |
85
+ | code | a short string with which to identify an account, e.g. `ret.earn` for retained earnings
86
+ | type | 'A' for asset, 'L' for liability, 'O' for (owners) equity, 'I' for income, and 'E' for expenses.
87
+ | name | a longer more descriptive name, used in reports, so no more than 30 or so characters long is recommended
88
+
89
+
90
+ So, the chart of accounts data might include something like this:
91
+
92
+ ```
93
+ ck.xyz A XYZ Bank Checking Account
94
+ loan.owner L Loan Payable to Owner
95
+ o.equity O Owner's Equity
96
+ sls.cons I Consulting Sales
97
+ tr.airfare E Travel - Air Fare
98
+ ```
99
+
100
+ Although hyphens and underscores are typically used to logically separate string fragments,
101
+ we recommend periods; they're much easier to type, and you'll be doing a lot of that.
102
+
103
+ There is no maximum length for account codes, and reports will automatically align based
104
+ on the longest account code. However, keep in mind that you will need to type these codes,
105
+ and they will consume space in reports.
106
+
107
+ ### Journals
108
+
109
+ Journals (also referred to as _documents_ by this application)
110
+ are used to record transactions of, for example:
111
+
112
+ * cash disbursements (expenditures for a single checking account)
113
+ * cash receipts (funds coming into a single checking account)
114
+ * combined cash disbursements and receipts
115
+ * a credit card account
116
+ * a Paypal account
117
+ * sales
118
+
119
+ Each journal data file needs to contain:
120
+
121
+ `@doc_type: journal`
122
+
123
+ Also, it needs to identify the code of the account the journal is representing.
124
+ So for example, if it is a journal of a PayPal account, and the PayPal
125
+ account's code is `paypal`, then you'll need a line like this in your journal file:
126
+
127
+ `@account_code: paypal`
128
+
129
+ For your convenience, when entering transactions in a journal (but _not_ a _general_ journal),
130
+ you may enter all numbers going in the direction natural for that journal as positive numbers.
131
+
132
+ For example, a _Cash Disbursements Journal_ (something like a
133
+ check register) may contain a transaction like this:
134
+
135
+ ```
136
+ 05-29 37.50 ofc.spls
137
+ ```
138
+
139
+ There may be many transactions in your journal, and it would be cumbersome to have to
140
+ type minus signs in front of all of them if they were credits.
141
+
142
+ Because of this, the program allows you to configure each journal as to the direction
143
+ (debit or credit) of the transaction. This is done with the `@debit_or_credit` property.
144
+
145
+ For an asset journal whose numbers will be crediting the main account
146
+ (e.g. a cash disbursements journal whose entries will primarily be crediting
147
+ the cash account), you would set the property to `debit`:
148
+
149
+ ```
150
+ @debit_or_credit: debit
151
+ ```
152
+
153
+
154
+ #### General Journal
155
+
156
+ The general journal is a special form of journal that does not have a primary account.
157
+
158
+ In this journal, debits and credits need to be specified literally as account code/amount
159
+ pairs, where positive numbers will result in debits, and negative numbers will result in credits, e.g.:
160
+
161
+ ```
162
+ 03-10 tr.perdiem.mi 495.00 loan.to.sh -495.00
163
+ Per Diem allowance for conference trip
164
+ ```
165
+
166
+
167
+
168
+ ## Installation
169
+
170
+ Add this line to your application's Gemfile:
171
+
172
+ ```ruby
173
+ gem 'rock_books'
174
+ ```
175
+
176
+ And then execute:
177
+
178
+ $ bundle
179
+
180
+ Or install it yourself as:
181
+
182
+ $ gem install rock_books
183
+
184
+ ## Usage
185
+
186
+ TODO: Write usage instructions here
187
+
188
+ ## Development
189
+
190
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
191
+
192
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
193
+
194
+ ## Contributing
195
+
196
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rock_books.
197
+
198
+ ## License
199
+
200
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/RELEASE_NOTES.md ADDED
@@ -0,0 +1,4 @@
1
+ ## v0.1.0
2
+
3
+ First release.
4
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rock_books"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/rock_books ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/rock_books/cmd_line/main'
4
+
5
+ RockBooks::Main.new.call
@@ -0,0 +1,391 @@
1
+ require 'fileutils'
2
+ require 'forwardable'
3
+ require 'ostruct'
4
+
5
+ require_relative '../../rock_books'
6
+ require_relative '../version'
7
+ require_relative '../reports/reporter'
8
+ require_relative '../helpers/book_set_loader'
9
+
10
+ module RockBooks
11
+
12
+ class CommandLineInterface
13
+
14
+ # Enable users to type methods of this class more conveniently:
15
+ include JournalEntryFilters
16
+ extend Forwardable
17
+
18
+ attr_reader :book_set, :interactive_mode, :run_options, :verbose_mode
19
+
20
+
21
+ class Command < Struct.new(:min_string, :max_string, :action); end
22
+
23
+
24
+ class BadCommandError < RuntimeError; end
25
+
26
+
27
+ # Enable use of some BookSet methods in shell with long and short names aaa, ae:
28
+
29
+ def_delegator :book_set, :all_acct_amounts
30
+ def_delegator :book_set, :all_acct_amounts, :aaa
31
+
32
+ def_delegator :book_set, :all_entries
33
+ def_delegator :book_set, :all_entries, :ae
34
+
35
+ def_delegator :book_set, :chart_of_accounts
36
+ def_delegator :book_set, :chart_of_accounts, :chart
37
+
38
+ # For conveniently finding the project on Github from the shell
39
+ PROJECT_URL = 'https://github.com/keithrbennett/rock_books'
40
+
41
+ # Help text to be used when requested by 'h' command, in case of unrecognized or nonexistent command, etc.
42
+ HELP_TEXT = "
43
+ Command Line Switches: [rock-books version #{RockBooks::VERSION} at https://github.com/keithrbennett/rock_books]
44
+
45
+ -i input directory specification, default: '#{DEFAULT_INPUT_DIR}'
46
+ -o output (reports) directory specification, default: '#{DEFAULT_OUTPUT_DIR}'
47
+ -r receipts directory, default: '#{DEFAULT_RECEIPT_DIR}'
48
+ -s run in shell mode
49
+
50
+ Commands:
51
+
52
+ rec[eipts] - receipts: a/:a all, m/:m missing, e/:e existing
53
+ rep[orts] - return an OpenStruct containing all reports (interactive shell mode only)
54
+ d[isplay_reports] - display all reports on stdout
55
+ w[rite_reports] - write all reports to the output directory (see -o option)
56
+ c[hart_of_accounts] - chart of accounts
57
+ h[elp] - prints this help
58
+ jo[urnals] - list of the journals' short names
59
+ rel[oad_data] - reload data from input files
60
+ q[uit] - exits this program (interactive shell mode only) (see also 'x')
61
+ x[it] - exits this program (interactive shell mode only) (see also 'q')
62
+
63
+ When in interactive shell mode:
64
+ * use quotes for string parameters such as method names.
65
+ * for pry commands, use prefix `%`.
66
+ * you can use the global variable $filter to filter reports
67
+
68
+ "
69
+
70
+ def initialize(run_options)
71
+ @run_options = run_options
72
+ @interactive_mode = !!(run_options.interactive_mode)
73
+ @verbose_mode = run_options.verbose
74
+
75
+ validate_run_options(run_options)
76
+ # book_set is set with a lazy initializer
77
+ end
78
+
79
+
80
+ def validate_run_options(options)
81
+
82
+ validate_input_dir = -> do
83
+ File.directory?(options.input_dir) ? nil : "Input directory '#{options.input_dir}' does not exist. "
84
+ end
85
+
86
+ validate_output_dir = -> do
87
+ dir = options.output_dir
88
+ subdir = File.join(dir, SINGLE_ACCT_SUBDIR)
89
+
90
+ # We need to create the reports directory and its single-account subdirectory.
91
+ # We can accomplish both by creating just the subdirectory.
92
+ FileUtils.mkdir_p(subdir) ? nil : \
93
+ "Output directory '#{dir}' and/or #{subdir} does not exist and could not be created. "
94
+ end
95
+
96
+ validate_receipts_dir = -> do
97
+ File.directory?(options.receipt_dir) ? nil : \
98
+ "Receipts directory '#{options.receipt_dir}' does not exist. " +
99
+ "If you do not want receipt handling, use the --no-receipts command line option."
100
+ end
101
+
102
+ output = []
103
+ output << validate_input_dir.()
104
+ output << validate_output_dir.()
105
+ if run_options.do_receipts
106
+ output << validate_receipts_dir.()
107
+ end
108
+
109
+ unless output.empty?
110
+ message = output.compact.join("\n") << "\n"
111
+ raise Error.new(message)
112
+ end
113
+ end
114
+
115
+
116
+ def print_help
117
+ puts HELP_TEXT
118
+ end
119
+
120
+
121
+ def enclose_in_hyphen_lines(string)
122
+ hyphen_line = "#{'-' * 80}\n"
123
+ hyphen_line + string + "\n" + hyphen_line
124
+ end
125
+
126
+
127
+ # Pry will output the content of the method from which it was called.
128
+ # This small method exists solely to reduce the amount of pry's output
129
+ # that is not needed here.
130
+ def run_pry
131
+ binding.pry
132
+
133
+ # the seemingly useless line below is needed to avoid pry's exiting
134
+ # (see https://github.com/deivid-rodriguez/pry-byebug/issues/45)
135
+ _a = nil
136
+ end
137
+
138
+
139
+ # Runs a pry session in the context of this object.
140
+ # Commands and options specified on the command line can also be specified in the shell.
141
+ def run_shell
142
+ begin
143
+ require 'pry'
144
+ rescue LoadError
145
+ message = "The 'pry' gem and/or one of its prerequisites, required for running the shell, was not found." +
146
+ " Please `gem install pry` or, if necessary, `sudo gem install pry`."
147
+ raise Error.new(message)
148
+ end
149
+
150
+ print_help
151
+
152
+ # Enable the line below if you have any problems with pry configuration being loaded
153
+ # that is messing up this runtime use of pry:
154
+ # Pry.config.should_load_rc = false
155
+
156
+ # Strangely, this is the only thing I have found that successfully suppresses the
157
+ # code context output, which is not useful here. Anyway, this will differentiate
158
+ # a pry command from a DSL command, which _is_ useful here.
159
+ Pry.config.command_prefix = '%'
160
+
161
+ run_pry
162
+ end
163
+
164
+
165
+ # Look up the command name and, if found, run it. If not, execute the passed block.
166
+ def attempt_command_action(command, *args, &error_handler_block)
167
+ no_command_specified = command.nil?
168
+ command = 'help' if no_command_specified
169
+
170
+ action = find_command_action(command)
171
+ result = nil
172
+
173
+ if action
174
+ result = action.(*args)
175
+ else
176
+ error_handler_block.call
177
+ nil
178
+ end
179
+
180
+ if no_command_specified
181
+ puts enclose_in_hyphen_lines('! No operations specified !')
182
+ end
183
+ result
184
+ end
185
+
186
+
187
+ # For use by the shell when the user types the DSL commands
188
+ def method_missing(method_name, *method_args)
189
+ attempt_command_action(method_name.to_s, *method_args) do
190
+ puts(%Q{"#{method_name}" is not a valid command or option. } \
191
+ << 'If you intend for this to be a string literal, ' \
192
+ << 'use quotes or %q{}/%Q{}.')
193
+ end
194
+ end
195
+
196
+
197
+ # Processes the command (ARGV[0]) and any relevant options (ARGV[1..-1]).
198
+ #
199
+ # CAUTION! In interactive mode, any strings entered (e.g. a network name) MUST
200
+ # be in a form that the Ruby interpreter will recognize as a string,
201
+ # i.e. single or double quotes, %q, %Q, etc.
202
+ # Otherwise it will assume it's a method name and pass it to method_missing!
203
+ def process_command_line
204
+ attempt_command_action(ARGV[0], *ARGV[1..-1]) do
205
+ print_help
206
+ raise BadCommandError.new(
207
+ %Q{! Unrecognized command. Command was #{ARGV.first.inspect} and options were #{ARGV[1..-1].inspect}.})
208
+ end
209
+ end
210
+
211
+
212
+ def quit
213
+ if interactive_mode
214
+ exit(0)
215
+ else
216
+ puts "This command can only be run in shell mode."
217
+ end
218
+ end
219
+
220
+
221
+ def cmd_c
222
+ puts chart_of_accounts.report_string
223
+ end
224
+
225
+
226
+ def cmd_h
227
+ print_help
228
+ end
229
+
230
+
231
+ def cmd_j
232
+ journal_names = book_set.journals.map(&:short_name)
233
+ interactive_mode ? journal_names : ap(journal_names)
234
+ end
235
+
236
+
237
+ def book_set
238
+ @book_set ||= load_data
239
+ end
240
+
241
+
242
+ def load_data
243
+ @book_set = BookSetLoader.load(run_options)
244
+ end
245
+ alias_method :reload_data, :load_data
246
+
247
+
248
+ def cmd_rel
249
+ reload_data
250
+ nil
251
+ end
252
+
253
+
254
+ # All reports as Ruby objects; only makes sense in shell mode.
255
+ def cmd_rep
256
+ unless run_options.interactive_mode
257
+ raise Error.new("Option 'all_reports' is only available in shell mode. Try 'display_reports' or 'write_reports'.")
258
+ end
259
+
260
+ os = OpenStruct.new(book_set.all_reports($filter))
261
+ def os.keys; to_h.keys.map(&:to_s); end # add hash methods for convenience
262
+ def os.values; to_h.values; end
263
+ def os.at(index); self.public_send(keys[index]); end # to access as array, e.g. `a.at(1)`
264
+ os
265
+ end
266
+
267
+
268
+ def cmd_d
269
+ book_set.all_reports($filter).each do |short_name, report_text|
270
+ puts "#{short_name}:\n\n"
271
+ puts report_text
272
+ puts "\n\n\n"
273
+ end
274
+ nil
275
+ end
276
+
277
+
278
+ def cmd_proj
279
+ `open https://github.com/keithrbennett/rock_books`
280
+ end
281
+
282
+
283
+ def cmd_rec(options)
284
+ unless run_options.do_receipts
285
+ raise Error.new("Receipt processing was requested but has been disabled with --no-receipts.")
286
+ end
287
+
288
+ missing, existing = book_set.missing_and_existing_receipts
289
+
290
+ print_missing = -> { puts "Missing Receipts:"; ap missing }
291
+ print_existing = -> { puts "Existing Receipts:"; ap existing }
292
+
293
+ case options.first.to_s
294
+ when 'a' # all
295
+ if run_options.interactive_mode
296
+ { missing: missing, existing: existing }
297
+ else
298
+ print_missing.(); print_existing.()
299
+ end
300
+
301
+ when 'm'
302
+ run_options.interactive_mode ? missing : print_missing.()
303
+
304
+ when 'e'
305
+ run_options.interactive_mode ? existing : print_existing.()
306
+
307
+ else
308
+ message = "Invalid option for receipts. Must be 'a' for all, 'm' for missing, or 'e' for existing."
309
+ if run_options.interactive_mode
310
+ puts message
311
+ else
312
+ raise Error.new(message)
313
+ end
314
+ end
315
+ end
316
+
317
+ def cmd_w
318
+ book_set.all_reports_to_files(run_options.output_dir, $filter)
319
+ nil
320
+ end
321
+
322
+
323
+ def cmd_x
324
+ quit
325
+ end
326
+
327
+
328
+ def commands
329
+ @commands_ ||= [
330
+ Command.new('rec', 'receipts', -> (*options) { cmd_rec(options) }),
331
+ Command.new('rep', 'reports', -> (*_options) { cmd_rep }),
332
+ Command.new('d', 'display_reports', -> (*_options) { cmd_d }),
333
+ Command.new('w', 'write_reports', -> (*_options) { cmd_w }),
334
+ Command.new('c', 'chart_of_accounts', -> (*_options) { cmd_c }),
335
+ Command.new('jo', 'journals', -> (*_options) { cmd_j }),
336
+ Command.new('h', 'help', -> (*_options) { cmd_h }),
337
+ Command.new('proj','project_page', -> (*_options) { cmd_proj }),
338
+ Command.new('q', 'quit', -> (*_options) { cmd_x }),
339
+ Command.new('rel', 'reload_data', -> (*_options) { cmd_rel }),
340
+ Command.new('x', 'xit', -> (*_options) { cmd_x })
341
+ ]
342
+ end
343
+
344
+
345
+ def find_command_action(command_string)
346
+ result = commands.detect do |cmd|
347
+ cmd.max_string.start_with?(command_string) \
348
+ && \
349
+ command_string.length >= cmd.min_string.length # e.g. 'c' by itself should not work
350
+ end
351
+ result ? result.action : nil
352
+ end
353
+
354
+
355
+ # If a post-processor has been configured (e.g. YAML or JSON), use it.
356
+ def post_process(object)
357
+ post_processor ? post_processor.(object) : object
358
+ end
359
+
360
+
361
+ def post_processor
362
+ run_options.post_processor
363
+ end
364
+
365
+
366
+ # Convenience Method(s)
367
+
368
+ # Easier than remembering and typing Date.iso8601.
369
+ def td(date_string)
370
+ Date.iso8601(date_string)
371
+ end
372
+
373
+
374
+ def call
375
+ begin
376
+ # By this time, the Main class has removed the command line options, and all that is left
377
+ # in ARGV is the commands and their options.
378
+ if @interactive_mode
379
+ run_shell
380
+ else
381
+ process_command_line
382
+ end
383
+
384
+ rescue BadCommandError => error
385
+ separator_line = "! #{'-' * 75} !\n"
386
+ puts '' << separator_line << error.to_s << "\n" << separator_line
387
+ exit(-1)
388
+ end
389
+ end
390
+ end
391
+ end