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.
File without changes
data/README.rdoc CHANGED
@@ -1,16 +1,21 @@
1
- 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.
1
+ = acts_as_account
2
2
 
3
3
  == Theory
4
4
 
5
- Acts as account hooks into ActiveRecord and allows to add accounts to any model by
6
- simply adding "has_account" to your model. Because the accounts
7
- are connected via a has_many relation no migration to the account-hoder
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 Request has left the uncommitted changes in the system.
14
+ if a request has left the uncommitted changes in the system.
15
+
16
+ == How to test
12
17
 
13
- If you would like to run the cucumber features from the acs_as_account gem, just execute
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 (http://github.com/unnu).
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.1.5
data/db/database.yml CHANGED
@@ -5,11 +5,3 @@ acts_as_account:
5
5
  username: root
6
6
  password:
7
7
  host: localhost
8
-
9
- betterplace:
10
- adapter: mysql
11
- encoding: utf8
12
- database: betterplace_production20100222
13
- username: root
14
- password:
15
- host: localhost
@@ -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 (\d+) € from (\w+)'s account to (\w+)'s account$/ do |amount, from, to|
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
@@ -1,3 +1,3 @@
1
1
  class Cheque < ActiveRecord::Base
2
- has_postings
2
+ is_reference
3
3
  end
@@ -1,5 +1,4 @@
1
1
  require 'rubygems'
2
-
3
2
  gem 'mysql'
4
3
  gem 'activerecord'
5
4
  gem 'actionpack'
@@ -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'
@@ -13,4 +13,5 @@ ActiveRecord::Base.class_eval do
13
13
  include ActsAsAccount::ActiveRecordExtension
14
14
  end
15
15
 
16
- require 'acts_as_account/global_account'
16
+ require 'acts_as_account/global_account'
17
+ require 'acts_as_account/manually_created_account'
@@ -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 has_postings
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 * -1, from_account, to_account, reference, valuta)
31
- add_posting(amount, to_account, from_account, reference, valuta)
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
- account.last_valuta = account.last_valuta ? [account.last_valuta, posting.valuta].max : posting.valuta
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
@@ -0,0 +1,9 @@
1
+ module ActsAsAccount
2
+ class ManuallyCreatedAccount < ActiveRecord::Base
3
+ set_table_name :acts_as_account_manually_created_accounts
4
+
5
+ has_account
6
+
7
+ validates_length_of :name, :minimum => 1
8
+ end
9
+ 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, valuta = Time.now)
16
+ def reverse(valuta = Time.now, reference = @reference, amount = @amount)
16
17
  @journal.transfer(
17
- @amount,
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: 17
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 1
9
- - 1
10
- version: 1.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-12 00:00:00 +01:00
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
- - MIT-LICENSE
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