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