rvgp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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