double_entry 2.0.0.beta4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -2
- data/README.md +93 -45
- data/double_entry.gemspec +4 -4
- data/lib/active_record/locking_extensions.rb +3 -3
- data/lib/double_entry/account.rb +2 -2
- data/lib/double_entry/account_balance.rb +3 -3
- data/lib/double_entry/balance_calculator.rb +5 -5
- data/lib/double_entry/configuration.rb +2 -2
- data/lib/double_entry/line.rb +8 -6
- data/lib/double_entry/locking.rb +2 -2
- data/lib/double_entry/transfer.rb +4 -2
- data/lib/double_entry/validation/account_fixer.rb +1 -1
- data/lib/double_entry/validation/line_check.rb +36 -20
- data/lib/double_entry/version.rb +1 -1
- data/lib/double_entry.rb +20 -19
- data/lib/generators/double_entry/install/templates/initializer.rb +4 -4
- data/lib/generators/double_entry/install/templates/migration.rb +26 -26
- metadata +19 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 496d452f002312016c033d18ba23257b677a07072fdea0d5dfc85f77563a3cc3
|
4
|
+
data.tar.gz: a53729a0facc3b920c70c87050cacd93743cea8f2895974fbddbd32925126807
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 626f6ee8bc46253bec15f21ef9a619d894fc5748dca7aa2a07518032777f0c2313889393f7d8d31f974bdd11c26aa309b329f816e1648915978131e228a9d740
|
7
|
+
data.tar.gz: bf503a7c96e23fcde06a04fc64e38799341a60be9c73443eda3d3ffe2583191f33e3ddfb61acb349e4062b1ae568784a49a0d3bbb54c1efce70ccc966057e395
|
data/CHANGELOG.md
CHANGED
@@ -7,7 +7,51 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
-
[Unreleased]: https://github.com/envato/double_entry/compare/v2.0.0
|
10
|
+
[Unreleased]: https://github.com/envato/double_entry/compare/v2.0.0...HEAD
|
11
|
+
|
12
|
+
## [2.0.0] - 2023-10-25
|
13
|
+
|
14
|
+
### Fixed
|
15
|
+
|
16
|
+
- Ensure LineCheck and AccountFixer can work correctly with unscoped accounts ([#207]).
|
17
|
+
- Fixes for running on Ruby 3 ([#212]).
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
|
21
|
+
- Return `[credit, debit]` from `DoubleEntry.transfer` ([#190]).
|
22
|
+
- Run the test suite against Rails 6.1, 7.0, 7.1, and Ruby 3.1, 3.2 ([#203], [#214], [#217]).
|
23
|
+
- Migrate CI to run on GitHub Actions ([#205])
|
24
|
+
|
25
|
+
### Removed
|
26
|
+
|
27
|
+
- Removed support for Rails < 6.1, and Ruby < 3.0 ([#215], [#217]).
|
28
|
+
|
29
|
+
### Added
|
30
|
+
|
31
|
+
- Add `credit` and `debit` scopes to the `Line` model ([#192]).
|
32
|
+
|
33
|
+
[2.0.0]: https://github.com/envato/double_entry/compare/v2.0.0.beta5...v2.0.0
|
34
|
+
[#190]: https://github.com/envato/double_entry/pull/190
|
35
|
+
[#192]: https://github.com/envato/double_entry/pull/192
|
36
|
+
[#203]: https://github.com/envato/double_entry/pull/203
|
37
|
+
[#205]: https://github.com/envato/double_entry/pull/205
|
38
|
+
[#207]: https://github.com/envato/double_entry/pull/207
|
39
|
+
[#212]: https://github.com/envato/double_entry/pull/212
|
40
|
+
[#214]: https://github.com/envato/double_entry/pull/214
|
41
|
+
[#215]: https://github.com/envato/double_entry/pull/215
|
42
|
+
[#217]: https://github.com/envato/double_entry/pull/217
|
43
|
+
|
44
|
+
## [2.0.0.beta5] - 2021-02-24
|
45
|
+
|
46
|
+
### Changed
|
47
|
+
|
48
|
+
- Use the Ruby 1.9 hash syntax ([#182]).
|
49
|
+
- Make the Line detail association optional ([#184]).
|
50
|
+
- Support Ruby 3 ([#196]).
|
51
|
+
|
52
|
+
[#182]: https://github.com/envato/double_entry/pull/182
|
53
|
+
[#184]: https://github.com/envato/double_entry/pull/184
|
54
|
+
[#196]: https://github.com/envato/double_entry/pull/196
|
11
55
|
|
12
56
|
## [2.0.0.beta4] - 2020-01-25
|
13
57
|
|
@@ -25,7 +69,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
25
69
|
|
26
70
|
- Prevent using Ruby 2.2 via restrictions in Gemfile and Gemspec ([#175]).
|
27
71
|
|
28
|
-
[2.0.0.beta4]: https://github.com/envato/double_entry/compare/v2.0.0.beta3...v2.0.0.beta4
|
29
72
|
[#175]: https://github.com/envato/double_entry/pull/175
|
30
73
|
[#176]: https://github.com/envato/double_entry/pull/176
|
31
74
|
[#178]: https://github.com/envato/double_entry/pull/178
|
@@ -462,6 +505,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
462
505
|
|
463
506
|
- Library released as Open Source!
|
464
507
|
|
508
|
+
[2.0.0.beta5]: https://github.com/envato/double_entry/compare/v2.0.0.beta4...v2.0.0.beta5
|
509
|
+
[2.0.0.beta4]: https://github.com/envato/double_entry/compare/v2.0.0.beta3...v2.0.0.beta4
|
465
510
|
[2.0.0.beta3]: https://github.com/envato/double_entry/compare/v2.0.0.beta2...v2.0.0.beta3
|
466
511
|
[2.0.0.beta2]: https://github.com/envato/double_entry/compare/v2.0.0.beta1...v2.0.0.beta2
|
467
512
|
[2.0.0.beta1]: https://github.com/envato/double_entry/compare/v1.0.1...v2.0.0.beta1
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
[![License MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/envato/double_entry/blob/master/LICENSE.md)
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/double_entry.svg)](http://badge.fury.io/rb/double_entry)
|
6
|
-
[![Build Status](https://
|
6
|
+
[![Build Status](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/envato/double_entry/actions/workflows/ci-workflow.yml)
|
7
7
|
[![Code Climate](https://codeclimate.com/github/envato/double_entry/badges/gpa.svg)](https://codeclimate.com/github/envato/double_entry)
|
8
8
|
|
9
9
|
![Show me the Money](http://24.media.tumblr.com/tumblr_m3bwbqNJIG1rrgbmqo1_500.gif)
|
@@ -13,7 +13,7 @@ Keep track of all the monies!
|
|
13
13
|
DoubleEntry is an accounting system based on the principles of a
|
14
14
|
[Double-entry Bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)
|
15
15
|
system. While this gem acts like a double-entry bookkeeping system, as it creates
|
16
|
-
two entries in the database for each transfer, it does *not* enforce accounting rules.
|
16
|
+
two entries in the database for each transfer, it does *not* enforce accounting rules, other than optionally ensuring a balance is positive, and through an allowlist of approved transfers.
|
17
17
|
|
18
18
|
DoubleEntry uses the [Money gem](https://github.com/RubyMoney/money) to encapsulate operations on currency values.
|
19
19
|
|
@@ -22,18 +22,15 @@ DoubleEntry uses the [Money gem](https://github.com/RubyMoney/money) to encapsul
|
|
22
22
|
DoubleEntry is tested against:
|
23
23
|
|
24
24
|
Ruby
|
25
|
-
* 2.3.x
|
26
|
-
* 2.4.x
|
27
|
-
* 2.5.x
|
28
|
-
* 2.6.x
|
29
25
|
* 2.7.x
|
26
|
+
* 3.0.x
|
27
|
+
* 3.1.x
|
28
|
+
* 3.2.x
|
30
29
|
|
31
30
|
Rails
|
32
|
-
* 4.2.x
|
33
|
-
* 5.0.x
|
34
|
-
* 5.1.x
|
35
|
-
* 5.2.x
|
36
31
|
* 6.0.x
|
32
|
+
* 6.1.x
|
33
|
+
* 7.0.x
|
37
34
|
|
38
35
|
Databases
|
39
36
|
* MySQL
|
@@ -72,7 +69,7 @@ rake db:migrate
|
|
72
69
|
## Interface
|
73
70
|
|
74
71
|
The entire API for recording financial transactions is available through a few
|
75
|
-
methods in the
|
72
|
+
methods in the [DoubleEntry](lib/double_entry.rb) module. For full details on
|
76
73
|
what the API provides, please view the documentation on these methods.
|
77
74
|
|
78
75
|
A configuration file should be used to define a set of accounts, and potential
|
@@ -93,12 +90,12 @@ than scoped accounts due to lock contention.
|
|
93
90
|
To get a particular account:
|
94
91
|
|
95
92
|
```ruby
|
96
|
-
account = DoubleEntry.account(:spending, :
|
93
|
+
account = DoubleEntry.account(:spending, scope: user)
|
97
94
|
```
|
98
95
|
|
99
96
|
(This actually returns an Account::Instance object.)
|
100
97
|
|
101
|
-
See
|
98
|
+
See [DoubleEntry::Account](lib/double_entry/account.rb) for more info.
|
102
99
|
|
103
100
|
|
104
101
|
### Balances
|
@@ -119,15 +116,15 @@ To transfer money between accounts:
|
|
119
116
|
```ruby
|
120
117
|
DoubleEntry.transfer(
|
121
118
|
Money.new(20_00),
|
122
|
-
:
|
123
|
-
:
|
124
|
-
:
|
119
|
+
from: one_account,
|
120
|
+
to: another_account,
|
121
|
+
code: :a_business_code_for_this_type_of_transfer,
|
125
122
|
)
|
126
123
|
```
|
127
124
|
|
128
125
|
The possible transfers, and their codes, should be defined in the configuration.
|
129
126
|
|
130
|
-
See
|
127
|
+
See [DoubleEntry::Transfer](lib/double_entry/transfer.rb) for more info.
|
131
128
|
|
132
129
|
### Metadata
|
133
130
|
|
@@ -136,10 +133,10 @@ You may associate arbitrary metadata with transfers, for example:
|
|
136
133
|
```ruby
|
137
134
|
DoubleEntry.transfer(
|
138
135
|
Money.new(20_00),
|
139
|
-
:
|
140
|
-
:
|
141
|
-
:
|
142
|
-
:
|
136
|
+
from: one_account,
|
137
|
+
to: another_account,
|
138
|
+
code: :a_business_code_for_this_type_of_transfer,
|
139
|
+
metadata: {key1: ['value 1', 'value 2'], key2: 'value 3'},
|
143
140
|
)
|
144
141
|
```
|
145
142
|
|
@@ -152,7 +149,7 @@ manually lock the accounts you're using:
|
|
152
149
|
```ruby
|
153
150
|
DoubleEntry.lock_accounts(account_a, account_b) do
|
154
151
|
# Perhaps transfer some money
|
155
|
-
DoubleEntry.transfer(Money.new(20_00), :
|
152
|
+
DoubleEntry.transfer(Money.new(20_00), from: account_a, to: account_b, code: :purchase)
|
156
153
|
# Perform other tasks that should be commited atomically with the transfer of funds...
|
157
154
|
end
|
158
155
|
```
|
@@ -160,8 +157,31 @@ end
|
|
160
157
|
The lock_accounts call generates a database transaction, which must be the
|
161
158
|
outermost transaction.
|
162
159
|
|
163
|
-
See
|
160
|
+
See [DoubleEntry::Locking](lib/double_entry/locking.rb) for more info.
|
164
161
|
|
162
|
+
### Account Checker/Fixer
|
163
|
+
|
164
|
+
DoubleEntry tries really hard to make sure that stored account balances reflect the running balances from the `double_entry_lines` table, but there is always the unlikely possibility that something will go wrong and the calculated balance might get out of sync with the actual running balance of the lines.
|
165
|
+
|
166
|
+
DoubleEntry therefore provides a couple of tools to give you some confidence that things are working as expected.
|
167
|
+
|
168
|
+
The `DoubleEntry::Validation::LineCheck` will check the `double_entry_lines` table to make sure that the `balance` column correctly reflects the calculated running balance, and that the `double_entry_account_balances` table has the correct value in the `balance` column. If either one of these turn out to be incorrect then it will write an entry into the `double_entry_line_checks` table reporting on the differences.
|
169
|
+
|
170
|
+
You can alternatively pass a `fixer` to the `DoubleEntry::Validation::LineCheck.perform` method which will try and correct the balances. This gem provides the `DoubleEntry::Validation::AccountFixer` class which will correct the balance if it's out of sync.
|
171
|
+
|
172
|
+
Using these classes is optional and both are provided for additional safety checks. If you want to make use of them then it's recommended to run them in a scheduled job, somewhere on the order of hourly to daily, depending on transaction volume. Keep in mind that this process locks accounts as it inspects their balances, so it will prevent new transactions from being written for a short time.
|
173
|
+
|
174
|
+
Here are examples that could go in your scheduled job, depending on your needs:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
# Check all accounts & write the results to the double_entry_line_checks table
|
178
|
+
DoubleEntry::Validation::LineCheck.perform!
|
179
|
+
|
180
|
+
# Check & fix accounts (results will also be written to the table)
|
181
|
+
DoubleEntry::Validation::LineCheck.perform!(fixer: DoubleEntry::Validation::AccountFixer.new)
|
182
|
+
```
|
183
|
+
|
184
|
+
See [DoubleEntry::Validation](lib/double_entry/validation) for more info.
|
165
185
|
|
166
186
|
## Implementation
|
167
187
|
|
@@ -173,7 +193,7 @@ Lines table entries also store the running balance for the account. To retrieve
|
|
173
193
|
the current balance for an account, we find the most recent lines table entry
|
174
194
|
for it.
|
175
195
|
|
176
|
-
See
|
196
|
+
See [DoubleEntry::Line](lib/double_entry/line.rb) for more info.
|
177
197
|
|
178
198
|
AccountBalance records cache the current balance for each Account, and are used
|
179
199
|
to perform database level locking.
|
@@ -206,13 +226,13 @@ DoubleEntry.configure do |config|
|
|
206
226
|
raise 'not a User' unless user.class.name == 'User'
|
207
227
|
user.id
|
208
228
|
end
|
209
|
-
accounts.define(:
|
210
|
-
accounts.define(:
|
229
|
+
accounts.define(identifier: :savings, scope_identifier: user_scope, positive_only: true)
|
230
|
+
accounts.define(identifier: :checking, scope_identifier: user_scope)
|
211
231
|
end
|
212
232
|
|
213
233
|
config.define_transfers do |transfers|
|
214
|
-
transfers.define(:
|
215
|
-
transfers.define(:
|
234
|
+
transfers.define(from: :checking, to: :savings, code: :deposit)
|
235
|
+
transfers.define(from: :savings, to: :checking, code: :withdraw)
|
216
236
|
end
|
217
237
|
end
|
218
238
|
```
|
@@ -225,11 +245,25 @@ Transfers between accounts of different currencies are not allowed.
|
|
225
245
|
```ruby
|
226
246
|
DoubleEntry.configure do |config|
|
227
247
|
config.define_accounts do |accounts|
|
228
|
-
accounts.define(:
|
248
|
+
accounts.define(identifier: :savings, scope_identifier: user_scope, currency: 'AUD')
|
229
249
|
end
|
230
250
|
end
|
231
251
|
```
|
232
252
|
|
253
|
+
## Testing with RSpec
|
254
|
+
|
255
|
+
Transfering money needs to be run as a top level transaction. This conflicts with RSpec's default behavior of creating a new transaction for every test, causing an exception of type `DoubleEntry::Locking::LockMustBeOutermostTransaction` to be raised. This behavior may be disabled by adding the following lines into your `rails_helper.rb`.
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
RSpec.configure do |config|
|
259
|
+
# ...
|
260
|
+
# This first line should already be there. You will need to add the second one
|
261
|
+
config.use_transactional_fixtures = true
|
262
|
+
DoubleEntry::Locking.configuration.running_inside_transactional_fixtures = true
|
263
|
+
# ...
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
233
267
|
## Jackhammer
|
234
268
|
|
235
269
|
Run a concurrency test on the code.
|
@@ -277,42 +311,56 @@ See the Github project [issues](https://github.com/envato/double_entry/issues).
|
|
277
311
|
|
278
312
|
## Development Environment Setup
|
279
313
|
|
280
|
-
|
314
|
+
We're using Docker to provide a convenient and consistent environment for
|
315
|
+
executing tests during development. This allows engineers to quickly set up
|
316
|
+
a productive development environment.
|
281
317
|
|
282
|
-
|
283
|
-
|
284
|
-
|
318
|
+
Note: Most development files are mounted in the Docker container. This
|
319
|
+
enables engineers to edit files in their favourite editor (on the host
|
320
|
+
OS) and have the changes immediately available in the Docker container
|
321
|
+
to be exercised.
|
322
|
+
|
323
|
+
One exception to this is the RSpec configuration. Changes to these files will
|
324
|
+
require a rebuild of the Docker image (step 2).
|
325
|
+
|
326
|
+
Prerequisites:
|
285
327
|
|
286
|
-
|
328
|
+
* Docker
|
329
|
+
* Docker Compose
|
330
|
+
* Git
|
331
|
+
|
332
|
+
1. Clone this repo.
|
287
333
|
|
288
334
|
```sh
|
289
|
-
|
335
|
+
git clone git@github.com:envato/double_entry.git && cd double_entry
|
290
336
|
```
|
291
337
|
|
292
|
-
|
293
|
-
4. Create a database in MySQL.
|
338
|
+
2. Build the Docker image we'll use to run tests
|
294
339
|
|
295
340
|
```sh
|
296
|
-
|
341
|
+
docker-compose build --pull double_entry
|
297
342
|
```
|
298
343
|
|
299
|
-
|
344
|
+
3. Startup a container and attach a terminal. This will also start up a
|
345
|
+
MySQL and Postgres database.
|
300
346
|
|
301
347
|
```sh
|
302
|
-
|
348
|
+
docker-compose run --rm double_entry ash
|
303
349
|
```
|
304
350
|
|
305
|
-
|
351
|
+
4. Run the tests
|
306
352
|
|
307
353
|
```sh
|
308
|
-
|
309
|
-
|
354
|
+
DB=mysql bundle exec rspec
|
355
|
+
DB=postgres bundle exec rspec
|
356
|
+
DB=sqlite bundle exec rspec
|
310
357
|
```
|
311
358
|
|
312
|
-
|
359
|
+
5. When finished, exit the container terminal and shut down the databases.
|
313
360
|
|
314
361
|
```sh
|
315
|
-
|
362
|
+
exit
|
363
|
+
docker-compose down
|
316
364
|
```
|
317
365
|
|
318
366
|
## Contributors
|
data/double_entry.gemspec
CHANGED
@@ -24,12 +24,12 @@ Gem::Specification.new do |gem|
|
|
24
24
|
f.match(%r{^(?:double_entry.gemspec|README|LICENSE|CHANGELOG|lib/)})
|
25
25
|
end
|
26
26
|
gem.require_paths = ['lib']
|
27
|
-
gem.required_ruby_version = '>=
|
27
|
+
gem.required_ruby_version = '>= 3'
|
28
28
|
|
29
|
-
gem.add_dependency 'activerecord', '>=
|
30
|
-
gem.add_dependency 'activesupport', '>=
|
29
|
+
gem.add_dependency 'activerecord', '>= 6.1.0'
|
30
|
+
gem.add_dependency 'activesupport', '>= 6.1.0'
|
31
31
|
gem.add_dependency 'money', '>= 6.0.0'
|
32
|
-
gem.add_dependency 'railties', '>=
|
32
|
+
gem.add_dependency 'railties', '>= 6.1.0'
|
33
33
|
|
34
34
|
gem.add_development_dependency 'mysql2'
|
35
35
|
gem.add_development_dependency 'pg'
|
@@ -18,7 +18,7 @@ module ActiveRecord
|
|
18
18
|
yield
|
19
19
|
rescue ActiveRecord::StatementInvalid => exception
|
20
20
|
if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
|
21
|
-
ActiveSupport::Notifications.publish('deadlock_restart.double_entry', :
|
21
|
+
ActiveSupport::Notifications.publish('deadlock_restart.double_entry', exception: exception)
|
22
22
|
|
23
23
|
raise ActiveRecord::RestartTransaction
|
24
24
|
else
|
@@ -46,7 +46,7 @@ module ActiveRecord
|
|
46
46
|
yield
|
47
47
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotUnique => exception
|
48
48
|
if exception.message =~ /duplicate/i || exception.message =~ /ConstraintException/
|
49
|
-
ActiveSupport::Notifications.publish('duplicate_ignore.double_entry', :
|
49
|
+
ActiveSupport::Notifications.publish('duplicate_ignore.double_entry', exception: exception)
|
50
50
|
|
51
51
|
# Just ignore it...someone else has already created the record.
|
52
52
|
else
|
@@ -63,7 +63,7 @@ module ActiveRecord
|
|
63
63
|
if exception.message =~ /deadlock/i || exception.message =~ /database is locked/i
|
64
64
|
# Somebody else is in the midst of creating the record. We'd better
|
65
65
|
# retry, so we ensure they're done before we move on.
|
66
|
-
ActiveSupport::Notifications.publish('deadlock_retry.double_entry', :
|
66
|
+
ActiveSupport::Notifications.publish('deadlock_retry.double_entry', exception: exception)
|
67
67
|
|
68
68
|
retry
|
69
69
|
else
|
data/lib/double_entry/account.rb
CHANGED
@@ -15,7 +15,7 @@ module DoubleEntry
|
|
15
15
|
# @api private
|
16
16
|
def account(identifier, options = {})
|
17
17
|
account = accounts.find(identifier, (options[:scope].present? || options[:scope_identity].present?))
|
18
|
-
Instance.new(:
|
18
|
+
Instance.new(account: account, scope: options[:scope], scope_identity: options[:scope_identity])
|
19
19
|
end
|
20
20
|
|
21
21
|
# @api private
|
@@ -67,7 +67,7 @@ module DoubleEntry
|
|
67
67
|
|
68
68
|
class Instance
|
69
69
|
attr_reader :account, :scope
|
70
|
-
delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, :
|
70
|
+
delegate :identifier, :scope_identifier, :scoped?, :positive_only, :negative_only, :currency, to: :account
|
71
71
|
|
72
72
|
def initialize(args)
|
73
73
|
@account = args[:account]
|
@@ -8,7 +8,7 @@ module DoubleEntry
|
|
8
8
|
#
|
9
9
|
# Account balances are created on demand when transfers occur.
|
10
10
|
class AccountBalance < ActiveRecord::Base
|
11
|
-
delegate :currency, :
|
11
|
+
delegate :currency, to: :account
|
12
12
|
|
13
13
|
def balance
|
14
14
|
self[:balance] && Money.new(self[:balance], currency)
|
@@ -25,11 +25,11 @@ module DoubleEntry
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def account
|
28
|
-
DoubleEntry.account(self[:account].to_sym, :
|
28
|
+
DoubleEntry.account(self[:account].to_sym, scope_identity: self[:scope])
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.find_by_account(account, options = {})
|
32
|
-
scope = where(:
|
32
|
+
scope = where(scope: account.scope_identity, account: account.identifier.to_s)
|
33
33
|
scope = scope.lock(true) if options[:lock]
|
34
34
|
scope.first
|
35
35
|
end
|
@@ -79,18 +79,18 @@ module DoubleEntry
|
|
79
79
|
# @api private
|
80
80
|
class RelationBuilder
|
81
81
|
attr_reader :options
|
82
|
-
delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, :
|
82
|
+
delegate :account, :scope, :scope?, :from, :to, :between?, :at, :at?, :codes, :code?, to: :options
|
83
83
|
|
84
84
|
def initialize(options)
|
85
85
|
@options = options
|
86
86
|
end
|
87
87
|
|
88
88
|
def build
|
89
|
-
lines = Line.where(:
|
89
|
+
lines = Line.where(account: account)
|
90
90
|
lines = lines.where('created_at <= ?', at) if at?
|
91
|
-
lines = lines.where(:
|
92
|
-
lines = lines.where(:
|
93
|
-
lines = lines.where(:
|
91
|
+
lines = lines.where(created_at: from..to) if between?
|
92
|
+
lines = lines.where(code: codes) if code?
|
93
|
+
lines = lines.where(scope: scope) if scope?
|
94
94
|
lines
|
95
95
|
end
|
96
96
|
end
|
@@ -16,7 +16,7 @@ module DoubleEntry
|
|
16
16
|
:scope_identifier_max_length=,
|
17
17
|
:account_identifier_max_length,
|
18
18
|
:account_identifier_max_length=,
|
19
|
-
:
|
19
|
+
to: 'DoubleEntry::Account',
|
20
20
|
)
|
21
21
|
|
22
22
|
delegate(
|
@@ -24,7 +24,7 @@ module DoubleEntry
|
|
24
24
|
:transfers=,
|
25
25
|
:code_max_length,
|
26
26
|
:code_max_length=,
|
27
|
-
:
|
27
|
+
to: 'DoubleEntry::Transfer',
|
28
28
|
)
|
29
29
|
|
30
30
|
def define_accounts
|
data/lib/double_entry/line.rb
CHANGED
@@ -55,8 +55,10 @@ module DoubleEntry
|
|
55
55
|
# by account, or account and code, over a particular period.
|
56
56
|
#
|
57
57
|
class Line < ActiveRecord::Base
|
58
|
-
belongs_to :detail, :
|
59
|
-
has_many :metadata, :
|
58
|
+
belongs_to :detail, polymorphic: true, required: false
|
59
|
+
has_many :metadata, class_name: 'DoubleEntry::LineMetadata' unless -> { DoubleEntry.config.json_metadata }
|
60
|
+
scope :credits, -> { where('amount > 0') }
|
61
|
+
scope :debits, -> { where('amount < 0') }
|
60
62
|
scope :with_id_greater_than, ->(id) { where('id > ?', id) }
|
61
63
|
|
62
64
|
def amount
|
@@ -75,12 +77,12 @@ module DoubleEntry
|
|
75
77
|
self[:balance] = (money && money.fractional)
|
76
78
|
end
|
77
79
|
|
78
|
-
def save(
|
80
|
+
def save(**)
|
79
81
|
check_balance_will_remain_valid
|
80
82
|
super
|
81
83
|
end
|
82
84
|
|
83
|
-
def save!(
|
85
|
+
def save!(**)
|
84
86
|
check_balance_will_remain_valid
|
85
87
|
super
|
86
88
|
end
|
@@ -102,7 +104,7 @@ module DoubleEntry
|
|
102
104
|
end
|
103
105
|
|
104
106
|
def account
|
105
|
-
DoubleEntry.account(self[:account].to_sym, :
|
107
|
+
DoubleEntry.account(self[:account].to_sym, scope_identity: scope)
|
106
108
|
end
|
107
109
|
|
108
110
|
def currency
|
@@ -117,7 +119,7 @@ module DoubleEntry
|
|
117
119
|
end
|
118
120
|
|
119
121
|
def partner_account
|
120
|
-
DoubleEntry.account(self[:partner_account].to_sym, :
|
122
|
+
DoubleEntry.account(self[:partner_account].to_sym, scope_identity: partner_scope)
|
121
123
|
end
|
122
124
|
|
123
125
|
def partner
|
data/lib/double_entry/locking.rb
CHANGED
@@ -149,7 +149,7 @@ module DoubleEntry
|
|
149
149
|
# If one or more account balance records don't exist, set
|
150
150
|
# accounts_with_balances to the corresponding accounts, and return false.
|
151
151
|
def grab_locks
|
152
|
-
account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, :
|
152
|
+
account_balances = @accounts.map { |account| AccountBalance.find_by_account(account, lock: true) }
|
153
153
|
|
154
154
|
if account_balances.any?(&:nil?)
|
155
155
|
@accounts_without_balances = @accounts.zip(account_balances).
|
@@ -168,7 +168,7 @@ module DoubleEntry
|
|
168
168
|
# Get the initial balance from the lines table.
|
169
169
|
balance = account.balance
|
170
170
|
# Try to create the balance record, but ignore it if someone else has done it in the meantime.
|
171
|
-
AccountBalance.create_ignoring_duplicates!(:
|
171
|
+
AccountBalance.create_ignoring_duplicates!(account: account, balance: balance)
|
172
172
|
end
|
173
173
|
end
|
174
174
|
end
|
@@ -76,6 +76,7 @@ module DoubleEntry
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def process(amount, options)
|
79
|
+
credit = debit = nil
|
79
80
|
from_account = options[:from]
|
80
81
|
to_account = options[:to]
|
81
82
|
code = options[:code]
|
@@ -91,6 +92,7 @@ module DoubleEntry
|
|
91
92
|
credit, debit = create_lines(amount, code, detail, from_account, to_account, metadata)
|
92
93
|
create_line_metadata(credit, debit, metadata) if metadata && !DoubleEntry.config.json_metadata
|
93
94
|
end
|
95
|
+
[credit, debit]
|
94
96
|
end
|
95
97
|
|
96
98
|
def create_lines(amount, code, detail, from_account, to_account, metadata)
|
@@ -121,8 +123,8 @@ module DoubleEntry
|
|
121
123
|
def create_line_metadata(credit, debit, metadata)
|
122
124
|
metadata.each_pair do |key, value|
|
123
125
|
Array(value).each do |each_value|
|
124
|
-
LineMetadata.create!(:
|
125
|
-
LineMetadata.create!(:
|
126
|
+
LineMetadata.create!(line: credit, key: key, value: each_value)
|
127
|
+
LineMetadata.create!(line: debit, key: key, value: each_value)
|
126
128
|
end
|
127
129
|
end
|
128
130
|
end
|
@@ -34,9 +34,9 @@ module DoubleEntry
|
|
34
34
|
|
35
35
|
unless active_accounts.empty?
|
36
36
|
LineCheck.create!(
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
37
|
+
errors_found: incorrect_accounts.any?,
|
38
|
+
last_line_id: current_line_id,
|
39
|
+
log: log,
|
40
40
|
)
|
41
41
|
end
|
42
42
|
end
|
@@ -48,34 +48,50 @@ module DoubleEntry
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def running_balance_correct?(line, log)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
previous_line = find_previous_line(line.account.identifier.to_s, line.scope, line.id)
|
52
|
+
|
53
|
+
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
|
54
|
+
|
55
|
+
if line.balance != (line.amount + previous_balance)
|
56
|
+
log << line_error_message(line, previous_line, previous_balance)
|
57
|
+
end
|
58
|
+
|
59
|
+
line.balance == previous_balance + line.amount
|
60
|
+
end
|
60
61
|
|
62
|
+
def find_previous_line(identifier, scope, id)
|
61
63
|
# yes, it needs to be find_by_sql, because any other find will be affected
|
62
64
|
# by the find_each call in perform!
|
63
|
-
|
65
|
+
|
66
|
+
if scope.nil?
|
67
|
+
Line.find_by_sql([<<-SQL, identifier, id])
|
68
|
+
SELECT * FROM #{Line.quoted_table_name} #{force_index}
|
69
|
+
WHERE account = ?
|
70
|
+
AND scope IS NULL
|
71
|
+
AND id < ?
|
72
|
+
ORDER BY id DESC
|
73
|
+
LIMIT 1
|
74
|
+
SQL
|
75
|
+
else
|
76
|
+
Line.find_by_sql([<<-SQL, identifier, scope, id])
|
64
77
|
SELECT * FROM #{Line.quoted_table_name} #{force_index}
|
65
78
|
WHERE account = ?
|
66
79
|
AND scope = ?
|
67
80
|
AND id < ?
|
68
81
|
ORDER BY id DESC
|
69
82
|
LIMIT 1
|
70
|
-
|
71
|
-
|
72
|
-
previous_balance = previous_line.length == 1 ? previous_line[0].balance : Money.zero(line.account.currency)
|
73
|
-
|
74
|
-
if line.balance != (line.amount + previous_balance)
|
75
|
-
log << line_error_message(line, previous_line, previous_balance)
|
83
|
+
SQL
|
76
84
|
end
|
85
|
+
end
|
77
86
|
|
78
|
-
|
87
|
+
def force_index
|
88
|
+
# Another work around for the MySQL 5.1 query optimiser bug that causes the ORDER BY
|
89
|
+
# on the query to fail in some circumstances, resulting in an old balance being
|
90
|
+
# returned. This was biting us intermittently in spec runs.
|
91
|
+
# See http://bugs.mysql.com/bug.php?id=51431
|
92
|
+
return '' unless Line.connection.adapter_name.match(/mysql/i)
|
93
|
+
|
94
|
+
'FORCE INDEX (lines_scope_account_id_idx)'
|
79
95
|
end
|
80
96
|
|
81
97
|
def line_error_message(line, previous_line, previous_balance)
|
data/lib/double_entry/version.rb
CHANGED
data/lib/double_entry.rb
CHANGED
@@ -64,11 +64,11 @@ module DoubleEntry
|
|
64
64
|
# @example Transfer $20 from a user's checking to savings account
|
65
65
|
# checking_account = DoubleEntry.account(:checking, scope: user)
|
66
66
|
# savings_account = DoubleEntry.account(:savings, scope: user)
|
67
|
-
# DoubleEntry.transfer(
|
67
|
+
# credit, debit = DoubleEntry.transfer(
|
68
68
|
# 20.dollars,
|
69
|
-
# :
|
70
|
-
# :
|
71
|
-
# :
|
69
|
+
# from: checking_account,
|
70
|
+
# to: savings_account,
|
71
|
+
# code: :save,
|
72
72
|
# )
|
73
73
|
# @param [Money] amount The quantity of money to transfer from one account
|
74
74
|
# to the other.
|
@@ -80,6 +80,7 @@ module DoubleEntry
|
|
80
80
|
# type of transfer. As specified in the transfer configuration.
|
81
81
|
# @option options :detail [ActiveRecord::Base] ActiveRecord model
|
82
82
|
# associated (via a polymorphic association) with the transfer.
|
83
|
+
# @return [[Line, Line]] The credit & debit (in that order) created by the transfer
|
83
84
|
# @raise [DoubleEntry::TransferIsNegative] The amount is less than zero.
|
84
85
|
# @raise [DoubleEntry::TransferNotAllowed] A transfer between these
|
85
86
|
# accounts with the provided code is not allowed. Check configuration.
|
@@ -91,35 +92,35 @@ module DoubleEntry
|
|
91
92
|
# Get the current or historic balance of an account.
|
92
93
|
#
|
93
94
|
# @example Obtain the current balance of my checking account
|
94
|
-
# checking_account = DoubleEntry.account(:checking, :
|
95
|
+
# checking_account = DoubleEntry.account(:checking, scope: user)
|
95
96
|
# DoubleEntry.balance(checking_account)
|
96
97
|
# @example Obtain the current balance of my checking account (without account or user model)
|
97
|
-
# DoubleEntry.balance(:checking, :
|
98
|
+
# DoubleEntry.balance(:checking, scope: user_id)
|
98
99
|
# @example Obtain a historic balance of my checking account
|
99
|
-
# checking_account = DoubleEntry.account(:checking, :
|
100
|
-
# DoubleEntry.balance(checking_account, :
|
100
|
+
# checking_account = DoubleEntry.account(:checking, scope: user)
|
101
|
+
# DoubleEntry.balance(checking_account, at: Time.new(2012, 1, 1))
|
101
102
|
# @example Obtain the net balance of my checking account during may
|
102
|
-
# checking_account = DoubleEntry.account(:checking, :
|
103
|
+
# checking_account = DoubleEntry.account(:checking, scope: user)
|
103
104
|
# DoubleEntry.balance(
|
104
105
|
# checking_account,
|
105
|
-
# :
|
106
|
-
# :
|
106
|
+
# from: Time.new(2012, 5, 1, 0, 0, 0),
|
107
|
+
# to: Time.new(2012, 5, 31, 23, 59, 59),
|
107
108
|
# )
|
108
109
|
# @example Obtain the balance of salary deposits made to my checking account during may
|
109
|
-
# checking_account = DoubleEntry.account(:checking, :
|
110
|
+
# checking_account = DoubleEntry.account(:checking, scope: user)
|
110
111
|
# DoubleEntry.balance(
|
111
112
|
# checking_account,
|
112
|
-
# :
|
113
|
-
# :
|
114
|
-
# :
|
113
|
+
# code: :salary,
|
114
|
+
# from: Time.new(2012, 5, 1, 0, 0, 0),
|
115
|
+
# to: Time.new(2012, 5, 31, 23, 59, 59),
|
115
116
|
# )
|
116
117
|
# @example Obtain the balance of salary & lottery deposits made to my checking account during may
|
117
|
-
# checking_account = DoubleEntry.account(:checking, :
|
118
|
+
# checking_account = DoubleEntry.account(:checking, scope: user)
|
118
119
|
# DoubleEntry.balance(
|
119
120
|
# checking_account,
|
120
|
-
# :
|
121
|
-
# :
|
122
|
-
# :
|
121
|
+
# codes: [ :salary, :lottery ],
|
122
|
+
# from: Time.new(2012, 5, 1, 0, 0, 0),
|
123
|
+
# to: Time.new(2012, 5, 31, 23, 59, 59),
|
123
124
|
# )
|
124
125
|
# @param [DoubleEntry::Account:Instance, Symbol] account Find the balance
|
125
126
|
# for this account
|
@@ -9,12 +9,12 @@ DoubleEntry.configure do |config|
|
|
9
9
|
# raise 'not a User' unless user.class.name == 'User'
|
10
10
|
# user.id
|
11
11
|
# end
|
12
|
-
# accounts.define(:
|
13
|
-
# accounts.define(:
|
12
|
+
# accounts.define(identifier: :savings, scope_identifier: user_scope, positive_only: true)
|
13
|
+
# accounts.define(identifier: :checking, scope_identifier: user_scope)
|
14
14
|
# end
|
15
15
|
#
|
16
16
|
# config.define_transfers do |transfers|
|
17
|
-
# transfers.define(:
|
18
|
-
# transfers.define(:
|
17
|
+
# transfers.define(from: :checking, to: :savings, code: :deposit)
|
18
|
+
# transfers.define(from: :savings, to: :checking, code: :withdraw)
|
19
19
|
# end
|
20
20
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def self.up
|
3
3
|
create_table "double_entry_account_balances" do |t|
|
4
|
-
t.string "account", :
|
4
|
+
t.string "account", null: false
|
5
5
|
t.string "scope"
|
6
|
-
t.bigint "balance", :
|
7
|
-
t.timestamps :
|
6
|
+
t.bigint "balance", null: false
|
7
|
+
t.timestamps null: false
|
8
8
|
end
|
9
9
|
|
10
|
-
add_index "double_entry_account_balances", ["account"], :
|
11
|
-
add_index "double_entry_account_balances", ["scope", "account"], :
|
10
|
+
add_index "double_entry_account_balances", ["account"], name: "index_account_balances_on_account"
|
11
|
+
add_index "double_entry_account_balances", ["scope", "account"], name: "index_account_balances_on_scope_and_account", unique: true
|
12
12
|
|
13
13
|
create_table "double_entry_lines" do |t|
|
14
|
-
t.string "account", :
|
14
|
+
t.string "account", null: false
|
15
15
|
t.string "scope"
|
16
|
-
t.string "code", :
|
17
|
-
t.bigint "amount", :
|
18
|
-
t.bigint "balance", :
|
19
|
-
t.references "partner", :
|
20
|
-
t.string "partner_account", :
|
16
|
+
t.string "code", null: false
|
17
|
+
t.bigint "amount", null: false
|
18
|
+
t.bigint "balance", null: false
|
19
|
+
t.references "partner", index: false
|
20
|
+
t.string "partner_account", null: false
|
21
21
|
t.string "partner_scope"
|
22
|
-
t.references "detail", :
|
22
|
+
t.references "detail", index: false, polymorphic: true
|
23
23
|
<%- if json_metadata -%>
|
24
24
|
if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
|
25
25
|
t.jsonb "metadata"
|
@@ -27,32 +27,32 @@ class CreateDoubleEntryTables < ActiveRecord::Migration<%= migration_version %>
|
|
27
27
|
t.json "metadata"
|
28
28
|
end
|
29
29
|
<%- end -%>
|
30
|
-
t.timestamps :
|
30
|
+
t.timestamps null: false
|
31
31
|
end
|
32
32
|
|
33
|
-
add_index "double_entry_lines", ["account", "code", "created_at"], :
|
34
|
-
add_index "double_entry_lines", ["account", "created_at"], :
|
35
|
-
add_index "double_entry_lines", ["scope", "account", "created_at"], :
|
36
|
-
add_index "double_entry_lines", ["scope", "account", "id"], :
|
33
|
+
add_index "double_entry_lines", ["account", "code", "created_at"], name: "lines_account_code_created_at_idx"
|
34
|
+
add_index "double_entry_lines", ["account", "created_at"], name: "lines_account_created_at_idx"
|
35
|
+
add_index "double_entry_lines", ["scope", "account", "created_at"], name: "lines_scope_account_created_at_idx"
|
36
|
+
add_index "double_entry_lines", ["scope", "account", "id"], name: "lines_scope_account_id_idx"
|
37
37
|
|
38
38
|
create_table "double_entry_line_checks" do |t|
|
39
|
-
t.references "last_line", :
|
40
|
-
t.boolean "errors_found", :
|
39
|
+
t.references "last_line", null: false, index: false
|
40
|
+
t.boolean "errors_found", null: false
|
41
41
|
t.text "log"
|
42
|
-
t.timestamps :
|
42
|
+
t.timestamps null: false
|
43
43
|
end
|
44
44
|
|
45
|
-
add_index "double_entry_line_checks", ["created_at", "last_line_id"], :
|
45
|
+
add_index "double_entry_line_checks", ["created_at", "last_line_id"], name: "line_checks_created_at_last_line_id_idx"
|
46
46
|
<%- unless json_metadata -%>
|
47
47
|
|
48
48
|
create_table "double_entry_line_metadata" do |t|
|
49
|
-
t.references "line", :
|
50
|
-
t.string "key", :
|
51
|
-
t.string "value", :
|
52
|
-
t.timestamps :
|
49
|
+
t.references "line", null: false, index: false
|
50
|
+
t.string "key", null: false
|
51
|
+
t.string "value", null: false
|
52
|
+
t.timestamps null: false
|
53
53
|
end
|
54
54
|
|
55
|
-
add_index "double_entry_line_metadata", ["line_id", "key", "value"], :
|
55
|
+
add_index "double_entry_line_metadata", ["line_id", "key", "value"], name: "lines_meta_line_id_key_value_idx"
|
56
56
|
<%- end -%>
|
57
57
|
end
|
58
58
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: double_entry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Envato
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 6.1.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 6.1.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: money
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 6.1.0
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 6.1.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: mysql2
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -220,7 +220,7 @@ dependencies:
|
|
220
220
|
- - ">="
|
221
221
|
- !ruby/object:Gem::Version
|
222
222
|
version: '0'
|
223
|
-
description:
|
223
|
+
description:
|
224
224
|
email:
|
225
225
|
- rubygems@envato.com
|
226
226
|
executables: []
|
@@ -256,10 +256,10 @@ licenses:
|
|
256
256
|
- MIT
|
257
257
|
metadata:
|
258
258
|
bug_tracker_uri: https://github.com/envato/double_entry/issues
|
259
|
-
changelog_uri: https://github.com/envato/double_entry/blob/v2.0.0
|
260
|
-
documentation_uri: https://www.rubydoc.info/gems/double_entry/2.0.0
|
261
|
-
source_code_uri: https://github.com/envato/double_entry/tree/v2.0.0
|
262
|
-
post_install_message:
|
259
|
+
changelog_uri: https://github.com/envato/double_entry/blob/v2.0.0/CHANGELOG.md
|
260
|
+
documentation_uri: https://www.rubydoc.info/gems/double_entry/2.0.0
|
261
|
+
source_code_uri: https://github.com/envato/double_entry/tree/v2.0.0
|
262
|
+
post_install_message:
|
263
263
|
rdoc_options: []
|
264
264
|
require_paths:
|
265
265
|
- lib
|
@@ -267,15 +267,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
267
267
|
requirements:
|
268
268
|
- - ">="
|
269
269
|
- !ruby/object:Gem::Version
|
270
|
-
version:
|
270
|
+
version: '3'
|
271
271
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
272
272
|
requirements:
|
273
|
-
- - "
|
273
|
+
- - ">="
|
274
274
|
- !ruby/object:Gem::Version
|
275
|
-
version:
|
275
|
+
version: '0'
|
276
276
|
requirements: []
|
277
|
-
rubygems_version: 3.
|
278
|
-
signing_key:
|
277
|
+
rubygems_version: 3.4.21
|
278
|
+
signing_key:
|
279
279
|
specification_version: 4
|
280
280
|
summary: Tools to build your double entry financial ledger
|
281
281
|
test_files: []
|