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
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
data/Rakefile
ADDED
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
data/exe/rock_books
ADDED
@@ -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
|