ledger-rest 2.0.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +1 -1
  4. data/Guardfile +4 -22
  5. data/{LICENSE.txt → LICENSE} +1 -1
  6. data/README.md +62 -11
  7. data/config.ru +1 -1
  8. data/ledger-rest.gemspec +17 -17
  9. data/ledger-rest.yml.example +3 -0
  10. data/lib/ledger-rest/app.rb +21 -34
  11. data/lib/ledger-rest/core_ext.rb +4 -3
  12. data/lib/ledger-rest/git.rb +8 -7
  13. data/lib/ledger-rest/ledger.rb +29 -24
  14. data/lib/ledger-rest/ledger/balance.rb +48 -15
  15. data/lib/ledger-rest/ledger/budget.rb +35 -16
  16. data/lib/ledger-rest/ledger/entry.rb +1 -4
  17. data/lib/ledger-rest/ledger/parser.rb +41 -37
  18. data/lib/ledger-rest/ledger/register.rb +31 -24
  19. data/lib/ledger-rest/ledger/transaction.rb +33 -28
  20. data/lib/ledger-rest/transactions.py +47 -0
  21. data/lib/ledger-rest/version.rb +2 -1
  22. data/spec/files/append.ledger +13 -0
  23. data/spec/files/budget.ledger +4 -0
  24. data/spec/files/main.ledger +4 -0
  25. data/spec/files/opening_balance.ledger +4 -0
  26. data/spec/files/transactions.ledger +11 -0
  27. data/spec/ledger-rest.yml +3 -0
  28. data/spec/ledger-rest/api/accounts_spec.rb +19 -0
  29. data/spec/ledger-rest/api/balance_spec.rb +117 -0
  30. data/spec/ledger-rest/api/budget_spec.rb +110 -0
  31. data/spec/ledger-rest/api/payees_spec.rb +19 -0
  32. data/spec/ledger-rest/api/register_spec.rb +233 -0
  33. data/spec/ledger-rest/api/transactions_spec.rb +201 -0
  34. data/spec/ledger-rest/api/version_spec.rb +13 -0
  35. data/spec/ledger-rest/ledger/balance_spec.rb +124 -0
  36. data/spec/ledger-rest/ledger/parser_spec.rb +184 -117
  37. data/spec/ledger-rest/ledger/transaction_spec.rb +38 -31
  38. data/spec/spec_helper.rb +22 -0
  39. data/spec/support/deep_eq_matcher.rb +149 -0
  40. metadata +52 -31
  41. data/ledger-rest.org +0 -22
@@ -0,0 +1,47 @@
1
+ import ledger
2
+ import json
3
+ import sys
4
+
5
+ filename = sys.argv[1]
6
+ query = sys.argv[2]
7
+
8
+ transactions = []
9
+ last_xact = None
10
+ for match in ledger.read_journal(filename).query(query):
11
+ if match.xact != last_xact:
12
+ transaction = {}
13
+ transaction['payee'] = match.xact.payee
14
+ transaction['date'] = match.xact.date.strftime('%Y/%m/%d')
15
+
16
+ if match.xact.code != None:
17
+ transaction['code'] = match.xact.code
18
+
19
+ if match.xact.note != None:
20
+ transaction['note'] = match.xact.note.strip()
21
+
22
+ if match.xact.aux_date != None:
23
+ transaction['effective_date'] = match.xact.aux_date.strftime('%Y/%m/%d')
24
+
25
+ if str(match.xact.state) == 'Cleared':
26
+ transaction['cleared'] = True
27
+ transaction['pending'] = False
28
+ elif str(match.xact.state) == 'Pending':
29
+ transaction['cleared'] = False
30
+ transaction['pending'] = True
31
+ else:
32
+ transaction['cleared'] = False
33
+ transaction['pending'] = False
34
+
35
+ transaction['posts'] = []
36
+ for post in match.xact.posts():
37
+ new_post = {}
38
+ new_post['account'] = str(post.account)
39
+ new_post['amount'] = float(post.amount)
40
+ new_post['commodity'] = str(post.amount.commodity)
41
+ transaction['posts'].append(new_post)
42
+
43
+ transactions.append(transaction)
44
+
45
+ last_xact = match.xact
46
+
47
+ print json.dumps(transactions)
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module LedgerRest
2
- VERSION = "2.0.3"
3
+ VERSION = '3.0.0'
3
4
  end
