acts_as_account 1.1.1 → 1.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/{MIT-LICENSE → LICENSE} +0 -0
- data/README.rdoc +14 -13
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/db/database.yml +0 -8
- data/features/step_definitions/account_steps.rb +9 -1
- data/features/support/cheque.rb +1 -1
- data/features/support/env.rb +0 -1
- data/features/transfer/transfer.feature +9 -0
- data/init.rb +1 -1
- data/lib/acts_as_account.rb +2 -1
- data/lib/acts_as_account/account.rb +43 -1
- data/lib/acts_as_account/active_record_extensions.rb +6 -1
- data/lib/acts_as_account/journal.rb +21 -5
- data/lib/acts_as_account/manually_created_account.rb +9 -0
- data/lib/acts_as_account/posting.rb +5 -0
- data/lib/acts_as_account/transfer.rb +4 -3
- metadata +50 -8
- data/env.sh +0 -1
data/{MIT-LICENSE → LICENSE}
RENAMED
File without changes
|
data/README.rdoc
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
acts_as_account
|
1
|
+
= acts_as_account
|
2
2
|
|
3
3
|
== Theory
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
ActsAsAccount implements a "Double Entry Accounting" system for your
|
6
|
+
Rails-models.
|
7
|
+
|
8
|
+
It hooks into ActiveRecord and allows to add accounts to any model by
|
9
|
+
simply means of adding "has_account" to your model. Because the accounts
|
10
|
+
are connected via a has_many relation no migration to the account-holder
|
8
11
|
tables is needed.
|
9
12
|
|
10
13
|
We also hook into the ActionController request cycle to warn the developer
|
11
|
-
if a
|
14
|
+
if a request has left the uncommitted changes in the system.
|
15
|
+
|
16
|
+
== How to test
|
12
17
|
|
13
|
-
|
18
|
+
Run the cucumber features from the acs_as_account gem, just execute
|
14
19
|
* rake features:create_database
|
15
20
|
* cucumber
|
16
21
|
|
@@ -18,14 +23,10 @@ If you would like to run the cucumber features from the acs_as_account gem, just
|
|
18
23
|
|
19
24
|
* Double Entry Accounting in a Relational Database: http://homepages.tcp.co.uk/~m-wigley/gc_wp_ded.html
|
20
25
|
|
21
|
-
== Todo
|
22
|
-
|
23
|
-
* add transaction isolation tests
|
24
|
-
|
25
26
|
== Credits
|
26
27
|
|
27
|
-
This gem was written for the payment backend of betterplace.org by Thies C. Arntzen (http://github.com/thieso2) and Norman Timmler (
|
28
|
+
This gem was written for the payment backend of betterplace.org by Thies C. Arntzen (http://github.com/thieso2) and Norman Timmler (github.com/unnu).
|
28
29
|
|
29
30
|
== Copyright
|
30
|
-
|
31
|
-
Copyright (c) 2010 gut.org gAG
|
31
|
+
|
32
|
+
Copyright (c) 2010 gut.org gAG
|
data/Rakefile
CHANGED
@@ -10,6 +10,9 @@ begin
|
|
10
10
|
gem.email = "thieso@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/betterplace/acts_as_account"
|
12
12
|
gem.authors = ["Thies C. Arntzen, Norman Timmler, Matthias Frick, Phillip Oertel"]
|
13
|
+
gem.add_dependency 'activerecord'
|
14
|
+
gem.add_dependency 'actionpack'
|
15
|
+
gem.add_dependency 'database_cleaner'
|
13
16
|
end
|
14
17
|
Jeweler::GemcutterTasks.new
|
15
18
|
rescue LoadError
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.1.
|
1
|
+
1.1.5
|
data/db/database.yml
CHANGED
@@ -45,7 +45,7 @@ Then /^the global (\w+) account balance is (-?\d+) €$/ do |name, balance|
|
|
45
45
|
assert_equal balance.to_i, Account.for(name).balance
|
46
46
|
end
|
47
47
|
|
48
|
-
When /^I transfer (
|
48
|
+
When /^I transfer (-?\d+) € from (\w+)'s account to (\w+)'s account$/ do |amount, from, to|
|
49
49
|
from_account = User.find_by_name(from).account
|
50
50
|
to_account = User.find_by_name(to).account
|
51
51
|
Journal.current.transfer(amount.to_i, from_account, to_account, @reference, @valuta)
|
@@ -122,4 +122,12 @@ end
|
|
122
122
|
Then /^(\w+) with (\w+) (\w+) references all postings$/ do |reference_class, name, value|
|
123
123
|
reference = reference_class.constantize.find(:first, :conditions => "#{name} = #{value}")
|
124
124
|
assert_equal Posting.all, reference.postings
|
125
|
+
end
|
126
|
+
|
127
|
+
Then /^the order of the postings is correct$/ do
|
128
|
+
# make sure we always book "Soll an Haben"
|
129
|
+
Posting.all.in_groups_of(2) do |from, to|
|
130
|
+
assert from.amount < 0
|
131
|
+
assert to.amount > 0
|
132
|
+
end
|
125
133
|
end
|
data/features/support/cheque.rb
CHANGED
data/features/support/env.rb
CHANGED
@@ -9,7 +9,16 @@ Feature: Transfer
|
|
9
9
|
When I transfer 30 € from Thies's account to Norman's account
|
10
10
|
Then Thies's account balance is -30 €
|
11
11
|
And Norman's account balance is 30 €
|
12
|
+
And the order of the postings is correct
|
12
13
|
|
14
|
+
Scenario: I transfer a negative amount between accounts having holders
|
15
|
+
Given I create a user Thies
|
16
|
+
Given I create a user Norman
|
17
|
+
When I transfer -30 € from Thies's account to Norman's account
|
18
|
+
Then Thies's account balance is 30 €
|
19
|
+
And Norman's account balance is -30 €
|
20
|
+
And the order of the postings is correct
|
21
|
+
|
13
22
|
Scenario: I transfer money between global accounts
|
14
23
|
Given I create a global wirecard account
|
15
24
|
Given I create a global anonymous_donation account
|
data/init.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'acts_as_account'
|
1
|
+
require 'acts_as_account'
|
data/lib/acts_as_account.rb
CHANGED
@@ -13,6 +13,34 @@ module ActsAsAccount
|
|
13
13
|
# validates_presence_of :holder
|
14
14
|
|
15
15
|
class << self
|
16
|
+
|
17
|
+
def recalculate_all_balances
|
18
|
+
ActsAsAccount::Account.update_all(:balance => 0, :postings_count => 0, :last_valuta => nil)
|
19
|
+
sql = <<-EOT
|
20
|
+
SELECT
|
21
|
+
account_id as id,
|
22
|
+
count(*) as calculated_postings_count,
|
23
|
+
sum(amount) as calculated_balance,
|
24
|
+
max(valuta) as calculated_valuta
|
25
|
+
FROM
|
26
|
+
acts_as_account_postings
|
27
|
+
GROUP BY
|
28
|
+
account_id
|
29
|
+
HAVING
|
30
|
+
calculated_postings_count > 0
|
31
|
+
EOT
|
32
|
+
|
33
|
+
ActsAsAccount::Account.find_by_sql(sql).each do |account|
|
34
|
+
account.lock!
|
35
|
+
account.update_attributes(
|
36
|
+
:balance => account.calculated_balance,
|
37
|
+
:postings_count => account.calculated_postings_count,
|
38
|
+
:last_valuta => account.calculated_valuta)
|
39
|
+
|
40
|
+
puts "account:#{account.id}, balance:#{account.balance}, postings_count:#{account.postings_count}, last_valuta:#{account.last_valuta}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
16
44
|
def for(name)
|
17
45
|
GlobalAccount.find_or_create_by_name(name.to_s).account
|
18
46
|
end
|
@@ -29,6 +57,16 @@ module ActsAsAccount
|
|
29
57
|
end
|
30
58
|
end
|
31
59
|
|
60
|
+
def delete_account(number)
|
61
|
+
transaction do
|
62
|
+
account = find(number)
|
63
|
+
raise ActiveRecord::ActiveRecordError, "Cannot be deleted" unless account.deleteable?
|
64
|
+
|
65
|
+
account.holder.destroy if [ManuallyCreatedAccount, GlobalAccount].include?(account.holder.class)
|
66
|
+
account.destroy
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
32
70
|
def find_on_error(attributes)
|
33
71
|
yield
|
34
72
|
|
@@ -45,5 +83,9 @@ module ActsAsAccount
|
|
45
83
|
record || raise
|
46
84
|
end
|
47
85
|
end
|
86
|
+
|
87
|
+
def deleteable?
|
88
|
+
postings.empty? && journals.empty?
|
89
|
+
end
|
48
90
|
end
|
49
|
-
end
|
91
|
+
end
|
@@ -18,8 +18,13 @@ module ActsAsAccount
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
21
|
+
def is_reference
|
22
22
|
has_many :postings, :class_name => "ActsAsAccount::Posting", :as => :reference
|
23
|
+
class_eval <<-EOS
|
24
|
+
def booked?
|
25
|
+
postings.any?
|
26
|
+
end
|
27
|
+
EOS
|
23
28
|
end
|
24
29
|
|
25
30
|
def has_global_account(name)
|
@@ -18,17 +18,28 @@ module ActsAsAccount
|
|
18
18
|
Thread.current[:acts_as_account_current] = nil
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
def transfers
|
23
|
+
returning([]) do |transfers|
|
24
|
+
postings.in_groups_of(2) { |postings| transfers << Transfer.new(*postings) }
|
25
|
+
end
|
26
|
+
end
|
21
27
|
|
22
28
|
def transfer(amount, from_account, to_account, reference = nil, valuta = Time.now)
|
23
29
|
transaction do
|
30
|
+
if (amount < 0)
|
31
|
+
# change order if amount is negative
|
32
|
+
amount, from_account, to_account = -amount, to_account, from_account
|
33
|
+
end
|
34
|
+
|
24
35
|
logger.debug { "ActsAsAccount::Journal.transfer amount: #{amount} from:#{from_account.id} to:#{to_account.id} reference:#{reference.class.name}(#{reference.id}) valuta:#{valuta}" } if logger
|
25
36
|
|
26
37
|
# to avoid possible deadlocks we need to ensure that the locking order is always
|
27
38
|
# the same therfore the sort by id.
|
28
39
|
[from_account, to_account].sort_by(&:id).map(&:lock!)
|
29
|
-
|
30
|
-
add_posting(amount
|
31
|
-
add_posting(amount,
|
40
|
+
|
41
|
+
add_posting(-amount, from_account, to_account, reference, valuta)
|
42
|
+
add_posting( amount, to_account, from_account, reference, valuta)
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
@@ -43,8 +54,13 @@ module ActsAsAccount
|
|
43
54
|
|
44
55
|
account.balance += posting.amount
|
45
56
|
account.postings_count += 1
|
46
|
-
|
47
|
-
|
57
|
+
|
58
|
+
# The last valuta will be the most recent date btw this posting valuta (payment.created_at)
|
59
|
+
# and the last valuta.
|
60
|
+
# TODO 2010-05-20 jm: Ask Holger how the last valuta could be more recent than the payment ???
|
61
|
+
|
62
|
+
|
63
|
+
# TODO 2010-05-20 jm: Ask Holger with saving *without* validations ???
|
48
64
|
posting.save_without_validation
|
49
65
|
account.save_without_validation
|
50
66
|
end
|
@@ -6,5 +6,10 @@ module ActsAsAccount
|
|
6
6
|
belongs_to :other_account, :class_name => 'Account'
|
7
7
|
belongs_to :journal
|
8
8
|
belongs_to :reference, :polymorphic => true
|
9
|
+
|
10
|
+
named_scope :soll, :conditions => 'amount >= 0'
|
11
|
+
named_scope :haben, :conditions => 'amount < 0'
|
12
|
+
named_scope :start_date, lambda{|date| {:conditions => ['DATE(valuta) >= ?', date]}}
|
13
|
+
named_scope :end_date, lambda{|date| {:conditions => ['DATE(valuta) <= ?', date]}}
|
9
14
|
end
|
10
15
|
end
|
@@ -1,20 +1,21 @@
|
|
1
1
|
module ActsAsAccount
|
2
2
|
class Transfer
|
3
|
-
attr_accessor :amount, :reference, :from, :to, :journal
|
3
|
+
attr_accessor :amount, :reference, :from, :to, :journal, :valuta
|
4
4
|
|
5
5
|
def initialize(posting_1, posting_2)
|
6
6
|
@amount, @reference = posting_2.amount, posting_2.reference
|
7
7
|
@from, @to = posting_1.account, posting_2.account
|
8
8
|
@journal = posting_1.journal
|
9
|
+
@valuta = posting_1.valuta
|
9
10
|
end
|
10
11
|
|
11
12
|
def referencing_a?(klasse)
|
12
13
|
reference.kind_of?(klasse)
|
13
14
|
end
|
14
15
|
|
15
|
-
def reverse(reference = @reference,
|
16
|
+
def reverse(valuta = Time.now, reference = @reference, amount = @amount)
|
16
17
|
@journal.transfer(
|
17
|
-
|
18
|
+
amount,
|
18
19
|
@to,
|
19
20
|
@from,
|
20
21
|
reference,
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_account
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 1.1.
|
9
|
+
- 5
|
10
|
+
version: 1.1.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Thies C. Arntzen, Norman Timmler, Matthias Frick, Phillip Oertel
|
@@ -15,10 +15,51 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-11-
|
18
|
+
date: 2010-11-15 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
|
-
dependencies:
|
21
|
-
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: actionpack
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: database_cleaner
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
22
63
|
description: acts_as_account implements double entry accounting for Rails models. Your models get accounts and you can do consistent transactions between them. Since the documentation is sparse, see the transfer.feature for usage examples.
|
23
64
|
email: thieso@gmail.com
|
24
65
|
executables: []
|
@@ -26,17 +67,17 @@ executables: []
|
|
26
67
|
extensions: []
|
27
68
|
|
28
69
|
extra_rdoc_files:
|
70
|
+
- LICENSE
|
29
71
|
- README.rdoc
|
30
72
|
files:
|
31
73
|
- .specification
|
32
|
-
-
|
74
|
+
- LICENSE
|
33
75
|
- README.rdoc
|
34
76
|
- Rakefile
|
35
77
|
- VERSION
|
36
78
|
- cucumber.yml
|
37
79
|
- db/database.yml
|
38
80
|
- db/schema.rb
|
39
|
-
- env.sh
|
40
81
|
- features/account/account_creation.feature
|
41
82
|
- features/step_definitions/account_steps.rb
|
42
83
|
- features/support/cheque.rb
|
@@ -50,6 +91,7 @@ files:
|
|
50
91
|
- lib/acts_as_account/active_record_extensions.rb
|
51
92
|
- lib/acts_as_account/global_account.rb
|
52
93
|
- lib/acts_as_account/journal.rb
|
94
|
+
- lib/acts_as_account/manually_created_account.rb
|
53
95
|
- lib/acts_as_account/posting.rb
|
54
96
|
- lib/acts_as_account/transfer.rb
|
55
97
|
has_rdoc: true
|
data/env.sh
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
rvm use ruby-1.8.6-p399@rails235
|