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