@@ -0,0 +1,13 @@
1
+ 2013/12/03 NaveenaPath
2
+ Expenses:Restaurants 9.00EUR
3
+ Assets:Cash
4
+
5
+ 2013/12/05 Shikgoo
6
+ Expenses:Restaurants 12.00EUR
7
+ Liabilities:Max Mustermann
8
+ ; meta_info: Some interesting meta information
9
+
10
+ 2013/12/10 Bioladen Tegeler Straße
11
+ ; Sonnenblumenkernbrot
12
+ Assets:Reimbursements:Hans Maulwurf 3.10EUR
13
+ Assets:Cash
@@ -0,0 +1,4 @@
1
+ ~ Monthly
2
+ Expenses:Flat 450EUR
3
+ Expenses:Restaurants 100EUR
4
+ Assets
@@ -0,0 +1,4 @@
1
+ include opening_balance.ledger
2
+ include append.ledger
3
+ include budget.ledger
4
+ include transactions.ledger
@@ -0,0 +1,4 @@
1
+ 2013/01/01 * Opening Balance
2
+ Assets:Giro 2000EUR
3
+ Assets:Reimbursements:Hans Maulwurf 19EUR
4
+ Equity:Opening Balances
@@ -0,0 +1,11 @@
1
+ 2013/12/01=2013/12/02 * Sparkasse
2
+ Assets:Cash 50.00EUR
3
+ Assets:Giro
4
+
5
+ 2013/12/04 Trattoria La Vialla
6
+ Expenses:Restaurants 32.20EUR
7
+ Assets:Cash
8
+
9
+ 2013/12/06 Customer X
10
+ Assets:Giro 1200.00EUR
11
+ Income:Invoice
@@ -0,0 +1,3 @@
1
+ ledger_file: "spec/files/main.ledger"
2
+ ledger_bin: "/bin/ledger"
3
+ ledger_append_file: "spec/files/append.ledger"
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe '/accounts' do
5
+ it 'returns accounts' do
6
+ get '/accounts'
7
+
8
+ JSON.parse(last_response.body).should =~
9
+ [
10
+ 'Assets:Cash',
11
+ 'Assets:Giro',
12
+ 'Assets:Reimbursements:Hans Maulwurf',
13
+ 'Equity:Opening Balances',
14
+ 'Expenses:Restaurants',
15
+ 'Income:Invoice',
16
+ 'Liabilities:Max Mustermann'
17
+ ]
18
+ end
19
+ end
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe '/balance' do
5
+ context 'normally' do
6
+ let(:valid_json) do
7
+ {
8
+ 'accounts' =>
9
+ [
10
+ {
11
+ 'total' => '3177.80EUR',
12
+ 'name' => 'Assets',
13
+ 'depth' => 1,
14
+ 'fullname' => 'Assets',
15
+ 'accounts' =>
16
+ [
17
+ {
18
+ 'total' => '5.70EUR',
19
+ 'name' => 'Cash',
20
+ 'depth' => 2,
21
+ 'fullname' => 'Assets:Cash'
22
+ },
23
+ {
24
+ 'total' => '3150.00EUR',
25
+ 'name' => 'Giro',
26
+ 'depth' => 2,
27
+ 'fullname' => 'Assets:Giro'
28
+ },
29
+ {
30
+ 'total' => '22.10EUR',
31
+ 'name' => 'Reimbursements',
32
+ 'depth' => 2,
33
+ 'fullname' => 'Assets:Reimbursements',
34
+ 'accounts' =>
35
+ [
36
+ {
37
+ 'total' => '22.10EUR',
38
+ 'name' => 'Hans Maulwurf',
39
+ 'depth' => 3,
40
+ 'fullname' => 'Assets:Reimbursements:Hans Maulwurf'
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ 'total' => '-12.00EUR',
48
+ 'name' => 'Liabilities',
49
+ 'depth' => 1,
50
+ 'fullname' => 'Liabilities',
51
+ 'accounts' =>
52
+ [
53
+ {
54
+ 'total' => '-12.00EUR',
55
+ 'name' => 'Max Mustermann',
56
+ 'depth' => 2,
57
+ 'fullname' => 'Liabilities:Max Mustermann'
58
+ }
59
+ ]
60
+ }
61
+ ],
62
+ 'total' => '3165.80EUR'
63
+ }
64
+ end
65
+
66
+ it 'expands and wraps accounts' do
67
+ get '/balance', query: 'Assets Liabilities'
68
+ JSON.parse(last_response.body).should deep_eq valid_json
69
+ end
70
+ end
71
+
72
+ context 'with --flat query' do
73
+ let(:valid_json) do
74
+ {
75
+ 'accounts' =>
76
+ [
77
+ {
78
+ 'total' => '5.70EUR',
79
+ 'name' => 'Cash',
80
+ 'depth' => 2,
81
+ 'fullname' => 'Assets:Cash'
82
+ }, {
83
+ 'total' => '3150.00EUR',
84
+ 'name' => 'Giro',
85
+ 'depth' => 2,
86
+ 'fullname' => 'Assets:Giro'
87
+ }, {
88
+ 'total' => '22.10EUR',
89
+ 'name' => 'Reimbursements',
90
+ 'depth' => 2,
91
+ 'fullname' => 'Assets:Reimbursements'
92
+ }, {
93
+ 'total' => '22.10EUR',
94
+ 'name' => 'Hans Maulwurf',
95
+ 'depth' => 3,
96
+ 'fullname' => 'Assets:Reimbursements:Hans Maulwurf'
97
+ }, {
98
+ 'total' => '-12.00EUR',
99
+ 'name' => 'Liabilities',
100
+ 'depth' => 1,
101
+ 'fullname' => 'Liabilities'
102
+ }, {
103
+ 'total' => '-12.00EUR',
104
+ 'name' => 'Max Mustermann',
105
+ 'depth' => 2,
106
+ 'fullname' => 'Liabilities:Max Mustermann' }
107
+ ],
108
+ 'total' => '3165.80EUR'
109
+ }
110
+ end
111
+
112
+ it 'does not wrap accounts' do
113
+ get '/balance', query: '--flat Assets Liabilities'
114
+ JSON.parse(last_response.body).should deep_eq valid_json
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe '/budget' do
4
+ context 'with query' do
5
+ let(:valid_json) do
6
+ {
7
+ 'accounts' =>
8
+ [
9
+ {
10
+ 'total' => '3177.80EUR',
11
+ 'budget' => '-6600.00EUR',
12
+ 'difference' => '9777.80EUR',
13
+ 'percentage' => '-48%',
14
+ 'account' => 'Assets'
15
+ }, {
16
+ 'total' => '53.20EUR',
17
+ 'budget' => '6600.00EUR',
18
+ 'difference' => '-6546.80EUR',
19
+ 'percentage' => '1%',
20
+ 'account' => 'Expenses'
21
+ }, {
22
+ 'total' => '0',
23
+ 'budget' => '5400.00EUR',
24
+ 'difference' => '-5400.00EUR',
25
+ 'percentage' => '0%',
26
+ 'account' => 'Expenses:Flat'
27
+ }, {
28
+ 'total' => '53.20EUR',
29
+ 'budget' => '1200.00EUR',
30
+ 'difference' => '-1146.80EUR',
31
+ 'percentage' => '4%',
32
+ 'account' => 'Expenses:Restaurants'
33
+ }
34
+ ],
35
+ 'total' => '3231.00EUR',
36
+ 'budget' => '0',
37
+ 'difference' => '3231.00EUR',
38
+ 'percentage' => '0'
39
+ }
40
+ end
41
+
42
+ it 'responds with the right budget' do
43
+ get '/budget'
44
+
45
+ JSON.parse(last_response.body).should deep_eq valid_json
46
+ end
47
+ end
48
+
49
+ context 'querying single account' do
50
+ let(:valid_json) do
51
+ {
52
+ 'accounts' =>
53
+ [
54
+ {
55
+ 'total' => '53.20EUR',
56
+ 'budget' => '100.00EUR',
57
+ 'difference' => '-46.80EUR',
58
+ 'percentage' => '53%',
59
+ 'account' => 'Expenses:Restaurants'
60
+ }
61
+ ]
62
+ }
63
+ end
64
+
65
+ it 'responds with the right budget' do
66
+ get '/budget', query: 'Restaurants'
67
+
68
+ JSON.parse(last_response.body).should deep_eq valid_json
69
+ end
70
+ end
71
+
72
+ context 'querying single account' do
73
+ let(:valid_json) do
74
+ {
75
+ 'accounts' =>
76
+ [
77
+ {
78
+ 'total' => '53.20EUR',
79
+ 'budget' => '550.00EUR',
80
+ 'difference' => '-496.80EUR',
81
+ 'percentage' => '10%',
82
+ 'account' => 'Expenses'
83
+ }, {
84
+ 'total' => '0',
85
+ 'budget' => '450.00EUR',
86
+ 'difference' => '-450.00EUR',
87
+ 'percentage' => '0%',
88
+ 'account' => 'Expenses:Flat'
89
+ }, {
90
+ 'total' => '53.20EUR',
91
+ 'budget' => '100.00EUR',
92
+ 'difference' => '-46.80EUR',
93
+ 'percentage' => '53%',
94
+ 'account' => 'Expenses:Restaurants'
95
+ }
96
+ ],
97
+ 'total' => '53.20EUR',
98
+ 'budget' => '550.00EUR',
99
+ 'difference' => '-496.80EUR',
100
+ 'percentage' => '10%',
101
+ }
102
+ end
103
+
104
+ it 'responds with the right budget' do
105
+ get '/budget', query: 'Expenses'
106
+
107
+ JSON.parse(last_response.body).should deep_eq valid_json
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe '/payees' do
5
+ it 'returns payees' do
6
+ get '/payees'
7
+
8
+ JSON.parse(last_response.body).should =~
9
+ [
10
+ 'Bioladen Tegeler Straße',
11
+ 'Customer X',
12
+ 'NaveenaPath',
13
+ 'Opening Balance',
14
+ 'Shikgoo',
15
+ 'Sparkasse',
16
+ 'Trattoria La Vialla'
17
+ ]
18
+ end
19
+ end
@@ -0,0 +1,233 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe '/register' do
5
+ context 'normal' do
6
+ let(:valid_response) do
7
+ [
8
+ {
9
+ 'date' => '2013/12/01',
10
+ 'effective_date' => '2013/12/02',
11
+ 'code' => nil,
12
+ 'cleared' => true,
13
+ 'pending' => false,
14
+ 'payee' => 'Sparkasse',
15
+ 'postings' =>
16
+ [
17
+ { 'account' => 'Assets:Cash',
18
+ 'amount' => '50',
19
+ 'total' => '50',
20
+ 'commodity' => 'EUR'
21
+ },
22
+ {
23
+ 'account' => 'Assets:Giro',
24
+ 'amount' => '-50',
25
+ 'total' => '-50',
26
+ 'commodity' => 'EUR'
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ end
32
+
33
+ it 'shows all fields' do
34
+ get '/register', query: '-p "2013-12-01"'
35
+ JSON.parse(last_response.body).should deep_eq valid_response
36
+ end
37
+ end
38
+
39
+ context 'daily report' do
40
+ let(:valid_response) do
41
+ [
42
+ {
43
+ 'beginning' => '2013/12/03',
44
+ 'end' => '2013/12/03',
45
+ 'postings' =>
46
+ [
47
+ {
48
+ 'account' => 'Expenses:Restaurants',
49
+ 'amount' => '9',
50
+ 'total' => '9',
51
+ 'commodity' => 'EUR'
52
+ }
53
+ ]
54
+ }, {
55
+ 'beginning' => '2013/12/04',
56
+ 'end' => '2013/12/04',
57
+ 'postings' =>
58
+ [
59
+ {
60
+ 'account' => 'Expenses:Restaurants',
61
+ 'amount' => '32.2',
62
+ 'total' => '32.2',
63
+ 'commodity' => 'EUR'
64
+ }
65
+ ]
66
+ }, {
67
+ 'beginning' => '2013/12/05',
68
+ 'end' => '2013/12/05',
69
+ 'postings' =>
70
+ [
71
+ {
72
+ 'account' => 'Expenses:Restaurants',
73
+ 'amount' => '12',
74
+ 'total' => '12',
75
+ 'commodity' => 'EUR'
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ end
81
+
82
+ it 'shows beginning, end and postings with account, amount and total' do
83
+ get '/register', query: '--daily ^Expenses'
84
+ JSON.parse(last_response.body).should deep_eq valid_response
85
+ end
86
+ end
87
+
88
+ context 'weekly report' do
89
+ let(:valid_response) do
90
+ [
91
+ {
92
+ 'beginning' => '2013/12/01',
93
+ 'end' => '2013/12/07',
94
+ 'postings' =>
95
+ [
96
+ {
97
+ 'account' => 'Expenses:Restaurants',
98
+ 'amount' => '53.2',
99
+ 'total' => '53.2',
100
+ 'commodity' => 'EUR'
101
+ }
102
+ ]
103
+ }
104
+ ]
105
+ end
106
+
107
+ it 'shows beginning, end and postings with account, amount and total' do
108
+ get '/register', query: '-W ^Expenses'
109
+ JSON.parse(last_response.body).should deep_eq valid_response
110
+ end
111
+ end
112
+
113
+ context 'monthly report' do
114
+ let(:valid_response) do
115
+ [
116
+ {
117
+ 'beginning' => '2013/12/01',
118
+ 'end' => '2013/12/31',
119
+ 'postings' =>
120
+ [
121
+ {
122
+ 'account' => 'Expenses:Restaurants',
123
+ 'amount' => '53.2',
124
+ 'total' => '53.2',
125
+ 'commodity' => 'EUR'
126
+ }
127
+ ]
128
+ }
129
+ ]
130
+ end
131
+
132
+ it 'shows beginning, end and postings with account, amount and total' do
133
+ get '/register', query: '-M ^Expenses'
134
+ JSON.parse(last_response.body).should deep_eq valid_response
135
+ end
136
+ end
137
+
138
+ context 'quarterly report' do
139
+ let(:valid_response) do
140
+ [
141
+ {
142
+ 'beginning' => '2013/10/01',
143
+ 'end' => '2013/12/31',
144
+ 'postings' =>
145
+ [
146
+ {
147
+ 'account' => 'Expenses:Restaurants',
148
+ 'amount' => '53.2',
149
+ 'total' => '53.2',
150
+ 'commodity' => 'EUR'
151
+ }
152
+ ]
153
+ }
154
+ ]
155
+ end
156
+
157
+ it 'shows beginning, end and postings with account, amount and total' do
158
+ get '/register', query: '--quarterly ^Expenses'
159
+ JSON.parse(last_response.body).should deep_eq valid_response
160
+ end
161
+ end
162
+
163
+ context 'yearly report' do
164
+ let(:valid_response) do
165
+ [
166
+ {
167
+ 'beginning' => '2013/01/01',
168
+ 'end' => '2013/12/31',
169
+ 'postings' =>
170
+ [
171
+ {
172
+ 'account' => 'Expenses:Restaurants',
173
+ 'amount' => '53.2',
174
+ 'total' => '53.2',
175
+ 'commodity' => 'EUR'
176
+ }
177
+ ]
178
+ }
179
+ ]
180
+ end
181
+
182
+ it 'shows beginning, end and postings with account, amount and total' do
183
+ get '/register', query: '-Y ^Expenses'
184
+ JSON.parse(last_response.body).should deep_eq valid_response
185
+ end
186
+ end
187
+
188
+ context 'report by payee' do
189
+ let(:valid_response) do
190
+ [
191
+ {
192
+ 'payee' => 'NaveenaPath',
193
+ 'postings' =>
194
+ [
195
+ {
196
+ 'account' => 'Expenses:Restaurants',
197
+ 'amount' => '9',
198
+ 'total' => '9',
199
+ 'commodity' => 'EUR'
200
+ }
201
+ ]
202
+ }, {
203
+ 'payee' => 'Shikgoo',
204
+ 'postings' =>
205
+ [
206
+ {
207
+ 'account' => 'Expenses:Restaurants',
208
+ 'amount' => '12',
209
+ 'total' => '12',
210
+ 'commodity' => 'EUR'
211
+ }
212
+ ]
213
+ }, {
214
+ 'payee' => 'Trattoria La Vialla',
215
+ 'postings' =>
216
+ [
217
+ {
218
+ 'account' => 'Expenses:Restaurants',
219
+ 'amount' => '32.2',
220
+ 'total' => '32.2',
221
+ 'commodity' => 'EUR'
222
+ }
223
+ ]
224
+ }
225
+ ]
226
+ end
227
+
228
+ it 'shows beginning, end and postings with account, amount and total' do
229
+ get '/register', query: '--by-payee ^Expenses'
230
+ JSON.parse(last_response.body).should deep_eq valid_response
231
+ end
232
+ end
233
+ end