rvgp 0.3.2

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 (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +23 -0
  4. data/LICENSE +504 -0
  5. data/README.md +223 -0
  6. data/Rakefile +32 -0
  7. data/bin/rvgp +8 -0
  8. data/lib/rvgp/application/config.rb +159 -0
  9. data/lib/rvgp/application/descendant_registry.rb +122 -0
  10. data/lib/rvgp/application/status_output.rb +139 -0
  11. data/lib/rvgp/application.rb +170 -0
  12. data/lib/rvgp/base/command.rb +457 -0
  13. data/lib/rvgp/base/grid.rb +531 -0
  14. data/lib/rvgp/base/reader.rb +29 -0
  15. data/lib/rvgp/base/reconciler.rb +434 -0
  16. data/lib/rvgp/base/validation.rb +261 -0
  17. data/lib/rvgp/commands/cashflow.rb +160 -0
  18. data/lib/rvgp/commands/grid.rb +70 -0
  19. data/lib/rvgp/commands/ireconcile.rb +95 -0
  20. data/lib/rvgp/commands/new_project.rb +296 -0
  21. data/lib/rvgp/commands/plot.rb +41 -0
  22. data/lib/rvgp/commands/publish_gsheets.rb +83 -0
  23. data/lib/rvgp/commands/reconcile.rb +58 -0
  24. data/lib/rvgp/commands/rotate_year.rb +202 -0
  25. data/lib/rvgp/commands/validate_journal.rb +59 -0
  26. data/lib/rvgp/commands/validate_system.rb +44 -0
  27. data/lib/rvgp/commands.rb +160 -0
  28. data/lib/rvgp/dashboard.rb +252 -0
  29. data/lib/rvgp/fakers/fake_feed.rb +245 -0
  30. data/lib/rvgp/fakers/fake_journal.rb +57 -0
  31. data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
  32. data/lib/rvgp/fakers/faker_helpers.rb +25 -0
  33. data/lib/rvgp/gem.rb +80 -0
  34. data/lib/rvgp/journal/commodity.rb +453 -0
  35. data/lib/rvgp/journal/complex_commodity.rb +214 -0
  36. data/lib/rvgp/journal/currency.rb +101 -0
  37. data/lib/rvgp/journal/journal.rb +141 -0
  38. data/lib/rvgp/journal/posting.rb +156 -0
  39. data/lib/rvgp/journal/pricer.rb +267 -0
  40. data/lib/rvgp/journal.rb +24 -0
  41. data/lib/rvgp/plot/gnuplot.rb +478 -0
  42. data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
  43. data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
  44. data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
  45. data/lib/rvgp/plot.rb +293 -0
  46. data/lib/rvgp/pta/hledger.rb +237 -0
  47. data/lib/rvgp/pta/ledger.rb +308 -0
  48. data/lib/rvgp/pta.rb +311 -0
  49. data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
  50. data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
  51. data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
  52. data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
  53. data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
  54. data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
  55. data/lib/rvgp/utilities/grid_query.rb +190 -0
  56. data/lib/rvgp/utilities/yaml.rb +131 -0
  57. data/lib/rvgp/utilities.rb +44 -0
  58. data/lib/rvgp/validations/balance_validation.rb +68 -0
  59. data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
  60. data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
  61. data/lib/rvgp.rb +66 -0
  62. data/resources/README.MD/2022-cashflow-google.png +0 -0
  63. data/resources/README.MD/2022-cashflow.png +0 -0
  64. data/resources/README.MD/all-wealth-growth-google.png +0 -0
  65. data/resources/README.MD/all-wealth-growth.png +0 -0
  66. data/resources/gnuplot/default.yml +80 -0
  67. data/resources/i18n/en.yml +192 -0
  68. data/resources/iso-4217-currencies.json +171 -0
  69. data/resources/skel/Rakefile +5 -0
  70. data/resources/skel/app/grids/cashflow_grid.rb +27 -0
  71. data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
  72. data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
  73. data/resources/skel/app/plots/cashflow.yml +33 -0
  74. data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
  75. data/resources/skel/app/plots/wealth-growth.yml +20 -0
  76. data/resources/skel/config/csv-format-acme-checking.yml +9 -0
  77. data/resources/skel/config/google-secrets.yml +5 -0
  78. data/resources/skel/config/rvgp.yml +0 -0
  79. data/resources/skel/journals/prices.db +0 -0
  80. data/rvgp.gemspec +6 -0
  81. data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
  82. data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
  83. data/test/test_command_base.rb +61 -0
  84. data/test/test_commodity.rb +270 -0
  85. data/test/test_csv_reconciler.rb +60 -0
  86. data/test/test_currency.rb +24 -0
  87. data/test/test_fake_feed.rb +228 -0
  88. data/test/test_fake_journal.rb +98 -0
  89. data/test/test_fake_reconciler.rb +60 -0
  90. data/test/test_journal_parse.rb +545 -0
  91. data/test/test_ledger.rb +102 -0
  92. data/test/test_plot.rb +133 -0
  93. data/test/test_posting.rb +50 -0
  94. data/test/test_pricer.rb +139 -0
  95. data/test/test_pta_adapter.rb +575 -0
  96. data/test/test_utilities.rb +45 -0
  97. metadata +268 -0
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../application/descendant_registry'
4
+
5
+ module RVGP
6
+ module Base
7
+ # This class contains methods shared by both {RVGP::Base::JournalValidation} and {RVGP::Base::SystemValidation}.
8
+ # Validations are run during a project build, after the reconcile tasks.
9
+ #
10
+ # Validations are typically defined inside a .rb in your project's app/validations folder, and should inherit from
11
+ # JournalValidation or SystemValidation (though not this class itself). Your validations can be customized
12
+ # in ruby, to add warnings or errors to your build. Warnings are non-fatal, and merely output a notice on the
13
+ # command line. Errors are fatal, and halt a build.
14
+ #
15
+ # This Base class contains some common helpers for use in your validations, regardless of whether its a system or
16
+ # journal validation. Here are the differences between these two validation classes:
17
+ #
18
+ # ##Journal Validations
19
+ # Validate the output of one reconciler at a time<br>
20
+ #
21
+ # These validations are run immediately after the reconcile task, and before system validations
22
+ # are run. Each instance of these validations is applied to a reconcilers output file (typically located in
23
+ # build/journal). And by default, any journal validations that are defined in a project's app/validations are
24
+ # instantiated against every reconciler's output, in the project. This behavior can be overwritten, by defining a
25
+ # 'disable_checks' array in the root of the reconciler's yaml, containing the name(s) of validations to disable
26
+ # for that journal. These names are expected to be the classname of the validation, underscorized, lowercase, and
27
+ # with the 'Validation' suffix removed from the class. For example, to disable the
28
+ # {RVGP::Validations::BalanceValidation} in one of the reconcilers of your project, add the following lines to its
29
+ # yaml:
30
+ # ```
31
+ # disable_checks:
32
+ # - balance
33
+ # ```
34
+ # A JournalValidation is passed the reconciler corresponding to it's instance in its initialize method. For further
35
+ # details on how these validations work, see the documentation for this class here {RVGP::Base::JournalValidation}
36
+ # or check out an example implementation. Here's the BalanceValidation itself, which is a relatively easy example to
37
+ # {https://github.com/brighton36/rvgp/blob/main/lib/rvgp/validations/balance_validation.rb balance_validation.rb}
38
+ # follow.
39
+ #
40
+ # ##System Validations
41
+ # Validate the entire, finished, journal output for the project <br>
42
+ #
43
+ # Unlike Journal validations, these Validations are run without a target, and are expected to generate warnings and
44
+ # errors based on the state of queries spanning multiple journals.
45
+ #
46
+ # There are no example SystemValidations included in the distribution of rvgp. However, here's an easy one, to serve
47
+ # as reference. This validation ensures Transfers between accounts are always credited and
48
+ # debited on both sides:
49
+ # ```
50
+ # class TransferAccountValidation < RVGP::Base::SystemValidation
51
+ # STATUS_LABEL = 'Unbalanced inter-account transfers'
52
+ # DESCRIPTION = "Ensure that debits and credits through Transfer accounts, complete without remainders"
53
+ #
54
+ # def validate
55
+ # warnings = pta.balance('Transfers').accounts.map do |account|
56
+ # account.amounts.map do |amount|
57
+ # [ account.fullname, RVGP.pastel.yellow('━'), amount.to_s(commatize: true) ].join(' ')
58
+ # end
59
+ # end.compact.flatten
60
+ #
61
+ # warning! 'Unbalanced Transfer Encountered', warnings if warnings.length > 0
62
+ # end
63
+ # end
64
+ # ```
65
+ #
66
+ # The above validation works, if you assign transfers between accounts like so:
67
+ # ```
68
+ # ; This is how a Credit Card Payment looks in my Checking account, the source of funds:
69
+ # 2023-01-25 Payment to American Express card ending in 1234
70
+ # Transfers:PersonalChecking_PersonalAmex $ 10000.00
71
+ # Personal:Assets:AcmeBank:Checking
72
+ #
73
+ # ; This is how a Credit Card Payment looks in my Amex account, the destination of funds:
74
+ # 2023-01-25 Payment Thank You - Web
75
+ # Transfers:PersonalChecking_PersonalAmex $ -10000.00
76
+ # Personal:Liabilities:AmericanExpress
77
+ # ```
78
+ #
79
+ # In this format of transfering money, if either the first or second transfer was omitted, the
80
+ # TransferAccountValidation will alert you that money has gone missing somewhere at the bank, and/or is taking
81
+ # longer to complete, than you expected.
82
+ #
83
+ # ##Summary of Differences
84
+ # SystemValidations are largely identical to JournalValidations, with, the following exceptions:
85
+ #
86
+ # **Priority**
87
+ # Journal validations are run sooner in the rake process. Just after reconciliations have completed. System
88
+ # validations run immediately after all Journal validations have completed.
89
+ #
90
+ # **Input**
91
+ # Journal validations have one input, accessible via its {RVGP::Base::JournalValidation#reconciler}. System
92
+ # validations have no preconfigured inputs at all. Journal Validations support a disable_checks attribute in the
93
+ # reconciler yaml, and system validations have no such directive.
94
+ #
95
+ # **Labeling**
96
+ # With Journal validations, tasks are labeled automatically by rvgp, based on their class name. System validations
97
+ # are expected to define a STATUS_LABEL and DESCRIPTION constant, in order to arrive at these labels.
98
+ #
99
+ # Note that for either type of validation, most/all of the integration functionality is provided by way of the
100
+ # {RVGP::Base::Validation#error!} and {RVGP::Base::Validation#warning!} methods, instigated in the class'
101
+ # validate method.
102
+ #
103
+ # ##Error and Warning formatting
104
+ # The format of errors and warnings are a bit peculiar, and probably need a bit more polish to the interface.
105
+ # Nonetheless, it's not complicated. Here's the way it works, for both collections:
106
+ # - The formatting of :errors and :warnings is identical. These collections contain a hierarchy of errors, which
107
+ # is used to display fatal and non-fatal output to the console.
108
+ # - Every element of these collections are expected to be a two element Array. The first element of which is to
109
+ # be a string containing the topmost error/warning. The second element of this Array is optional. If present,
110
+ # this second element is expected to be an Array of Strings, which are subordinate to the message in the first
111
+ # element.
112
+ #
113
+ # @attr_reader [Array<String,Array<String>>] errors Errors encountered by this validation. See the above note on
114
+ # 'Error and Warning formatting'
115
+ # @attr_reader [Array<String,Array<String>>] warnings Warnings encountered by this validation. See the above note on
116
+ # 'Error and Warning formatting'
117
+ class Validation
118
+ include RVGP::Pta::AvailabilityHelper
119
+
120
+ # @!visibility private
121
+ NAME_CAPTURE = /([^:]+)Validation\Z/.freeze
122
+
123
+ attr_reader :errors, :warnings
124
+
125
+ # Create a new Validation
126
+ def initialize
127
+ @errors = []
128
+ @warnings = []
129
+ end
130
+
131
+ # Returns true if there are no warnings or errors present in this validation's instance. Otherwise, returns false.
132
+ # @return [TrueClass, FalseClass] whether this validation has passed
133
+ def valid?
134
+ @errors = []
135
+ @warnings = []
136
+ validate
137
+ (@errors.length + @warnings.length).zero?
138
+ end
139
+
140
+ private
141
+
142
+ def format_error_or_warning(msg, citations = nil)
143
+ [msg, citations || []]
144
+ end
145
+
146
+ # @!visibility public
147
+ # Add an error to our {RVGP::Base::Validation#errors} collection. The format of this error is expected to match
148
+ # the formatting indicated in the 'Error and Warning formatting' above.
149
+ # @param msg [String] A description of the error.
150
+ # @param citations [Array<String>] Supporting details, subordinate error citations, denotated 'below' the :msg
151
+ def error!(msg, citations = nil)
152
+ @errors << format_error_or_warning(msg, citations)
153
+ end
154
+
155
+ # @!visibility public
156
+ # Add a warning to our {RVGP::Base::Validation#warnings} collection. The format of this warning is expected to
157
+ # match the formatting indicated in the 'Error and Warning formatting' above.
158
+ # @param msg [String] A description of the warning.
159
+ # @param citations [Array<String>] Supporting details, subordinate warning citations, denotated 'below' the :msg
160
+ def warning!(msg, citations = nil)
161
+ @warnings << format_error_or_warning(msg, citations)
162
+ end
163
+ end
164
+
165
+ # A base class, from which your journal validations should inherit. For more information on validations, and your
166
+ # options, see the documentation notes on {RVGP::Base::JournalValidation}.
167
+ # @attr_reader [RVGP::Reconcilers::CsvReconciler,RVGP::Reconcilers::JournalReconciler] reconciler
168
+ # The reconciler whose output will be inspected by this journal validation instance.
169
+ class JournalValidation < Validation
170
+ include RVGP::Application::DescendantRegistry
171
+
172
+ register_descendants RVGP, :journal_validations, name_capture: NAME_CAPTURE
173
+
174
+ attr_reader :reconciler
175
+
176
+ # Create a new Journal Validation
177
+ # @param [RVGP::Reconcilers::CsvReconciler,RVGP::Reconcilers::JournalReconciler] reconciler
178
+ # see {RVGP::Base::JournalValidation#reconciler}
179
+ def initialize(reconciler)
180
+ super()
181
+ @reconciler = reconciler
182
+ end
183
+
184
+ # This helper method will supply the provided arguments to pta.register. And if there are any transactions
185
+ # returned, the supplied error message will be added to our :errors colection, citing the transactions
186
+ # that were encountered.
187
+ # @param [String] with_error_msg A description of the error that corresponds to the returned transactions.
188
+ # @param [Array<Object>] args These arguments are supplied directly to {RVGP::Pta::AvailabilityHelper#pta}'s
189
+ # #register method
190
+ def validate_no_transactions(with_error_msg, *args)
191
+ ledger_opts = args.last.is_a?(Hash) ? args.pop : {}
192
+
193
+ results = pta.register(*args, { file: reconciler.output_file }.merge(ledger_opts))
194
+
195
+ transactions = block_given? ? yield(results.transactions) : results.transactions
196
+
197
+ error_citations = transactions.map do |posting|
198
+ format '%<date>s: %<payee>s', date: posting.date.to_s, payee: posting.payee
199
+ end
200
+
201
+ error! with_error_msg, error_citations unless error_citations.empty?
202
+ end
203
+
204
+ # This helper method will supply the provided account to pta.balance. And if there is a balance returned,
205
+ # the supplied error message will be added to our :errors colection, citing the balance that was encountered.
206
+ # @param [String] with_error_msg A description of the error that corresponds to the returned balances.
207
+ # @param [Array<String>] account This arguments is supplied directly to {RVGP::Pta::AvailabilityHelper#pta}'s
208
+ # #balance method
209
+ def validate_no_balance(with_error_msg, account)
210
+ results = pta.balance account, file: reconciler.output_file
211
+
212
+ error_citations = results.accounts.map do |ra|
213
+ ra.amounts.map { |commodity| [ra.fullname, RVGP.pastel.red('━'), commodity.to_s].join(' ') }
214
+ end
215
+
216
+ error_citations.flatten!
217
+
218
+ error! with_error_msg, error_citations unless error_citations.empty?
219
+ end
220
+ end
221
+
222
+ # A base class, from which your system validations should inherit. For more information on validations, and your
223
+ # options, see the documentation notes on {RVGP::Base::JournalValidation}.
224
+ class SystemValidation < Validation
225
+ include RVGP::Application::DescendantRegistry
226
+
227
+ task_names = ->(registry) { registry.names.map { |name| format('validate_system:%s', name) } }
228
+ register_descendants RVGP, :system_validations,
229
+ name_capture: NAME_CAPTURE,
230
+ accessors: { task_names: task_names }
231
+
232
+ # @!visibility private
233
+ def mark_validated!
234
+ FileUtils.touch self.class.build_validation_file_path
235
+ end
236
+
237
+ # @!visibility private
238
+ def self.validated?
239
+ FileUtils.uptodate? build_validation_file_path, [
240
+ RVGP.app.config.build_path('journals/*.journal'),
241
+ RVGP.app.config.project_path('journals/*.journal')
242
+ ].map { |glob| Dir.glob glob }.flatten
243
+ end
244
+
245
+ # @!visibility private
246
+ def self.status_label
247
+ const_get :STATUS_LABEL
248
+ end
249
+
250
+ # @!visibility private
251
+ def self.description
252
+ const_get :DESCRIPTION
253
+ end
254
+
255
+ # @!visibility private
256
+ def self.build_validation_file_path
257
+ RVGP.app.config.build_path(format('journals/system-validation-%s.valid', name.to_s))
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../utilities/grid_query'
4
+ require_relative '../dashboard'
5
+
6
+ module RVGP
7
+ module Commands
8
+ # @!visibility private
9
+ # This class contains the logic necessary to display the 'cashflow' dashboard
10
+ class Cashflow < RVGP::Base::Command
11
+ accepts_options OPTION_ALL, OPTION_LIST, [:date, :d, { has_value: 'DATE' }]
12
+
13
+ # @!visibility private
14
+ # This class handles the target argument, passed on the cli
15
+ class Target < RVGP::Base::Command::Target
16
+ # @!visibility private
17
+ def self.all
18
+ RVGP::Commands::Cashflow.grids_by_targetname.keys.map { |s| new s }
19
+ end
20
+ end
21
+
22
+ # @!visibility private
23
+ def initialize(*args)
24
+ super(*args)
25
+
26
+ options[:date] = Date.strptime options[:date] if options.key? :date
27
+
28
+ minimum_width = RVGP::Dashboard.table_width_given_column_widths(column_widths[0..1])
29
+
30
+ unless TTY::Screen.width > minimum_width
31
+ @errors << I18n.t(
32
+ 'commands.cashflow.errors.screen_too_small',
33
+ screen_width: TTY::Screen.width,
34
+ minimum_width: minimum_width
35
+ )
36
+ end
37
+ end
38
+
39
+ # @!visibility private
40
+ def execute!
41
+ puts dashboards.map { |dashboard|
42
+ dashboard.to_s(
43
+ column_widths: column_widths[0...show_columns],
44
+ rows_ordered_by: lambda { |row|
45
+ series = row[0]
46
+ data = row[1..]
47
+ # Sort by the series type [Expenses/Income/etc], then by 'consistency',
48
+ # then by total amount
49
+ [series.split(':')[1], data.count(&:nil?), data.compact.sum * -1]
50
+ },
51
+ # Hide rows without any data
52
+ show_row: ->(row) { !row[1..].all?(&:nil?) }
53
+ )
54
+ }.join("\n\n")
55
+ end
56
+
57
+ # @!visibility private
58
+ def self.cashflow_grid_files
59
+ Dir.glob RVGP.app.config.build_path('grids/*-cashflow-*.csv')
60
+ end
61
+
62
+ # @!visibility private
63
+ def self.grids_by_targetname
64
+ @grids_by_targetname ||= cashflow_grid_files.each_with_object({}) do |file, sum|
65
+ unless /([^-]+)\.csv\Z/.match file
66
+ raise StandardError, I18n.t('commands.cashflow.errors.unrecognized_path', file: file)
67
+ end
68
+
69
+ tablename = ::Regexp.last_match(1).capitalize
70
+
71
+ sum[tablename] ||= []
72
+ sum[tablename] << file
73
+
74
+ sum
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def dashboards
81
+ @dashboards ||= targets.map do |target|
82
+ RVGP::Dashboard.new(
83
+ target.name,
84
+ RVGP::Utilities::GridQuery.new(
85
+ self.class.grids_by_targetname[target.name],
86
+ store_cell: lambda { |cell|
87
+ cell ? RVGP::Journal::Commodity.from_symbol_and_amount('$', cell) : cell
88
+ },
89
+ select_columns: lambda { |col, column|
90
+ if options.key?(:date)
91
+ Date.strptime(col, '%m-%y') <= options[:date]
92
+ else
93
+ column.any? { |cell| !cell.nil? }
94
+ end
95
+ }
96
+ ),
97
+ { pastel: RVGP.pastel,
98
+ series_column_label: I18n.t('commands.cashflow.account'),
99
+ format_data_cell: ->(cell) { cell&.to_s commatize: true, precision: 2 },
100
+ columns_ordered_by: ->(a, b) { [b, a].map { |d| Date.strptime d, '%m-%y' }.reduce :<=> },
101
+ summaries: [
102
+ {
103
+ label: I18n.t('commands.cashflow.expenses'),
104
+ prettify: ->(row) { [RVGP.pastel.bold(row[0])] + row[1..].map { |s| RVGP.pastel.red(s) } },
105
+ contents: ->(series, data) { sum_column 'Expenses', series, data }
106
+ },
107
+ {
108
+ label: I18n.t('commands.cashflow.income'),
109
+ prettify: ->(row) { [RVGP.pastel.bold(row[0])] + row[1..].map { |s| RVGP.pastel.green(s) } },
110
+ contents: ->(series, data) { sum_column 'Income', series, data }
111
+ },
112
+ {
113
+ label: I18n.t('commands.cashflow.cash_flow'),
114
+ prettify: lambda { |row|
115
+ [RVGP.pastel.bold(row[0])] + row[1..].map do |cell|
116
+ /\$ *-/.match(cell) ? RVGP.pastel.red(cell) : RVGP.pastel.green(cell)
117
+ end
118
+ },
119
+ contents: lambda { |series, data|
120
+ %w[Expenses Income].map { |s| sum_column s, series, data }.sum.invert!
121
+ }
122
+ }
123
+ ] }
124
+ )
125
+ end
126
+ end
127
+
128
+ def column_widths
129
+ # We want every table being displayed, to have the same column widths.
130
+ # probably we can move most of this code into a Dashboard class method. But, no
131
+ # rush on that.
132
+ @column_widths ||= dashboards.map(&:column_data_widths)
133
+ .inject([]) do |sum, widths|
134
+ widths.map.with_index { |w, i| sum[i].nil? || sum[i] < w ? w : sum[i] }
135
+ end
136
+ end
137
+
138
+ def show_columns
139
+ return @show_columns if @show_columns
140
+
141
+ # Now let's calculate how many columns fit on screen:
142
+ @show_columns = 0
143
+ 0.upto(column_widths.length - 1) do |i|
144
+ break if RVGP::Dashboard.table_width_given_column_widths(column_widths[0..i]) > TTY::Screen.width
145
+
146
+ @show_columns += 1
147
+ end
148
+ @show_columns
149
+ end
150
+
151
+ def sum_column(for_series, series, data)
152
+ # NOTE: This for_series determination is a bit 'magic' and specific to our
153
+ # current accounting categorization taxonomy
154
+ 0.upto(series.length - 1).map do |i|
155
+ series[i].split(':')[1] == for_series && data[i] ? data[i] : '$0.00'.to_commodity
156
+ end.compact.sum
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RVGP
4
+ module Commands
5
+ # @!visibility private
6
+ # This class contains the handling of the 'grid' command and task. This
7
+ # code provides the list of grids that are available in the application, and
8
+ # dispatches requests to build these grids.
9
+ class Grid < RVGP::Base::Command
10
+ accepts_options OPTION_ALL, OPTION_LIST
11
+
12
+ include RakeTask
13
+ rake_tasks :grid
14
+
15
+ # @!visibility private
16
+ def execute!(&block)
17
+ RVGP.app.ensure_build_dir! 'grids'
18
+ super(&block)
19
+ end
20
+
21
+ # @!visibility private
22
+ # This class represents a grid, available for building. In addition, the #.all
23
+ # method, returns the list of available targets.
24
+ class Target < RVGP::Base::Command::Target
25
+ # @!visibility private
26
+ def initialize(grid_klass, starting_at, ending_at)
27
+ @starting_at = starting_at
28
+ @ending_at = ending_at
29
+ @grid_klass = grid_klass
30
+ super [year, grid_klass.name.tr('_', '-')].join('-'), grid_klass.status_name(year)
31
+ end
32
+
33
+ # @!visibility private
34
+ def description
35
+ I18n.t 'commands.grid.target_description', description: @grid_klass.description, year: year
36
+ end
37
+
38
+ # @!visibility private
39
+ def uptodate?
40
+ @grid_klass.uptodate? year
41
+ end
42
+
43
+ # @!visibility private
44
+ def execute(_options)
45
+ @grid_klass.new(@starting_at, @ending_at).to_file!
46
+ end
47
+
48
+ # @!visibility private
49
+ def self.all
50
+ starting_at = RVGP.app.config.grid_starting_at
51
+ ending_at = RVGP.app.config.grid_ending_at
52
+
53
+ starting_at.year.upto(ending_at.year).map do |y|
54
+ RVGP.grids.classes.map do |klass|
55
+ new klass,
56
+ y == starting_at.year ? starting_at : Date.new(y, 1, 1),
57
+ y == ending_at.year ? ending_at : Date.new(y, 12, 31)
58
+ end
59
+ end.flatten
60
+ end
61
+
62
+ private
63
+
64
+ def year
65
+ @starting_at.year
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module RVGP
6
+ module Commands
7
+ # @!visibility private
8
+ # This class contains the handling of the 'ireconcile' command. Note that
9
+ # there is no rake integration in this command, as that function is irrelevent
10
+ # to the notion of an 'export'.
11
+ class Ireconcile < RVGP::Base::Command
12
+ accepts_options OPTION_ALL, OPTION_LIST, %i[vsplit v]
13
+
14
+ # There's a bug here where we scroll to the top of the file sometimes, on
15
+ # reload. Not sure what to do about that...
16
+ # @!visibility private
17
+ VIMSCRIPT_HEADER = <<-VIMSCRIPT
18
+ let $LANG='en_US.utf-8'
19
+
20
+ function ReloadIfChanged(timer)
21
+ checktime
22
+ endfunction
23
+
24
+ function ExecuteReconcile()
25
+ let reconcile_path = expand("%%")
26
+ let output_path = tempname()
27
+ let pager = '/bin/less'
28
+ if len($PAGER) > 0
29
+ pager = $PAGER
30
+ endif
31
+
32
+ execute('!%<rvgp_path>s reconcile --concise ' .
33
+ \\ shellescape(reconcile_path, 1) .
34
+ \\ ' 2>' . shellescape(output_path, 1) .
35
+ \\ ' > ' . shellescape(output_path, 1))
36
+ if v:shell_error
37
+ echoerr "The following error(s) occurred during reconciliation:"
38
+ execute '!' . pager . ' -r ' . shellescape(output_path, 1)
39
+ redraw!
40
+ endif
41
+ silent execute('!rm '. shellescape(output_path,1))
42
+ endfunction
43
+ VIMSCRIPT
44
+
45
+ # @!visibility private
46
+ def initialize(*args)
47
+ super(*args)
48
+
49
+ unless /vim?\Z/.match ENV.fetch('EDITOR')
50
+ @errors << I18n.t('commands.ireconcile.errors.unsupported_editor', editor: ENV['EDITOR'].inspect)
51
+ end
52
+ end
53
+
54
+ # @!visibility private
55
+ def execute!
56
+ Tempfile.create 'ireconcile.vim' do |file|
57
+ file.write [format(VIMSCRIPT_HEADER, rvgp_path: $PROGRAM_NAME),
58
+ targets.map { |target| target.to_vimscript options[:vsplit] }.join("\ntabnew\n")].join
59
+
60
+ file.close
61
+
62
+ system [ENV.fetch('EDITOR'), '-S', file.path].join(' ')
63
+ end
64
+ end
65
+
66
+ # @!visibility private
67
+ # This class represents a reconciler. See RVGP::Base::Command::ReconcilerTarget, for
68
+ # most of the logic that this class inherits. Typically, these targets take the form
69
+ # of "#\\{year}-#\\{reconciler_name}"
70
+ class Target < RVGP::Base::Command::ReconcilerTarget
71
+ # @!visibility private
72
+ VIMSCRIPT_TEMPLATE = <<-VIMSCRIPT
73
+ edit %<output_file>s
74
+ setl autoread
75
+ autocmd VimEnter * let timer=timer_start(1000,'ReloadIfChanged', {'repeat': -1} )
76
+ call feedkeys("lh")
77
+ setl nomodifiable
78
+ %<split>s
79
+ edit %<input_file>s
80
+ autocmd BufWritePost * silent call ExecuteReconcile()
81
+ VIMSCRIPT
82
+
83
+ # @!visibility private
84
+ def to_vimscript(is_vsplit)
85
+ # NOTE: I guess we don't need to escape these paths, so long as there arent
86
+ # any \n's in the path name... I guess
87
+ format(VIMSCRIPT_TEMPLATE,
88
+ output_file: @reconciler.output_file,
89
+ input_file: @reconciler.file,
90
+ split: is_vsplit ? 'vsplit' : 'split')
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end