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,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
|