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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- metadata +268 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'faker_helpers'
|
|
4
|
+
require_relative '../journal/currency'
|
|
5
|
+
|
|
6
|
+
module RVGP
|
|
7
|
+
module Fakers
|
|
8
|
+
# Contains faker implementations that produce CSV files, for use as a data feed
|
|
9
|
+
class FakeFeed < Faker::Base
|
|
10
|
+
class << self
|
|
11
|
+
include FakerHelpers
|
|
12
|
+
|
|
13
|
+
# This error is thrown when there is a mismatch between two parameter arrays, passed to
|
|
14
|
+
# a function, whose lengths are required to match.
|
|
15
|
+
class ParameterLengthError < StandardError
|
|
16
|
+
# @!visibility private
|
|
17
|
+
MSG_FORMAT = 'Expected %<expected>s elements in %<parameter>s, but found %<found>s'
|
|
18
|
+
|
|
19
|
+
def initialize(parameter, expected, found)
|
|
20
|
+
super format(MSG_FORMAT, expected: expected, parameter: parameter, found: found)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @!visibility private
|
|
25
|
+
DEFAULT_LENGTH_IN_DAYS = 365 / 4
|
|
26
|
+
# @!visibility private
|
|
27
|
+
DEFAULT_POST_COUNT = 300
|
|
28
|
+
# @!visibility private
|
|
29
|
+
FEED_COLUMNS = ['Date', 'Type', 'Description', 'Withdrawal (-)', 'Deposit (+)', 'RunningBalance'].freeze
|
|
30
|
+
# @!visibility private
|
|
31
|
+
DEFAULT_CURRENCY = RVGP::Journal::Currency.from_code_or_symbol('$')
|
|
32
|
+
|
|
33
|
+
# Generates a basic csv feed string, that resembles thos
|
|
34
|
+
|
|
35
|
+
# Generates a basic csv feed string, that resembles those offered by banking institutions
|
|
36
|
+
#
|
|
37
|
+
# @param from [Date] The date to start generated feed from
|
|
38
|
+
# @param to [Date] The date to end generated feed
|
|
39
|
+
# @param income_descriptions [Array] Strings containing the pool of available income descriptions, for use in
|
|
40
|
+
# random selection
|
|
41
|
+
# @param expense_descriptions [Array] Strings containing the pool of available expense descriptions, for use in
|
|
42
|
+
# random selection
|
|
43
|
+
# @param deposit_average [RVGP::Journal::Commodity] The average deposit amount
|
|
44
|
+
# @param deposit_stddev [Float] The stand deviation, on random deposits
|
|
45
|
+
# @param withdrawal_average [RVGP::Journal::Commodity] The average withdrawal amount
|
|
46
|
+
# @param withdrawal_stddev [Float] The stand deviation, on random withdrawals
|
|
47
|
+
# @param starting_balance [RVGP::Journal::Commodity]
|
|
48
|
+
# The balance of the account, before generating the transactions in the feed
|
|
49
|
+
# @param post_count [Numeric] The number of transactions to generate, in this csv feed
|
|
50
|
+
# @param deposit_ratio [Float] The odds ratio, for a given transaction, to be a deposit
|
|
51
|
+
# @param entries [Array] An array of Array's, that are appended to the generated entries (aka 'lines')
|
|
52
|
+
# @return [String] A CSV, containing the generated transactions
|
|
53
|
+
def basic_checking(from: ::Date.today,
|
|
54
|
+
to: from + DEFAULT_LENGTH_IN_DAYS,
|
|
55
|
+
expense_descriptions: nil,
|
|
56
|
+
income_descriptions: nil,
|
|
57
|
+
deposit_average: RVGP::Journal::Commodity.from_symbol_and_amount('$', 6000),
|
|
58
|
+
deposit_stddev: 500.0,
|
|
59
|
+
withdrawal_average: RVGP::Journal::Commodity.from_symbol_and_amount('$', 300),
|
|
60
|
+
withdrawal_stddev: 24.0,
|
|
61
|
+
post_count: DEFAULT_POST_COUNT,
|
|
62
|
+
starting_balance: RVGP::Journal::Commodity.from_symbol_and_amount('$', 5000),
|
|
63
|
+
deposit_ratio: 0.05,
|
|
64
|
+
entries: [])
|
|
65
|
+
|
|
66
|
+
running_balance = starting_balance.dup
|
|
67
|
+
|
|
68
|
+
entry_to_row = lambda do |entry|
|
|
69
|
+
FEED_COLUMNS.map do |column|
|
|
70
|
+
if column == 'RunningBalance'
|
|
71
|
+
deposit = entry['Deposit (+)']
|
|
72
|
+
withdrawal = entry['Withdrawal (-)']
|
|
73
|
+
|
|
74
|
+
running_balance = withdrawal.nil? ? running_balance + deposit : running_balance - withdrawal
|
|
75
|
+
else
|
|
76
|
+
entry[column]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Newest to oldest:
|
|
82
|
+
to_csv do |csv|
|
|
83
|
+
dates = entries_over_date_range from, to, post_count
|
|
84
|
+
|
|
85
|
+
dates.each_with_index do |date, i|
|
|
86
|
+
# If there are any :entries to insert, in this date, do that now:
|
|
87
|
+
entries.each do |entry|
|
|
88
|
+
csv << entry_to_row.call(entry) if entry['Date'] <= date && (i.zero? || entry['Date'] > dates[i - 1])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
accumulator, mean, stddev, type, description = *(
|
|
92
|
+
if Faker::Boolean.boolean true_ratio: deposit_ratio
|
|
93
|
+
[:+, deposit_average.to_f, deposit_stddev, 'ACH',
|
|
94
|
+
format('%s DIRECT DEP',
|
|
95
|
+
income_descriptions ? income_descriptions.sample : Faker::Company.name.upcase)]
|
|
96
|
+
else
|
|
97
|
+
[:-, withdrawal_average.to_f, withdrawal_stddev, 'VISA',
|
|
98
|
+
expense_descriptions ? expense_descriptions.sample : Faker::Company.name.upcase]
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
amount = RVGP::Journal::Commodity.from_symbol_and_amount(
|
|
102
|
+
DEFAULT_CURRENCY.symbol,
|
|
103
|
+
Faker::Number.normal(mean: mean, standard_deviation: stddev)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
running_balance = running_balance.send accumulator, amount
|
|
107
|
+
|
|
108
|
+
amounts = [nil, amount]
|
|
109
|
+
csv << ([date, type, description] + (accumulator == :- ? amounts.reverse : amounts) + [running_balance])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Are there any more entries? If so, sort 'em and push them:
|
|
113
|
+
entries.each { |entry| csv << entry_to_row.call(entry) if entry['Date'] > dates.last }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Generates a basic csv feed string, that resembles those offered by banking institutions. Unlike
|
|
118
|
+
# #basic_checking, this faker supports a set of parameters that will better conform the output to a
|
|
119
|
+
# typical model of commerence for an employee with a paycheck and living expenses. As such, the
|
|
120
|
+
# parameters are a bit different, and suited to plotting aesthetics.
|
|
121
|
+
#
|
|
122
|
+
# @param from [Date] The date to start generated feed from
|
|
123
|
+
# @param to [Date] The date to end generated feed
|
|
124
|
+
# @param income_sources [Array] Strings containing the pool of income companies, to use for growing our assets
|
|
125
|
+
# @param expense_sources [Array] Strings containing the pool of available expense companies, to use for
|
|
126
|
+
# shrinking our assets
|
|
127
|
+
# @param opening_liability_balance [RVGP::Journal::Commodity] The opening balance of the liability account,
|
|
128
|
+
# preceeding month zero
|
|
129
|
+
# @param opening_asset_balance [RVGP::Journal::Commodity] The opening balance of the asset account, preceeding
|
|
130
|
+
# month zero
|
|
131
|
+
# @param liability_sources [Array] Strings containing the pool of available liability sources (aka 'companies')
|
|
132
|
+
# @param liabilities_by_month [Array] An array of RVGP::Journal::Commodity entries, indicatiing the liability
|
|
133
|
+
# balance for a month with offset n, from the from date
|
|
134
|
+
# @param assets_by_month [Array] An array of RVGP::Journal::Commodity entries, indicating the asset
|
|
135
|
+
# balance for a month with offset n, from the from date
|
|
136
|
+
# @return [String] A CSV, containing the generated transactions
|
|
137
|
+
def personal_checking(from: ::Date.today,
|
|
138
|
+
to: from + DEFAULT_LENGTH_IN_DAYS,
|
|
139
|
+
expense_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
|
|
140
|
+
income_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
|
|
141
|
+
monthly_expenses: {},
|
|
142
|
+
opening_liability_balance: '$ 0.00'.to_commodity,
|
|
143
|
+
opening_asset_balance: '$ 0.00'.to_commodity,
|
|
144
|
+
liability_sources: [Faker::Company.name.tr('^a-zA-Z0-9 ', '')],
|
|
145
|
+
liabilities_by_month: months_in_range(from, to).map.with_index do |_, n|
|
|
146
|
+
RVGP::Journal::Commodity.from_symbol_and_amount('$', 200 + ((n + 1) * 800))
|
|
147
|
+
end,
|
|
148
|
+
assets_by_month: months_in_range(from, to).map.with_index do |_, n|
|
|
149
|
+
RVGP::Journal::Commodity.from_symbol_and_amount('$', 500 * ((n + 1) * 5))
|
|
150
|
+
end)
|
|
151
|
+
|
|
152
|
+
num_months_in_range = ((to.year * 12) + to.month) - ((from.year * 12) + from.month) + 1
|
|
153
|
+
|
|
154
|
+
['liabilities_by_month', liabilities_by_month.length,
|
|
155
|
+
'assets_by_month', assets_by_month.length].each_slice(2) do |attr, length|
|
|
156
|
+
raise ParameterLengthError.new(attr, num_months_in_range, length) unless num_months_in_range == length
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
monthly_expenses.each_pair do |company, expenses_by_month|
|
|
160
|
+
unless num_months_in_range == expenses_by_month.length
|
|
161
|
+
attr = format('monthly_expenses: %s', company)
|
|
162
|
+
raise ParameterLengthError.new(attr, num_months_in_range, expenses_by_month.length)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
liability_balance = opening_liability_balance
|
|
167
|
+
asset_balance = opening_asset_balance
|
|
168
|
+
|
|
169
|
+
# Newest to oldest:
|
|
170
|
+
to_csv do |csv|
|
|
171
|
+
months_in_range(from, to).each_with_index do |first_of_month, i|
|
|
172
|
+
expected_liability = liabilities_by_month[i]
|
|
173
|
+
|
|
174
|
+
# Let's adjust the liability to suit
|
|
175
|
+
csv << if expected_liability > liability_balance
|
|
176
|
+
# We need to borrow some more money:
|
|
177
|
+
deposit = (expected_liability - liability_balance).abs
|
|
178
|
+
asset_balance += deposit
|
|
179
|
+
liability_balance += deposit
|
|
180
|
+
[first_of_month, 'ACH', liability_sources.sample, nil, deposit, asset_balance]
|
|
181
|
+
elsif expected_liability < liability_balance
|
|
182
|
+
# We want to pay off our balance:
|
|
183
|
+
payment = (expected_liability - liability_balance).abs
|
|
184
|
+
asset_balance -= payment
|
|
185
|
+
liability_balance -= payment
|
|
186
|
+
[first_of_month, 'ACH', liability_sources.sample, payment, nil, asset_balance]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
expected_assets = assets_by_month[i]
|
|
190
|
+
|
|
191
|
+
monthly_expenses.each_pair do |company, expenses_by_month|
|
|
192
|
+
asset_balance -= expenses_by_month[i]
|
|
193
|
+
csv << [first_of_month, 'VISA', company, expenses_by_month[i], nil, asset_balance]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Let's adjust the assets to suit
|
|
197
|
+
if expected_assets > asset_balance
|
|
198
|
+
# We need a paycheck:
|
|
199
|
+
|
|
200
|
+
deposit = expected_assets - asset_balance
|
|
201
|
+
asset_balance += deposit
|
|
202
|
+
csv << [first_of_month, 'ACH', income_sources.sample, nil, deposit, asset_balance]
|
|
203
|
+
elsif expected_assets < asset_balance
|
|
204
|
+
# We need to generate some expenses:
|
|
205
|
+
payment = asset_balance - expected_assets
|
|
206
|
+
asset_balance -= payment
|
|
207
|
+
csv << [first_of_month, 'VISA', expense_sources.sample, payment, nil, asset_balance]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def months_in_range(from, to)
|
|
216
|
+
ret = []
|
|
217
|
+
i = 0
|
|
218
|
+
loop do
|
|
219
|
+
ret << (Date.new(from.year, from.month, 1) >> i)
|
|
220
|
+
i += 1
|
|
221
|
+
break if ret.last.year == to.year && ret.last.month == to.month
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
ret
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def to_csv(&block)
|
|
228
|
+
converter = lambda do |field|
|
|
229
|
+
case field
|
|
230
|
+
when Date
|
|
231
|
+
field.strftime('%m/%d/%Y')
|
|
232
|
+
when RVGP::Journal::Commodity
|
|
233
|
+
field.to_s(precision: DEFAULT_CURRENCY.minor_unit)
|
|
234
|
+
else
|
|
235
|
+
field
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
CSV.generate force_quotes: true, headers: FEED_COLUMNS, write_headers: true, write_converters: [converter],
|
|
240
|
+
&block
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'faker_helpers'
|
|
4
|
+
|
|
5
|
+
module RVGP
|
|
6
|
+
# This module encapsulate various faker objects that we use (mostly) in our test cases.
|
|
7
|
+
# The new_project command uses some of these classes as well.
|
|
8
|
+
module Fakers
|
|
9
|
+
# Contains faker implementation(s) that produce pta journals
|
|
10
|
+
class FakeJournal < Faker::Base
|
|
11
|
+
class << self
|
|
12
|
+
include FakerHelpers
|
|
13
|
+
|
|
14
|
+
# Generates a basic journal, that credits/debits from a Cash account
|
|
15
|
+
#
|
|
16
|
+
# @param from [Date] The date to start generated postings from
|
|
17
|
+
# @param to [Date] The date to end generated postings
|
|
18
|
+
# @param sum [RVGP::Journal::Commodity]
|
|
19
|
+
# The amount that all postings in the generated journal, will add up to
|
|
20
|
+
# @param post_count [Numeric] The number of postings to generate, in this journal
|
|
21
|
+
# @param postings [Array] An array of RVGP::Journal::Posting objects, for inclusion in the output
|
|
22
|
+
# @return [RVGP::Journal] A fake journal, conforming to the provided params
|
|
23
|
+
def basic_cash(from: ::Date.today,
|
|
24
|
+
to: from + 9,
|
|
25
|
+
sum: '$ 100.00'.to_commodity,
|
|
26
|
+
post_count: 10,
|
|
27
|
+
postings: [])
|
|
28
|
+
|
|
29
|
+
raise StandardError unless sum.is_a?(RVGP::Journal::Commodity)
|
|
30
|
+
|
|
31
|
+
amount_increment = (sum / post_count).floor sum.precision
|
|
32
|
+
running_sum = nil
|
|
33
|
+
|
|
34
|
+
generated_postings = entries_over_date_range(from, to, post_count).map.with_index do |date, i|
|
|
35
|
+
post_amount = i + 1 == post_count ? (sum - running_sum) : amount_increment
|
|
36
|
+
|
|
37
|
+
running_sum = running_sum.nil? ? post_amount : (running_sum + post_amount)
|
|
38
|
+
|
|
39
|
+
simple_posting date, post_amount
|
|
40
|
+
end
|
|
41
|
+
RVGP::Journal.new((postings + generated_postings).sort_by(&:date))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def simple_posting(date, amount)
|
|
47
|
+
transfers = [to_transfer('Expense', commodity: amount), to_transfer('Cash')]
|
|
48
|
+
RVGP::Journal::Posting.new date, Faker::Company.name, transfers: transfers
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_transfer(*args)
|
|
52
|
+
RVGP::Journal::Posting::Transfer.new(*args)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'faker_helpers'
|
|
4
|
+
|
|
5
|
+
module RVGP
|
|
6
|
+
module Fakers
|
|
7
|
+
# Contains faker implementations that produce reconciler yamls
|
|
8
|
+
class FakeReconciler < Faker::Base
|
|
9
|
+
class << self
|
|
10
|
+
include FakerHelpers
|
|
11
|
+
|
|
12
|
+
# @!visibility private
|
|
13
|
+
DEFAULT_FORMAT = <<~FORMAT_TEMPLATE
|
|
14
|
+
csv_headers: true
|
|
15
|
+
reverse_order: true
|
|
16
|
+
default_currency: $
|
|
17
|
+
fields:
|
|
18
|
+
date: !!proc Date.strptime(row['Date'], '%m/%d/%Y')
|
|
19
|
+
amount: !!proc >
|
|
20
|
+
withdrawal, deposit = row[3..4].collect {|a| a.to_commodity unless a.empty?};
|
|
21
|
+
( deposit ? deposit.invert! : withdrawal ).quantity_as_s
|
|
22
|
+
description: !!proc row['Description']
|
|
23
|
+
FORMAT_TEMPLATE
|
|
24
|
+
|
|
25
|
+
# @!visibility private
|
|
26
|
+
BASIC_CHECKING_FEED = <<~FEED_TEMPLATE
|
|
27
|
+
from: "%<from>s"
|
|
28
|
+
label: "%<label>s"
|
|
29
|
+
format: %<format>s
|
|
30
|
+
input: %<input_path>s
|
|
31
|
+
output: %<output_path>s
|
|
32
|
+
balances:
|
|
33
|
+
# TODO: Transcribe some expected balances, based off bank statements,
|
|
34
|
+
# here, in the form :
|
|
35
|
+
# '2023-01-04': $ 1000.00
|
|
36
|
+
income: %<income>s
|
|
37
|
+
expense: %<expense>s
|
|
38
|
+
FEED_TEMPLATE
|
|
39
|
+
|
|
40
|
+
# Generates a basic reconciler, for use in reconciling a basic_checking feed
|
|
41
|
+
# @param from [String] The from parameter to write into our yaml
|
|
42
|
+
# @param label [String] The label parameter to write into our yaml
|
|
43
|
+
# @param format_path [String] A path to the format yaml, for use in the format parameter of our yaml
|
|
44
|
+
# @param input_path [String] A path to the input feed, for use in the input parameter of our yaml
|
|
45
|
+
# @param output_path [String] A path to the output journal, for use in the output parameter of our yaml
|
|
46
|
+
# @param income [Array] An array of hashes, containing the income rules, to write into our yaml
|
|
47
|
+
# @param expense [Array] An array of hashes, containing the expense rules, to write into our yaml
|
|
48
|
+
# @return [String] A YAML file, containing the generated reconciler
|
|
49
|
+
def basic_checking(from: 'Personal:Assets:AcmeBank:Checking',
|
|
50
|
+
label: nil,
|
|
51
|
+
format_path: nil,
|
|
52
|
+
input_path: nil,
|
|
53
|
+
output_path: nil,
|
|
54
|
+
income: nil,
|
|
55
|
+
expense: nil)
|
|
56
|
+
|
|
57
|
+
raise StandardError if [from, label, input_path, output_path].any?(&:nil?)
|
|
58
|
+
|
|
59
|
+
format = "!!include #{format_path}" if format_path
|
|
60
|
+
format ||= format("\n%s", DEFAULT_FORMAT.gsub(/^/, ' ').chomp)
|
|
61
|
+
|
|
62
|
+
format BASIC_CHECKING_FEED,
|
|
63
|
+
from: from,
|
|
64
|
+
label: label,
|
|
65
|
+
format: format,
|
|
66
|
+
input_path: input_path,
|
|
67
|
+
output_path: output_path,
|
|
68
|
+
income: hashes_to_yaml_array(
|
|
69
|
+
[income, { match: '/.*/', to: 'Personal:Income:Unknown' }].flatten.compact
|
|
70
|
+
),
|
|
71
|
+
expense: hashes_to_yaml_array(
|
|
72
|
+
[expense, { match: '/.*/', to: 'Personal:Expenses:Unknown' }].flatten.compact
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def hashes_to_yaml_array(hashes)
|
|
79
|
+
format("\n%s", hashes.map do |hash|
|
|
80
|
+
hash.each_with_index.map do |pair, i|
|
|
81
|
+
(i.zero? ? ' - ' : ' ') + pair.join(': ')
|
|
82
|
+
end
|
|
83
|
+
end.flatten.join("\n"))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faker'
|
|
4
|
+
|
|
5
|
+
require_relative '../journal'
|
|
6
|
+
require_relative '../journal/commodity'
|
|
7
|
+
|
|
8
|
+
module RVGP
|
|
9
|
+
module Fakers
|
|
10
|
+
# This module offers code that is shared by a number of our fakers. Mostly,
|
|
11
|
+
# this just keeps those fakers DRY.
|
|
12
|
+
module FakerHelpers
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# Uniformly distribute the transactions over a date range
|
|
16
|
+
def entries_over_date_range(from, to, count = nil)
|
|
17
|
+
raise StandardError unless [from.is_a?(::Date), to.is_a?(::Date), count.is_a?(Numeric)].all?
|
|
18
|
+
|
|
19
|
+
run_length = ((to - from).to_f + 1) / (count - 1)
|
|
20
|
+
|
|
21
|
+
1.upto(count).map { |n| n == count ? to : from + (run_length * (n - 1)) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/rvgp/gem.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RVGP
|
|
6
|
+
# This class contains information relating to our Gem configuration, and is
|
|
7
|
+
# used to produce a gemspec.
|
|
8
|
+
class Gem
|
|
9
|
+
VERSION = '0.3.2'
|
|
10
|
+
|
|
11
|
+
# @!visibility private
|
|
12
|
+
GEM_DIR = File.expand_path format('%s/../..', File.dirname(__FILE__))
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Returns the gem specification
|
|
16
|
+
# @return [Gem::Specification]
|
|
17
|
+
def specification
|
|
18
|
+
::Gem::Specification.new do |s|
|
|
19
|
+
s.name = 'rvgp'
|
|
20
|
+
s.version = VERSION
|
|
21
|
+
s.required_ruby_version = '>= 3.0.0'
|
|
22
|
+
s.licenses = ['LGPL-2.0']
|
|
23
|
+
s.authors = ['Chris DeRose']
|
|
24
|
+
s.email = 'chris@chrisderose.com'
|
|
25
|
+
s.metadata = {
|
|
26
|
+
'source_code_uri' => 'https://github.com/brighton36/rvgp',
|
|
27
|
+
'documentation_uri' => ['https://www.rubydoc.info/gems/rvgp', VERSION].join('/')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
s.doc_dir 'doc'
|
|
31
|
+
|
|
32
|
+
s.summary = 'A workflow tool to: reconcile bank-downloaded csv\'s into ' \
|
|
33
|
+
'categorized pta journals. Run finance validations on those ' \
|
|
34
|
+
'journals. And generate csvs and plots on the output.'
|
|
35
|
+
s.homepage = 'https://github.com/brighton36/rvgp'
|
|
36
|
+
|
|
37
|
+
s.files = files
|
|
38
|
+
|
|
39
|
+
s.executables = ['rvgp']
|
|
40
|
+
|
|
41
|
+
s.add_development_dependency 'minitest', '~> 5.16.0'
|
|
42
|
+
s.add_development_dependency 'yard', '~> 0.9.34'
|
|
43
|
+
s.add_development_dependency 'redcarpet', '~> 3.6.0'
|
|
44
|
+
|
|
45
|
+
s.add_dependency 'open3', '~> 0.1.1'
|
|
46
|
+
s.add_dependency 'shellwords', '~> 0.1.0'
|
|
47
|
+
s.add_dependency 'google-apis-sheets_v4', '~> 0.28.0'
|
|
48
|
+
s.add_dependency 'faker', '~> 3.2.0'
|
|
49
|
+
s.add_dependency 'finance', '~> 2.0.0'
|
|
50
|
+
s.add_dependency 'tty-table', '~> 0.12.0'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# This is a git-less alternative to : `git ls-files`.split "\n"
|
|
55
|
+
# @return [Array<String>] the paths of all rvgp development files in this gem.
|
|
56
|
+
def files
|
|
57
|
+
output, exit_code = Open3.capture2(format("find %s -type f -printf '%%P\n'", root))
|
|
58
|
+
raise StandardError, 'find command failed' unless exit_code.success?
|
|
59
|
+
|
|
60
|
+
output.split("\n").reject do |file|
|
|
61
|
+
ignores = ['.git/*'] + File.read(format('%s/.gitignore', GEM_DIR)).split("\n")
|
|
62
|
+
ignores.any? { |glob| File.fnmatch glob, file }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return all ruby (code) files in this project.
|
|
67
|
+
# @return [Array<String>] the paths of all ruby files in this gem.
|
|
68
|
+
def ruby_files
|
|
69
|
+
files.select { |f| /\A(?:bin.*|Rakefile|.*\.rb)\Z/.match f }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The directory path to the rvgp gem, as calculated from the location of this gem.rb file.
|
|
73
|
+
# @param [String] sub_path If provided, append this path to the output
|
|
74
|
+
# @return [String] The full path to the gem root, plus any subpathing, if appropriate
|
|
75
|
+
def root(sub_path = nil)
|
|
76
|
+
sub_path ? [GEM_DIR, sub_path].join('/') : GEM_DIR
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|