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,575 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'csv'
|
|
5
|
+
require 'minitest/autorun'
|
|
6
|
+
|
|
7
|
+
require_relative '../lib/rvgp'
|
|
8
|
+
|
|
9
|
+
[RVGP::Pta::Ledger, RVGP::Pta::HLedger].each do |pta_klass|
|
|
10
|
+
describe pta_klass do
|
|
11
|
+
subject { pta_klass.new }
|
|
12
|
+
|
|
13
|
+
describe "#{pta_klass}#adapter_name" do
|
|
14
|
+
it 'should return the appropriate symbol' do
|
|
15
|
+
value(subject.adapter_name).must_equal subject.is_a?(RVGP::Pta::HLedger) ? :hledger : :ledger
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "#{pta_klass} - #oldest_transaction, #newest_transaction_date, and #{pta_klass}#newest_transaction" do
|
|
20
|
+
let(:journal) do
|
|
21
|
+
<<~JOURNAL
|
|
22
|
+
1990-01-01 Wendy's
|
|
23
|
+
Personal:Expenses:Food:Restaurant $8.44
|
|
24
|
+
Personal:Assets:Cash
|
|
25
|
+
|
|
26
|
+
1982-01-01 McDonald's
|
|
27
|
+
Personal:Expenses:Food:Restaurant $10.15
|
|
28
|
+
Personal:Assets:Cash
|
|
29
|
+
|
|
30
|
+
2002-01-01 Burger King
|
|
31
|
+
Personal:Expenses:Food:Restaurant $3.94
|
|
32
|
+
Personal:Assets:Cash
|
|
33
|
+
|
|
34
|
+
2000-01-01 Arby's
|
|
35
|
+
Personal:Expenses:Food:Restaurant $12.87
|
|
36
|
+
Personal:Assets:Cash
|
|
37
|
+
JOURNAL
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
let(:oldest_tx) { subject.oldest_transaction from_s: journal }
|
|
41
|
+
let(:newest_tx) { subject.newest_transaction from_s: journal }
|
|
42
|
+
|
|
43
|
+
it 'returns the oldest transaction in the file' do
|
|
44
|
+
value(oldest_tx.payee).must_equal "McDonald's"
|
|
45
|
+
value(oldest_tx.date).must_equal Date.new(1982, 1, 1)
|
|
46
|
+
value(oldest_tx.postings.length).must_equal 2
|
|
47
|
+
value(oldest_tx.postings[0].account).must_equal 'Personal:Expenses:Food:Restaurant'
|
|
48
|
+
value(oldest_tx.postings[0].amounts.map(&:to_s)).must_equal ['$ 10.15']
|
|
49
|
+
value(oldest_tx.postings[1].account).must_equal 'Personal:Assets:Cash'
|
|
50
|
+
value(oldest_tx.postings[1].amounts.map(&:to_s)).must_equal ['$ -10.15']
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'returns the newest transaction in the file' do
|
|
54
|
+
value(newest_tx.payee).must_equal 'Burger King'
|
|
55
|
+
value(newest_tx.date).must_equal Date.new(2002, 1, 1)
|
|
56
|
+
value(newest_tx.postings.length).must_equal 2
|
|
57
|
+
value(newest_tx.postings[0].account).must_equal 'Personal:Expenses:Food:Restaurant'
|
|
58
|
+
value(newest_tx.postings[0].amounts.map(&:to_s)).must_equal ['$ 3.94']
|
|
59
|
+
value(newest_tx.postings[1].account).must_equal 'Personal:Assets:Cash'
|
|
60
|
+
value(newest_tx.postings[1].amounts.map(&:to_s)).must_equal ['$ -3.94']
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns the newest transaction date in the file' do
|
|
64
|
+
# This is a specific optimization, that enables us to use hledger stats in the rvgp/config.rb
|
|
65
|
+
value(subject.newest_transaction_date(from_s: journal)).must_equal Date.new(2002, 1, 1)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "#{pta_klass}#files" do
|
|
70
|
+
it 'returns all the files referenced in a journal' do
|
|
71
|
+
journals = 5.times.map { Tempfile.open %w[rvgp_test .journal] }
|
|
72
|
+
|
|
73
|
+
journals[1...].each do |journal|
|
|
74
|
+
journal.write(<<~JOURNAL)
|
|
75
|
+
1990-01-01 Wendy's
|
|
76
|
+
Personal:Expenses:Food:Restaurant $8.44
|
|
77
|
+
Personal:Assets:Cash
|
|
78
|
+
JOURNAL
|
|
79
|
+
journal.close
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
journals[0].write(journals[1...].map do |journal|
|
|
83
|
+
format 'include %s', journal.path
|
|
84
|
+
end.zip(["\n"] * journals.length).flatten.join)
|
|
85
|
+
|
|
86
|
+
journals[0].close
|
|
87
|
+
|
|
88
|
+
value(subject.files(file: journals[0].path).sort).must_equal journals.map(&:path).sort
|
|
89
|
+
|
|
90
|
+
ensure
|
|
91
|
+
journals.each(&:unlink)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "#{pta_klass}#balance" do
|
|
96
|
+
it 'must parse a simple balance query' do
|
|
97
|
+
balance = subject.balance 'Transfers', from_s: <<~JOURNAL
|
|
98
|
+
2023-01-01 Transaction 1
|
|
99
|
+
Transfers:PersonalCredit_PersonalChecking $ 1234.00
|
|
100
|
+
Personal:Assets:AcmeBank:Checking
|
|
101
|
+
|
|
102
|
+
2023-01-02 Transaction 2
|
|
103
|
+
Transfers:PersonalSavings_PersonalChecking $ 5678.90
|
|
104
|
+
Personal:Assets:AcmeBank:Checking
|
|
105
|
+
JOURNAL
|
|
106
|
+
|
|
107
|
+
value(balance.accounts.map(&:fullname)).must_equal ['Transfers:PersonalCredit_PersonalChecking',
|
|
108
|
+
'Transfers:PersonalSavings_PersonalChecking']
|
|
109
|
+
value(balance.accounts[0].amounts.map(&:to_s)).must_equal ['$ 1234.00']
|
|
110
|
+
value(balance.accounts[1].amounts.map(&:to_s)).must_equal ['$ 5678.90']
|
|
111
|
+
|
|
112
|
+
# Maybe I should just remove this feature entirely?....
|
|
113
|
+
value(balance.summary_amounts.map(&:to_s)).must_equal ['$ 6912.90'] if subject.hledger?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'must parse a negative balances query' do
|
|
117
|
+
balance = subject.balance 'Unknown', from_s: <<~JOURNAL
|
|
118
|
+
2023-01-01 Transaction 1
|
|
119
|
+
Personal:Expenses:Unknown $ 700.00
|
|
120
|
+
Personal:Assets:AcmeBank:Checking
|
|
121
|
+
|
|
122
|
+
2023-01-02 Transaction 2
|
|
123
|
+
Personal:Income:Unknown $ -500.00
|
|
124
|
+
Personal:Assets:AcmeBank:Checking
|
|
125
|
+
|
|
126
|
+
2023-01-03 Transaction 3
|
|
127
|
+
Personal:Expenses:Unknown $ 50.00
|
|
128
|
+
Personal:Assets:AcmeBank:Checking
|
|
129
|
+
|
|
130
|
+
2023-01-04 Transaction 4
|
|
131
|
+
Personal:Income:Unknown $ -40.00
|
|
132
|
+
Personal:Assets:AcmeBank:Checking
|
|
133
|
+
JOURNAL
|
|
134
|
+
|
|
135
|
+
value(balance.accounts.length).must_equal 2
|
|
136
|
+
value(balance.accounts[0].fullname).must_equal 'Personal:Expenses:Unknown'
|
|
137
|
+
value(balance.accounts[0].amounts.length).must_equal 1
|
|
138
|
+
value(balance.accounts[0].amounts[0].to_s).must_equal '$ 750.00'
|
|
139
|
+
|
|
140
|
+
value(balance.accounts[1].fullname).must_equal 'Personal:Income:Unknown'
|
|
141
|
+
value(balance.accounts[1].amounts.length).must_equal 1
|
|
142
|
+
value(balance.accounts[1].amounts[0].to_s).must_equal '$ -540.00'
|
|
143
|
+
|
|
144
|
+
# Summary line.
|
|
145
|
+
if subject.hledger?
|
|
146
|
+
value(balance.summary_amounts.length).must_equal 1
|
|
147
|
+
value(balance.summary_amounts[0].to_s).must_equal '$ 210.00'
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'must parse balances in multiple currencies' do
|
|
152
|
+
balance = subject.balance 'Unknown', from_s: <<~JOURNAL
|
|
153
|
+
2023-01-01 Transaction 1
|
|
154
|
+
Personal:Expenses:Unknown $ 41.00
|
|
155
|
+
Personal:Assets:AcmeBank:Checking
|
|
156
|
+
|
|
157
|
+
2023-01-02 Transaction 2
|
|
158
|
+
Personal:Expenses:Unknown 1847.00 GTQ
|
|
159
|
+
Personal:Assets:AcmeBank:Checking
|
|
160
|
+
JOURNAL
|
|
161
|
+
|
|
162
|
+
assert_equal 1, balance.accounts.length
|
|
163
|
+
assert_equal 'Personal:Expenses:Unknown', balance.accounts[0].fullname
|
|
164
|
+
assert_equal 2, balance.accounts[0].amounts.length
|
|
165
|
+
assert_equal ['$ 41.00', '1847.00 GTQ'], balance.accounts[0].amounts.map(&:to_s).sort
|
|
166
|
+
|
|
167
|
+
# Summary line
|
|
168
|
+
if subject.hledger?
|
|
169
|
+
assert_equal 2, balance.summary_amounts.length
|
|
170
|
+
assert_equal '$ 41.00', balance.summary_amounts[0].to_s
|
|
171
|
+
assert_equal '1847.00 GTQ', balance.summary_amounts[1].to_s
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'converts commodities, given a pricer' do
|
|
176
|
+
# TODO: Put the TestLedger#test_balance_multiple_with_empty here. Refactored
|
|
177
|
+
skip 'TODO: Put the TestLedger#test_balance_multiple_with_empty here. Refactored'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it "parses depth #{pta_klass}" do
|
|
181
|
+
balance = pta_klass.new.balance 'Personal:Assets:AcmeBank:Checking',
|
|
182
|
+
depth: 1,
|
|
183
|
+
from_s: <<~JOURNAL
|
|
184
|
+
1996-02-03 Publix
|
|
185
|
+
Personal:Expenses:Food:Groceries $ 123.45
|
|
186
|
+
Personal:Assets:AcmeBank:Checking
|
|
187
|
+
JOURNAL
|
|
188
|
+
|
|
189
|
+
value(balance.accounts.length).must_equal 1
|
|
190
|
+
value(balance.accounts[0].fullname).must_equal 'Personal'
|
|
191
|
+
value(balance.accounts[0].amounts.length).must_equal 1
|
|
192
|
+
value(balance.accounts[0].amounts[0].to_s).must_equal '$ -123.45'
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "#{pta_klass}#register" do
|
|
197
|
+
it 'matches the output of the csv command, on longer queries' do
|
|
198
|
+
# This is just your basic activity, on a mostly unused savings account
|
|
199
|
+
journal = <<~JOURNAL
|
|
200
|
+
2021/01/26 INTEREST PAYMENT
|
|
201
|
+
Personal:Income:AcmeBank:Interest $ -7.89
|
|
202
|
+
Personal:Assets:AcmeBank:Savings
|
|
203
|
+
|
|
204
|
+
2021/01/26 MONTHLY SERVICE FEE
|
|
205
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
206
|
+
Personal:Assets:AcmeBank:Savings
|
|
207
|
+
|
|
208
|
+
2021/02/23 INTEREST PAYMENT
|
|
209
|
+
Personal:Income:AcmeBank:Interest $ -7.89
|
|
210
|
+
Personal:Assets:AcmeBank:Savings
|
|
211
|
+
|
|
212
|
+
2021/02/23 MONTHLY SERVICE FEE
|
|
213
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
214
|
+
Personal:Assets:AcmeBank:Savings
|
|
215
|
+
|
|
216
|
+
2021/03/15 ONLINE TRANSFER FROM PERSONAL CHECKING ON 03/13/21
|
|
217
|
+
Transfers:PersonalSavings_PersonalChecking $ -100.00
|
|
218
|
+
Personal:Assets:AcmeBank:Savings
|
|
219
|
+
|
|
220
|
+
2021/03/22 INTEREST PAYMENT
|
|
221
|
+
Personal:Income:AcmeBank:Interest $ -7.90
|
|
222
|
+
Personal:Assets:AcmeBank:Savings
|
|
223
|
+
|
|
224
|
+
2021/03/22 MONTHLY SERVICE FEE
|
|
225
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
226
|
+
Personal:Assets:AcmeBank:Savings
|
|
227
|
+
|
|
228
|
+
2021/04/22 INTEREST PAYMENT
|
|
229
|
+
Personal:Income:AcmeBank:Interest $ -7.89
|
|
230
|
+
Personal:Assets:AcmeBank:Savings
|
|
231
|
+
|
|
232
|
+
2021/04/22 MONTHLY SERVICE FEE
|
|
233
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
234
|
+
Personal:Assets:AcmeBank:Savings
|
|
235
|
+
|
|
236
|
+
2021/05/24 INTEREST PAYMENT
|
|
237
|
+
Personal:Income:AcmeBank:Interest $ -7.89
|
|
238
|
+
Personal:Assets:AcmeBank:Savings
|
|
239
|
+
|
|
240
|
+
2021/05/24 MONTHLY SERVICE FEE
|
|
241
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
242
|
+
Personal:Assets:AcmeBank:Savings
|
|
243
|
+
|
|
244
|
+
2021/06/22 INTEREST PAYMENT
|
|
245
|
+
Personal:Income:AcmeBank:Interest $ -7.89
|
|
246
|
+
Personal:Assets:AcmeBank:Savings
|
|
247
|
+
|
|
248
|
+
2021/06/22 MONTHLY SERVICE FEE
|
|
249
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
250
|
+
Personal:Assets:AcmeBank:Savings
|
|
251
|
+
|
|
252
|
+
2021/07/23 INTEREST PAYMENT
|
|
253
|
+
Personal:Income:AcmeBank:Interest $ -7.90
|
|
254
|
+
Personal:Assets:AcmeBank:Savings
|
|
255
|
+
|
|
256
|
+
2021/07/23 MONTHLY SERVICE FEE
|
|
257
|
+
Personal:Expenses:Banking:Fees:AcmeBank $ 5.00
|
|
258
|
+
Personal:Assets:AcmeBank:Savings
|
|
259
|
+
JOURNAL
|
|
260
|
+
|
|
261
|
+
register = subject.register 'Personal:Assets:AcmeBank:Savings', related: true, from_s: journal
|
|
262
|
+
|
|
263
|
+
# We just use hledger here, rather than maintain two versions of our csv_rows truth table:
|
|
264
|
+
csv_rows = CSV.parse(RVGP::Pta::HLedger.new.command('register',
|
|
265
|
+
'Personal:Assets:AcmeBank:Savings',
|
|
266
|
+
from_s: journal, related: true, 'output-format': 'csv'),
|
|
267
|
+
headers: true)
|
|
268
|
+
|
|
269
|
+
assert_equal csv_rows.length, register.transactions.length
|
|
270
|
+
|
|
271
|
+
csv_rows.each_with_index do |csv_row, i|
|
|
272
|
+
value(register.transactions[i].postings.length).must_equal 1
|
|
273
|
+
value(register.transactions[i].postings[0].amounts.length).must_equal 1
|
|
274
|
+
value(register.transactions[i].postings[0].totals.length).must_equal 1
|
|
275
|
+
value(register.transactions[i].date.to_s).must_equal csv_row['date']
|
|
276
|
+
value(register.transactions[i].payee).must_equal csv_row['description']
|
|
277
|
+
value(register.transactions[i].postings[0].account).must_equal csv_row['account']
|
|
278
|
+
value(register.transactions[i].postings[0].amounts[0].to_s).must_equal csv_row['amount']
|
|
279
|
+
value(register.transactions[i].postings[0].totals[0].to_s).must_equal csv_row['total']
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'parses multiple commodities and tags' do
|
|
284
|
+
register = subject.register 'Personal:Expenses', from_s: <<~JOURNAL
|
|
285
|
+
2023-02-14 Food Lion
|
|
286
|
+
Personal:Expenses:Food:Groceries $26.18 ; intention: Personal
|
|
287
|
+
Personal:Expenses:Vices:Alcohol $18.26
|
|
288
|
+
; Dating:
|
|
289
|
+
; ValentinesDay:
|
|
290
|
+
; intention: Personal
|
|
291
|
+
Personal:Assets:Cash
|
|
292
|
+
|
|
293
|
+
2023-02-16 2x Lotto tickets
|
|
294
|
+
Personal:Expenses:Vices:Gambling $ 2.00
|
|
295
|
+
; Loss:
|
|
296
|
+
; intention: Personal
|
|
297
|
+
Personal:Assets:Cash
|
|
298
|
+
|
|
299
|
+
2023-02-19 Agua con Gas
|
|
300
|
+
Personal:Expenses:Food:Water 4000.00 COP
|
|
301
|
+
; intention: Personal
|
|
302
|
+
Personal:Assets:Cash
|
|
303
|
+
|
|
304
|
+
2023-02-20 Carulla
|
|
305
|
+
Personal:Expenses:Food:Groceries 56123.00 COP
|
|
306
|
+
; intention: Personal
|
|
307
|
+
Personal:Expenses:Food:Water 4000.00 COP
|
|
308
|
+
; intention: Personal
|
|
309
|
+
Personal:Assets:Cash
|
|
310
|
+
JOURNAL
|
|
311
|
+
|
|
312
|
+
value(register.transactions.length).must_equal 4
|
|
313
|
+
|
|
314
|
+
# Dates:
|
|
315
|
+
value(register.transactions.map(&:date).map(&:to_s)).must_equal %w[2023-02-14 2023-02-16 2023-02-19 2023-02-20]
|
|
316
|
+
|
|
317
|
+
# Payees
|
|
318
|
+
value(register.transactions.map(&:payee).map(&:to_s)).must_equal(
|
|
319
|
+
['Food Lion', '2x Lotto tickets', 'Agua con Gas', 'Carulla']
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Transaction 1:
|
|
323
|
+
value(register.transactions[0].postings.map(&:account)).must_equal ['Personal:Expenses:Food:Groceries',
|
|
324
|
+
'Personal:Expenses:Vices:Alcohol']
|
|
325
|
+
value(register.transactions[0].postings.map(&:amounts).flatten.map(&:to_s)).must_equal ['$ 26.18', '$ 18.26']
|
|
326
|
+
value(register.transactions[0].postings.map(&:totals).flatten.map(&:to_s)).must_equal ['$ 26.18', '$ 44.44']
|
|
327
|
+
value(register.transactions[0].postings.map(&:tags)).must_equal(
|
|
328
|
+
[{ 'intention' => 'Personal' },
|
|
329
|
+
if subject.hledger?
|
|
330
|
+
{ 'Dating' => true, 'ValentinesDay' => true, 'intention' => 'Personal' }
|
|
331
|
+
else
|
|
332
|
+
{ 'intention' => 'Personal' }
|
|
333
|
+
end]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Transaction 2:
|
|
337
|
+
value(register.transactions[1].postings.map(&:account)).must_equal ['Personal:Expenses:Vices:Gambling']
|
|
338
|
+
value(register.transactions[1].postings.map(&:amounts).flatten.map(&:to_s)).must_equal ['$ 2.00']
|
|
339
|
+
value(register.transactions[1].postings.map(&:totals).flatten.map(&:to_s)).must_equal ['$ 46.44']
|
|
340
|
+
value(register.transactions[1].postings.map(&:tags)).must_equal [if subject.hledger?
|
|
341
|
+
{ 'intention' => 'Personal', 'Loss' => true }
|
|
342
|
+
else
|
|
343
|
+
{ 'intention' => 'Personal' }
|
|
344
|
+
end]
|
|
345
|
+
|
|
346
|
+
# Transaction 3:
|
|
347
|
+
value(register.transactions[2].postings.map(&:account)).must_equal ['Personal:Expenses:Food:Water']
|
|
348
|
+
value(register.transactions[2].postings.map(&:amounts).flatten.map(&:to_s)).must_equal ['4000.00 COP']
|
|
349
|
+
value(register.transactions[2].postings.map(&:totals).flatten.map(&:to_s).sort).must_equal ['$ 46.44',
|
|
350
|
+
'4000.00 COP']
|
|
351
|
+
value(register.transactions[2].postings.map(&:tags)).must_equal [{ 'intention' => 'Personal' }]
|
|
352
|
+
|
|
353
|
+
# Transaction 4:
|
|
354
|
+
value(register.transactions[3].postings.map(&:account)).must_equal ['Personal:Expenses:Food:Groceries',
|
|
355
|
+
'Personal:Expenses:Food:Water']
|
|
356
|
+
value(register.transactions[3].postings.map(&:amounts).flatten.map(&:to_s)).must_equal ['56123.00 COP',
|
|
357
|
+
'4000.00 COP']
|
|
358
|
+
value(register.transactions[3].postings.map(&:totals).flatten.map(&:to_s).sort).must_equal ['$ 46.44',
|
|
359
|
+
'$ 46.44',
|
|
360
|
+
'60123.00 COP',
|
|
361
|
+
'64123.00 COP']
|
|
362
|
+
value(register.transactions[3].postings.map(&:tags)).must_equal [{ 'intention' => 'Personal' }] * 2
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it 'supports price conversion, through :pricer' do
|
|
366
|
+
transactions = subject.register(
|
|
367
|
+
'Personal:Assets:Cash',
|
|
368
|
+
monthly: true,
|
|
369
|
+
pricer: RVGP::Journal::Pricer.new(<<~PRICES),
|
|
370
|
+
P 2023-05-01 HNL $0.040504
|
|
371
|
+
P 2023-06-01 CLP $0.0012
|
|
372
|
+
PRICES
|
|
373
|
+
from_s: <<~JOURNAL
|
|
374
|
+
2023-05-02 Flight to Honduras
|
|
375
|
+
Personal:Expenses:Transportation:Airline $ 252.78
|
|
376
|
+
Personal:Assets:Cash
|
|
377
|
+
|
|
378
|
+
2023-05-14 Roatan Dive Center
|
|
379
|
+
Personal:Expenses:Hobbies:SCUBA 876.54 HNL
|
|
380
|
+
Personal:Assets:Cash
|
|
381
|
+
|
|
382
|
+
2023-05-15 Eldon's Supermarket
|
|
383
|
+
Personal:Expenses:Food:Groceries 543.21 HNL
|
|
384
|
+
Personal:Assets:Cash
|
|
385
|
+
|
|
386
|
+
2023-05-30 Flight to Chile
|
|
387
|
+
Personal:Expenses:Transportation:Airline $ 432.10
|
|
388
|
+
Personal:Assets:Cash
|
|
389
|
+
|
|
390
|
+
2023-06-02 Nevados de Chillan
|
|
391
|
+
Personal:Expenses:Hobbies:Snowboarding 33143.60 CLP
|
|
392
|
+
Personal:Assets:Cash
|
|
393
|
+
|
|
394
|
+
2023-06-03 La Cabrera Chile Isidora
|
|
395
|
+
Personal:Expenses:Food:Restaurants 24856.20 CLP
|
|
396
|
+
Personal:Assets:Cash
|
|
397
|
+
|
|
398
|
+
2023-06-14 Flight Home
|
|
399
|
+
Personal:Expenses:Transportation:Airline $ 651.09
|
|
400
|
+
Personal:Assets:Cash
|
|
401
|
+
JOURNAL
|
|
402
|
+
).transactions
|
|
403
|
+
|
|
404
|
+
value(transactions.map(&:date).map(&:to_s)).must_equal %w[2023-05-01 2023-06-01]
|
|
405
|
+
value(transactions.map(&:payee).compact).must_equal(subject.hledger? ? [] : ['- 23-May-31', '- 23-Jun-30'])
|
|
406
|
+
value(transactions.map(&:postings).flatten.map(&:account)).must_equal ['Personal:Assets:Cash'] * 2
|
|
407
|
+
value(transactions.map(&:postings).flatten.map(&:tags)).must_equal [{}] * 2
|
|
408
|
+
|
|
409
|
+
# Month 1:
|
|
410
|
+
value(transactions[0].postings[0].amounts.map(&:to_s).sort).must_equal ['$ -684.88', '-1419.75 HNL']
|
|
411
|
+
value(transactions[0].postings[0].totals.map(&:to_s).sort).must_equal ['$ -684.88', '-1419.75 HNL']
|
|
412
|
+
|
|
413
|
+
value(transactions[0].postings[0].amount_in('$').to_s).must_equal '$ -742.385554'
|
|
414
|
+
value(transactions[0].postings[0].total_in('$').to_s).must_equal '$ -742.385554'
|
|
415
|
+
|
|
416
|
+
# Month 2:
|
|
417
|
+
|
|
418
|
+
# NOTE: This is a bug in ledger. Seemingly, the register command shows the .80. The correct amount.
|
|
419
|
+
# However, the xml output, shows '.8'. I don't think there are any great solutions to this, so, for
|
|
420
|
+
# now, I'm doing this in the tests:
|
|
421
|
+
expected_clp = subject.hledger? ? '-57999.80 CLP' : '-57999.8 CLP'
|
|
422
|
+
|
|
423
|
+
value(transactions[1].postings[0].amounts.map(&:to_s)).must_equal ['$ -651.09', expected_clp]
|
|
424
|
+
value(transactions[1].postings[0].totals.map(&:to_s).sort).must_equal ['$ -1335.97',
|
|
425
|
+
'-1419.75 HNL',
|
|
426
|
+
expected_clp]
|
|
427
|
+
|
|
428
|
+
value(transactions[1].postings[0].amount_in('$').to_s).must_equal '$ -720.68976'
|
|
429
|
+
value(transactions[1].postings[0].total_in('$').to_s).must_equal '$ -1463.075314'
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
describe "#{pta_klass}#tags" do
|
|
434
|
+
let(:journal) do
|
|
435
|
+
<<~JOURNAL
|
|
436
|
+
2023-01-01 Transaction 1
|
|
437
|
+
Personal:Expenses:TaggedExpense $ 1.00
|
|
438
|
+
; color: red
|
|
439
|
+
; vacation: Hawaii
|
|
440
|
+
Personal:Assets:AcmeBank:Checking
|
|
441
|
+
|
|
442
|
+
2023-01-02 Transaction 2
|
|
443
|
+
Personal:Expenses:TaggedExpense $ 2.00
|
|
444
|
+
; color: orange
|
|
445
|
+
; :business:
|
|
446
|
+
Personal:Assets:AcmeBank:Checking
|
|
447
|
+
|
|
448
|
+
2023-01-03 Transaction 3
|
|
449
|
+
Personal:Expenses:TaggedExpense $ 3.00
|
|
450
|
+
; color: yellow
|
|
451
|
+
; :medical:
|
|
452
|
+
Personal:Assets:AcmeBank:Checking
|
|
453
|
+
|
|
454
|
+
2023-01-04 Transaction 4
|
|
455
|
+
Personal:Expenses:TaggedExpense $ 4.00
|
|
456
|
+
; color: green
|
|
457
|
+
Personal:Assets:AcmeBank:Checking
|
|
458
|
+
|
|
459
|
+
2023-01-05 Transaction 5
|
|
460
|
+
Personal:Expenses:TaggedExpense $ 5.00
|
|
461
|
+
; color: blue
|
|
462
|
+
Personal:Assets:AcmeBank:Checking
|
|
463
|
+
|
|
464
|
+
2023-01-06 Transaction 6
|
|
465
|
+
Personal:Expenses:TaggedExpense $ 6.00
|
|
466
|
+
; color: indigo
|
|
467
|
+
Personal:Assets:AcmeBank:Checking
|
|
468
|
+
|
|
469
|
+
2023-01-07 Transaction 7
|
|
470
|
+
Personal:Expenses:TaggedExpense $ 7.00
|
|
471
|
+
; color: violet
|
|
472
|
+
Personal:Assets:AcmeBank:Checking
|
|
473
|
+
|
|
474
|
+
2023-01-08 Transaction 8
|
|
475
|
+
Personal:Expenses:TaggedExpense $ 8.00
|
|
476
|
+
; vacation: Argentina
|
|
477
|
+
Personal:Assets:AcmeBank:Checking
|
|
478
|
+
|
|
479
|
+
2023-01-09 Transaction 9
|
|
480
|
+
Personal:Expenses:TaggedExpense $ 9.00
|
|
481
|
+
; vacation: Germany
|
|
482
|
+
Personal:Assets:AcmeBank:Checking
|
|
483
|
+
|
|
484
|
+
2023-01-10 Transaction 10
|
|
485
|
+
Personal:Expenses:TaggedExpense $ 10.00
|
|
486
|
+
; vacation: Japan
|
|
487
|
+
Personal:Assets:AcmeBank:Checking
|
|
488
|
+
JOURNAL
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
it 'parses tags' do
|
|
492
|
+
value(subject.tags(from_s: journal)).must_equal %w[business color medical vacation]
|
|
493
|
+
value(subject.tags('color', from_s: journal, values: true)).must_equal(
|
|
494
|
+
%w[blue green indigo orange red violet yellow]
|
|
495
|
+
)
|
|
496
|
+
value(subject.tags('vacation', from_s: journal, values: true)).must_equal %w[Argentina Germany Hawaii Japan]
|
|
497
|
+
|
|
498
|
+
value(subject.tags(values: true, from_s: journal)).must_equal(
|
|
499
|
+
# See the note on #tags to understand the ethos here, and why we can't/won't conform the output between pta
|
|
500
|
+
# adapter implementations on this query
|
|
501
|
+
if subject.ledger?
|
|
502
|
+
['business', 'color: blue', 'color: green', 'color: indigo', 'color: orange', 'color: red', 'color: violet',
|
|
503
|
+
'color: yellow', 'medical', 'vacation: Argentina', 'vacation: Germany', 'vacation: Hawaii',
|
|
504
|
+
'vacation: Japan']
|
|
505
|
+
else
|
|
506
|
+
%w[Argentina Germany Hawaii Japan blue green indigo orange red violet yellow]
|
|
507
|
+
end
|
|
508
|
+
)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
describe 'pta adapter errata' do
|
|
515
|
+
# This is a bug in ledger. The register output works fine. The xml output, does not. Possibly... we should
|
|
516
|
+
# warn here, so that when the problem is solved, we can ... know it?
|
|
517
|
+
# alternatively, we can just fix this in our code, I think. By adjusting the ledger adapter, for the case
|
|
518
|
+
# when 0 is returned....
|
|
519
|
+
describe '--empty bug' do
|
|
520
|
+
let(:args) do
|
|
521
|
+
# NOTE: This errata only triggers, if there's another tx in the listings, for that month. Hence the Reading
|
|
522
|
+
['Personal:Expenses',
|
|
523
|
+
{ monthly: true, begin: Date.new(2023, 7, 1), end: Date.new(2023, 8, 1), from_s: <<~JOURNAL }]
|
|
524
|
+
2023/07/11 Shakespeare and Company
|
|
525
|
+
Personal:Expenses:Hobbies:Reading $ 12.34
|
|
526
|
+
Personal:Assets:AcmeBank:Checking
|
|
527
|
+
|
|
528
|
+
2023/07/13 GAP Clothing
|
|
529
|
+
Personal:Expenses:Clothes $ 50.01
|
|
530
|
+
Personal:Assets:AcmeBank:Checking
|
|
531
|
+
|
|
532
|
+
2023/07/24 GAP Clothing (RMA)
|
|
533
|
+
Personal:Expenses:Clothes $ -50.01
|
|
534
|
+
Personal:Assets:AcmeBank:Checking
|
|
535
|
+
JOURNAL
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
let(:args_with_fix) do
|
|
539
|
+
[args[0], args[1].merge(empty: false)]
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
it 'returns nil for the case of a query whose net change is 0' do
|
|
543
|
+
# This was a very specific bug that crept up, mostly because ledger handles this a bit differently than
|
|
544
|
+
# hledger. If two transactions 'cancel each other out' in a given month, hledger reports nil, and ledger
|
|
545
|
+
# reports '0'. This may be a case where all 0's, without a currency code, should return nil. And, I just
|
|
546
|
+
# happened to find that case exhibited in this circumstance.
|
|
547
|
+
#
|
|
548
|
+
# This test also ensures that if there's only one such transaction - we receive an empty transactions list.
|
|
549
|
+
|
|
550
|
+
# Hledger just works, and these tests are here for posterity. Asserting that hledger works as expected
|
|
551
|
+
[args, args_with_fix].each do |a|
|
|
552
|
+
transactions = RVGP::Pta::HLedger.new.register(*a).transactions
|
|
553
|
+
value(transactions.length).must_equal 1
|
|
554
|
+
value(transactions[0].payee).must_be_nil
|
|
555
|
+
value(transactions[0].postings.length).must_equal 1
|
|
556
|
+
value(transactions[0].postings[0].amounts.map(&:to_s)).must_equal ['$ 12.34']
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# These specs just define the error, through our expectations of how it manefests:
|
|
560
|
+
transactions = RVGP::Pta::Ledger.new.register(*args).transactions
|
|
561
|
+
value(transactions.length).must_equal 1
|
|
562
|
+
value(transactions[0].payee).must_equal '- 23-Jul-31'
|
|
563
|
+
value(transactions[0].postings.length).must_equal 2
|
|
564
|
+
value(transactions[0].postings[0].amounts.map(&:to_s)).must_equal ['$ 0.00']
|
|
565
|
+
value(transactions[0].postings[1].amounts.map(&:to_s)).must_equal ['$ 12.34']
|
|
566
|
+
|
|
567
|
+
# This is the fix:
|
|
568
|
+
transactions = RVGP::Pta::Ledger.new.register(*args_with_fix).transactions
|
|
569
|
+
value(transactions.length).must_equal 1
|
|
570
|
+
value(transactions[0].payee).must_equal '- 23-Jul-31'
|
|
571
|
+
value(transactions[0].postings.length).must_equal 1
|
|
572
|
+
value(transactions[0].postings[0].amounts.map(&:to_s)).must_equal ['$ 12.34']
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'minitest/autorun'
|
|
5
|
+
|
|
6
|
+
require_relative '../lib/rvgp'
|
|
7
|
+
require_relative '../lib/rvgp/utilities'
|
|
8
|
+
|
|
9
|
+
# RVGP::Utilities tests
|
|
10
|
+
class TestUtilities < Minitest::Test
|
|
11
|
+
include RVGP::Utilities
|
|
12
|
+
|
|
13
|
+
# This is kind of a weird function... just sayin...
|
|
14
|
+
def test_months_through
|
|
15
|
+
string_to_date = ->(s) { Date.strptime s }
|
|
16
|
+
|
|
17
|
+
assert_equal %w[2019-01-01 2019-02-01 2019-03-01 2019-04-01 2019-05-01 2019-06-01
|
|
18
|
+
2019-07-01 2019-08-01 2019-09-01 2019-10-01 2019-11-01 2019-12-01].collect(&string_to_date),
|
|
19
|
+
months_through(
|
|
20
|
+
*%w[2019-01-01 2019-02-01 2019-03-01 2019-04-01 2019-05-01 2019-06-01
|
|
21
|
+
2019-07-01 2019-08-01 2019-09-01 2019-10-01 2019-11-01 2019-12-01
|
|
22
|
+
2019-01-01 2019-02-01 2019-03-01 2019-04-01 2019-05-01 2019-06-01
|
|
23
|
+
2019-07-01 2019-08-01 2019-09-01 2019-10-01 2019-11-01 2019-12-01].collect(&string_to_date)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert_equal %w[2021-01-01 2021-02-01 2021-03-01 2021-04-01 2021-05-01
|
|
27
|
+
2021-06-01 2021-07-01 2021-08-01 2021-09-01 2021-10-01
|
|
28
|
+
2021-11-01].collect(&string_to_date),
|
|
29
|
+
months_through(Date.new(2021, 1, 1), Date.new(2021, 11, 29))
|
|
30
|
+
|
|
31
|
+
assert_equal %w[2021-01-01 2021-02-01 2021-03-01 2021-04-01 2021-05-01
|
|
32
|
+
2021-06-01 2021-07-01 2021-08-01 2021-09-01 2021-10-01
|
|
33
|
+
2021-11-01 2021-12-01 2022-01-01].collect(&string_to_date),
|
|
34
|
+
months_through(Date.new(2021, 1, 1), Date.new(2022, 1, 1))
|
|
35
|
+
|
|
36
|
+
assert_equal %w[2018-01-01 2018-02-01 2018-03-01 2018-04-01 2018-05-01
|
|
37
|
+
2018-06-01 2018-07-01 2018-08-01 2018-09-01 2018-10-01
|
|
38
|
+
2018-11-01 2018-12-01].collect(&string_to_date),
|
|
39
|
+
months_through(Date.new(2018, 1, 1), Date.new(2018, 12, 31))
|
|
40
|
+
|
|
41
|
+
# I think this is the behavior we want....
|
|
42
|
+
assert_equal %w[2021-01-01 2021-02-01].collect(&string_to_date),
|
|
43
|
+
months_through(Date.new(2021, 1, 10), Date.new(2021, 2, 10))
|
|
44
|
+
end
|
|
45
|
+
end
|