acts_as_account 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.specification +57 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +31 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/cucumber.yml +1 -0
- data/db/database.yml +15 -0
- data/db/schema.rb +65 -0
- data/env.sh +1 -0
- data/features/account/account_creation.feature +11 -0
- data/features/step_definitions/account_steps.rb +125 -0
- data/features/support/cheque.rb +3 -0
- data/features/support/env.rb +29 -0
- data/features/support/user.rb +3 -0
- data/features/transfer/journal_creation.feature +13 -0
- data/features/transfer/transfer.feature +35 -0
- data/init.rb +1 -0
- data/lib/acts_as_account.rb +16 -0
- data/lib/acts_as_account/account.rb +49 -0
- data/lib/acts_as_account/active_record_extensions.rb +34 -0
- data/lib/acts_as_account/global_account.rb +7 -0
- data/lib/acts_as_account/journal.rb +52 -0
- data/lib/acts_as_account/posting.rb +10 -0
- data/lib/acts_as_account/transfer.rb +24 -0
- metadata +90 -0
data/.specification
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_account
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors: []
|
7
|
+
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-18 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email:
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib
|
26
|
+
- lib/acts_as_account
|
27
|
+
- lib/acts_as_account.rb
|
28
|
+
has_rdoc: true
|
29
|
+
homepage:
|
30
|
+
licenses: []
|
31
|
+
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: "0"
|
42
|
+
version:
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
rubyforge_project:
|
52
|
+
rubygems_version: 1.3.5
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary:
|
56
|
+
test_files: []
|
57
|
+
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 gut.org gAG
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,31 @@
|
|
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.
|
2
|
+
|
3
|
+
== Theory
|
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
|
8
|
+
tables is needed.
|
9
|
+
|
10
|
+
We also hook into the ActionController request cycle to warn the developer
|
11
|
+
if a Request has left the uncommitted changes in the system.
|
12
|
+
|
13
|
+
If you would like to run the cucumber features from the acs_as_account gem, just execute
|
14
|
+
* rake features:create_database
|
15
|
+
* cucumber
|
16
|
+
|
17
|
+
== Links
|
18
|
+
|
19
|
+
* Double Entry Accounting in a Relational Database: http://homepages.tcp.co.uk/~m-wigley/gc_wp_ded.html
|
20
|
+
|
21
|
+
== Todo
|
22
|
+
|
23
|
+
* add transaction isolation tests
|
24
|
+
|
25
|
+
== Credits
|
26
|
+
|
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
|
+
|
29
|
+
== Copyright
|
30
|
+
|
31
|
+
Copyright (c) 2010 gut.org gAG
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "acts_as_account"
|
8
|
+
gem.summary = %Q{acts_as_account implements double entry accounting for Rails models}
|
9
|
+
gem.description = %Q{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.}
|
10
|
+
gem.email = "thieso@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/thieso2/acts_as_account"
|
12
|
+
gem.authors = ["Thies C. Arntzen, Norman Timmler, Matthias Frick, Phillip Oertel"]
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
namespace :features do
|
20
|
+
desc "create test database out of db/schema.rb"
|
21
|
+
task :create_database do
|
22
|
+
require 'rubygems'
|
23
|
+
require 'active_record'
|
24
|
+
access_data = YAML.load_file(File.dirname(__FILE__) + '/db/database.yml')['acts_as_account']
|
25
|
+
conn = ActiveRecord::Base.establish_connection(Hash[access_data.select { |k, v| k != 'database'}]).connection
|
26
|
+
conn.execute('DROP DATABASE IF EXISTS acts_as_account')
|
27
|
+
conn.execute('CREATE DATABASE acts_as_account')
|
28
|
+
conn.execute('USE acts_as_account')
|
29
|
+
load(File.dirname(__FILE__) + '/db/schema.rb')
|
30
|
+
end
|
31
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --require features/support/env.rb
|
data/db/database.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
acts_as_account:
|
2
|
+
adapter: mysql
|
3
|
+
encoding: utf8
|
4
|
+
database: acts_as_account
|
5
|
+
username: root
|
6
|
+
password:
|
7
|
+
host: localhost
|
8
|
+
|
9
|
+
betterplace:
|
10
|
+
adapter: mysql
|
11
|
+
encoding: utf8
|
12
|
+
database: betterplace_production20100222
|
13
|
+
username: root
|
14
|
+
password:
|
15
|
+
host: localhost
|
data/db/schema.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
2
|
+
if ENV['DROP_FK']
|
3
|
+
execute "ALTER TABLE acts_as_account_postings DROP FOREIGN KEY account_id"
|
4
|
+
execute "ALTER TABLE acts_as_account_postings DROP FOREIGN KEY other_account_id"
|
5
|
+
execute "ALTER TABLE acts_as_account_postings DROP FOREIGN KEY journal_id"
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table "acts_as_account_accounts", :force => true do |t|
|
9
|
+
t.integer "holder_id", :null => false
|
10
|
+
t.string "holder_type", :null => false
|
11
|
+
t.string "name", :null => false
|
12
|
+
|
13
|
+
t.integer "balance", :default => 0
|
14
|
+
t.integer "postings_count", :default => 0
|
15
|
+
t.datetime "last_valuta"
|
16
|
+
|
17
|
+
t.datetime "created_at"
|
18
|
+
t.datetime "updated_at"
|
19
|
+
end
|
20
|
+
add_index "acts_as_account_accounts", ["holder_id", "holder_type", "name"], :name => 'account_unique', :unique => true
|
21
|
+
|
22
|
+
create_table "acts_as_account_journals", :force => true do |t|
|
23
|
+
t.datetime "created_at"
|
24
|
+
t.datetime "updated_at"
|
25
|
+
end
|
26
|
+
|
27
|
+
create_table "acts_as_account_postings", :force => true do |t|
|
28
|
+
t.integer "account_id", :null => false
|
29
|
+
t.integer "other_account_id", :null => false
|
30
|
+
t.integer "journal_id", :null => false
|
31
|
+
t.integer "amount", :null => false
|
32
|
+
|
33
|
+
t.integer "reference_id"
|
34
|
+
t.string "reference_type"
|
35
|
+
|
36
|
+
t.datetime "valuta"
|
37
|
+
|
38
|
+
t.datetime "created_at"
|
39
|
+
t.datetime "updated_at"
|
40
|
+
end
|
41
|
+
add_index "acts_as_account_postings", "account_id"
|
42
|
+
add_index "acts_as_account_postings", "journal_id"
|
43
|
+
add_index "acts_as_account_postings", ["reference_type", "reference_id"], :name => "reference"
|
44
|
+
add_index "acts_as_account_postings", ["valuta", "id"], :name => "sort_key"
|
45
|
+
|
46
|
+
execute "ALTER TABLE acts_as_account_postings ADD CONSTRAINT account_id FOREIGN KEY (account_id) REFERENCES acts_as_account_accounts (id)"
|
47
|
+
execute "ALTER TABLE acts_as_account_postings ADD CONSTRAINT other_account_id FOREIGN KEY (other_account_id) REFERENCES acts_as_account_accounts (id)"
|
48
|
+
execute "ALTER TABLE acts_as_account_postings ADD CONSTRAINT journal_id FOREIGN KEY (journal_id) REFERENCES acts_as_account_journals (id)"
|
49
|
+
|
50
|
+
create_table "acts_as_account_global_accounts", :force => true do |t|
|
51
|
+
t.string "name", :null => false
|
52
|
+
end
|
53
|
+
|
54
|
+
create_table "users", :force => true do |t|
|
55
|
+
t.string "name", :null => false
|
56
|
+
t.datetime "created_at"
|
57
|
+
t.datetime "updated_at"
|
58
|
+
end
|
59
|
+
|
60
|
+
create_table "cheques", :force => true do |t|
|
61
|
+
t.string "number"
|
62
|
+
t.datetime "created_at"
|
63
|
+
t.datetime "updated_at"
|
64
|
+
end
|
65
|
+
end
|
data/env.sh
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.8.6-p399@rails235
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Feature: Creating an Account
|
2
|
+
Background: I have a User
|
3
|
+
Given I create a user A
|
4
|
+
|
5
|
+
Scenario: Every Holder should have a default Account
|
6
|
+
Then User A has an Account named default
|
7
|
+
|
8
|
+
Scenario: Creating a second Accounts with the same name for a Holder returns original account
|
9
|
+
Given User A has an Account named default
|
10
|
+
And I create an Account named default for User A
|
11
|
+
Then I get the original account
|
@@ -0,0 +1,125 @@
|
|
1
|
+
include ActsAsAccount
|
2
|
+
|
3
|
+
def german_date_time_to_local(datestring, timestring)
|
4
|
+
Time.local(*(datestring.split(".").reverse + timestring.split(":")).map(&:to_i))
|
5
|
+
end
|
6
|
+
|
7
|
+
Given /^I create a user (\w+)$/ do |name|
|
8
|
+
User.create!(:name => name)
|
9
|
+
end
|
10
|
+
|
11
|
+
Given /^I create a global ([_\w]+) account$/ do |name|
|
12
|
+
Account.for(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
Then /^an account for user (\w+) exists$/ do |name|
|
16
|
+
@account = User.find_by_name(name).account
|
17
|
+
assert_not_nil @account
|
18
|
+
end
|
19
|
+
|
20
|
+
Then /^the account has (\d+) journals?$/ do |num_journals|
|
21
|
+
num_journals = num_journals.to_i
|
22
|
+
|
23
|
+
if num_journals == 1
|
24
|
+
@journal = @account.journals.first
|
25
|
+
else
|
26
|
+
@journals = @account.journals
|
27
|
+
end
|
28
|
+
|
29
|
+
assert_equal num_journals, @account.journals.count
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^the journal has (\d+) postings? with an amount of (\d+) €$/ do |num_postings, amount|
|
33
|
+
@postings = @journal.postings
|
34
|
+
assert_equal num_postings.to_i, @postings.size
|
35
|
+
@postings.each do |posting|
|
36
|
+
assert_equal amount.to_i, posting.amount
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Then /^(\w+)'s account balance is (-?\d+) €$/ do |name, balance|
|
41
|
+
assert_equal balance.to_i, User.find_by_name(name).account.balance
|
42
|
+
end
|
43
|
+
|
44
|
+
Then /^the global (\w+) account balance is (-?\d+) €$/ do |name, balance|
|
45
|
+
assert_equal balance.to_i, Account.for(name).balance
|
46
|
+
end
|
47
|
+
|
48
|
+
When /^I transfer (\d+) € from (\w+)'s account to (\w+)'s account$/ do |amount, from, to|
|
49
|
+
from_account = User.find_by_name(from).account
|
50
|
+
to_account = User.find_by_name(to).account
|
51
|
+
Journal.current.transfer(amount.to_i, from_account, to_account, @reference, @valuta)
|
52
|
+
end
|
53
|
+
|
54
|
+
When /^I transfer (\d+) € from global (\w+) account to global (\w+) account$/ do |amount, from, to|
|
55
|
+
from_account = Account.for(from)
|
56
|
+
to_account = Account.for(to)
|
57
|
+
Journal.current.transfer(amount.to_i, from_account, to_account, @reference, @valuta)
|
58
|
+
end
|
59
|
+
|
60
|
+
Then /^the balance\-sheet should be:$/ do |table|
|
61
|
+
table.hashes.each do |row|
|
62
|
+
assert_equal row['Balance'].to_i, User.find_by_name(row['User']).account.balance
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Then /^I fail with (.+)$/ do |exception|
|
67
|
+
assert_equal exception.constantize, @last_exception.class
|
68
|
+
end
|
69
|
+
|
70
|
+
When /^I create a Journal via (.+)$/ do |method|
|
71
|
+
@last_exception = nil
|
72
|
+
begin
|
73
|
+
eval <<-EOT
|
74
|
+
@journal = Journal.#{method}
|
75
|
+
EOT
|
76
|
+
rescue Exception => @last_exception
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Then /^I have a Journal$/ do
|
81
|
+
assert_equal Journal, @journal.class
|
82
|
+
end
|
83
|
+
|
84
|
+
Then /^User (\w+) has an Account named (\w+)$/ do |user_name, account_name|
|
85
|
+
@account = User.find_by_name(user_name).account
|
86
|
+
assert_equal account_name, @account.name
|
87
|
+
end
|
88
|
+
|
89
|
+
Given /^I create an Account named (\w+) for User (\w+)$/ do |account_name, user_name|
|
90
|
+
user = User.find_by_name(user_name)
|
91
|
+
@created_account = Account.create!(:holder => user, :name => account_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
Then /^I get the original account$/ do
|
95
|
+
assert_equal @account, @created_account
|
96
|
+
end
|
97
|
+
|
98
|
+
Given /I transfer (\d+) € from (\w+)'s account to (\w+)'s account referencing a (\w+) with (\w+) (\w+)$/ do |amount, from, to, reference, name, value|
|
99
|
+
@reference = reference.constantize.create!(name => value)
|
100
|
+
Given "I transfer #{amount} € from #{from}'s account to #{to}'s account"
|
101
|
+
end
|
102
|
+
|
103
|
+
Then /^all postings reference (\w+) with (\w+) (\w+)$/ do |reference_class, name, value|
|
104
|
+
reference = reference_class.constantize.find(:first, :conditions => "#{name} = #{value}")
|
105
|
+
Posting.all.each do |posting|
|
106
|
+
assert_equal reference, posting.reference
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
Given /^I transfer (\d+) € from (\w+)'s account to (\w+)'s account and specify (\S+) (\S+) as the booking time$/ do |amount, from, to, booking_date, booking_time|
|
111
|
+
@valuta = german_date_time_to_local(booking_date, booking_time)
|
112
|
+
Given "I transfer #{amount} € from #{from}'s account to #{to}'s account"
|
113
|
+
end
|
114
|
+
|
115
|
+
Then /^all postings have (\S+) (\S+) as the booking time$/ do |booking_date, booking_time|
|
116
|
+
valuta = german_date_time_to_local(booking_date, booking_time)
|
117
|
+
Posting.all.each do |posting|
|
118
|
+
assert_equal valuta, posting.valuta
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
Then /^(\w+) with (\w+) (\w+) references all postings$/ do |reference_class, name, value|
|
123
|
+
reference = reference_class.constantize.find(:first, :conditions => "#{name} = #{value}")
|
124
|
+
assert_equal Posting.all, reference.postings
|
125
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'mysql'
|
4
|
+
gem 'activerecord'
|
5
|
+
gem 'actionpack'
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
require 'test/unit/assertions'
|
9
|
+
|
10
|
+
World(Test::Unit::Assertions)
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(YAML.load_file(File.dirname(__FILE__) + '/../../db/database.yml')['acts_as_account'])
|
13
|
+
|
14
|
+
require File.dirname(__FILE__) + '/../../lib/acts_as_account'
|
15
|
+
|
16
|
+
#ActiveRecord::Base.logger = Logger.new(STDOUT)
|
17
|
+
|
18
|
+
require 'database_cleaner'
|
19
|
+
require 'database_cleaner/cucumber'
|
20
|
+
DatabaseCleaner.strategy = :transaction
|
21
|
+
|
22
|
+
Dir[File.dirname(__FILE__) + '/../step_definitions/*.rb'].each { |file| require file }
|
23
|
+
|
24
|
+
require File.dirname(__FILE__) + '/user'
|
25
|
+
require File.dirname(__FILE__) + '/cheque'
|
26
|
+
|
27
|
+
After do
|
28
|
+
ActsAsAccount::Journal.clear_current
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Creating a journal
|
2
|
+
|
3
|
+
Scenario: Creating a Journal via new or create is not possible
|
4
|
+
When I create a Journal via new
|
5
|
+
Then I fail with NoMethodError
|
6
|
+
When I create a Journal via create
|
7
|
+
Then I fail with NoMethodError
|
8
|
+
When I create a Journal via create!
|
9
|
+
Then I fail with NoMethodError
|
10
|
+
|
11
|
+
Scenario: Creating a Journal via current
|
12
|
+
When I create a Journal via current
|
13
|
+
Then I have a Journal
|
@@ -0,0 +1,35 @@
|
|
1
|
+
Feature: Transfer
|
2
|
+
In order to transfer money from one account to another
|
3
|
+
As a Bank
|
4
|
+
I want not to loose money
|
5
|
+
|
6
|
+
Scenario: I transfer money between accounts having holders
|
7
|
+
Given I create a user Thies
|
8
|
+
Given I create a user Norman
|
9
|
+
When I transfer 30 € from Thies's account to Norman's account
|
10
|
+
Then Thies's account balance is -30 €
|
11
|
+
And Norman's account balance is 30 €
|
12
|
+
|
13
|
+
Scenario: I transfer money between global accounts
|
14
|
+
Given I create a global wirecard account
|
15
|
+
Given I create a global anonymous_donation account
|
16
|
+
When I transfer 30 € from global wirecard account to global anonymous_donation account
|
17
|
+
Then the global wirecard account balance is -30 €
|
18
|
+
And the global anonymous_donation account balance is 30 €
|
19
|
+
|
20
|
+
Scenario: I transfer money between accounts having a domain object
|
21
|
+
Given I create a user Thies
|
22
|
+
Given I create a user Norman
|
23
|
+
When I transfer 50 € from Thies's account to Norman's account referencing a Cheque with number 8723
|
24
|
+
Then Thies's account balance is -50 €
|
25
|
+
And Norman's account balance is 50 €
|
26
|
+
And all postings reference Cheque with number 8723
|
27
|
+
And Cheque with number 8723 references all postings
|
28
|
+
|
29
|
+
Scenario: I transfer money between accounts setting the booking time
|
30
|
+
Given I create a user Thies
|
31
|
+
Given I create a user Norman
|
32
|
+
When I transfer 50 € from Thies's account to Norman's account and specify 22.05.1968 07:45 as the booking time
|
33
|
+
Then Thies's account balance is -50 €
|
34
|
+
And Norman's account balance is 50 €
|
35
|
+
And all postings have 22.05.1968 07:45 as the booking time
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'acts_as_account'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'action_controller'
|
3
|
+
|
4
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'acts_as_account/transfer'
|
7
|
+
require 'acts_as_account/account'
|
8
|
+
require 'acts_as_account/journal'
|
9
|
+
require 'acts_as_account/posting'
|
10
|
+
require 'acts_as_account/active_record_extensions'
|
11
|
+
|
12
|
+
ActiveRecord::Base.class_eval do
|
13
|
+
include ActsAsAccount::ActiveRecordExtension
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'acts_as_account/global_account'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActsAsAccount
|
2
|
+
class Account < ActiveRecord::Base
|
3
|
+
set_table_name :acts_as_account_accounts
|
4
|
+
|
5
|
+
belongs_to :holder, :polymorphic => true
|
6
|
+
has_many :postings
|
7
|
+
has_many :journals, :through => :postings
|
8
|
+
|
9
|
+
# TODO: discuss with norman:
|
10
|
+
# validates_presence_of will force an ActiveRecord::find on the object
|
11
|
+
# but we have to create accounts for deleted holder!
|
12
|
+
#
|
13
|
+
# validates_presence_of :holder
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def for(name)
|
17
|
+
GlobalAccount.find_or_create_by_name(name.to_s).account
|
18
|
+
end
|
19
|
+
|
20
|
+
def create!(attributes = nil)
|
21
|
+
find_on_error(attributes) do
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create(attributes = nil)
|
27
|
+
find_on_error(attributes) do
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_on_error(attributes)
|
33
|
+
yield
|
34
|
+
|
35
|
+
# Trying to create a duplicate key on a unique index raises StatementInvalid
|
36
|
+
rescue ActiveRecord::StatementInvalid => e
|
37
|
+
record = if attributes[:holder]
|
38
|
+
attributes[:holder].account(attributes[:name])
|
39
|
+
else
|
40
|
+
find(:first, :conditions => [
|
41
|
+
"holder_type = ? and holder_id = ? and name = ?",
|
42
|
+
attributes[:holder_type], attributes[:holder_id], attributes[:name]
|
43
|
+
])
|
44
|
+
end
|
45
|
+
record || raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActsAsAccount
|
2
|
+
module ActiveRecordExtension
|
3
|
+
def self.included(base) # :nodoc:
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.class_eval do
|
6
|
+
def account(name = :default)
|
7
|
+
__send__("#{name}_account") || __send__("create_#{name}_account", :name => name.to_s)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def has_account(name = :default)
|
15
|
+
has_one "#{name}_account", :conditions => "name = '#{name}'", :class_name => "ActsAsAccount::Account", :as => :holder
|
16
|
+
unless instance_methods.include?('accounts')
|
17
|
+
has_many :accounts, :class_name => "ActsAsAccount::Account", :as => :holder
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_postings
|
22
|
+
has_many :postings, :class_name => "ActsAsAccount::Posting", :as => :reference
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_global_account(name)
|
26
|
+
class_eval <<-EOS
|
27
|
+
def account
|
28
|
+
ActsAsAccount::Account.for(:#{name})
|
29
|
+
end
|
30
|
+
EOS
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActsAsAccount
|
2
|
+
class Journal < ActiveRecord::Base
|
3
|
+
set_table_name :acts_as_account_journals
|
4
|
+
|
5
|
+
has_many :postings
|
6
|
+
has_many :accounts, :through => :postings
|
7
|
+
|
8
|
+
class << self
|
9
|
+
private :new
|
10
|
+
private :create
|
11
|
+
private :create!
|
12
|
+
|
13
|
+
def current
|
14
|
+
Thread.current[:acts_as_account_current] ||= create!
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_current
|
18
|
+
Thread.current[:acts_as_account_current] = nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def transfer(amount, from_account, to_account, reference = nil, valuta = Time.now)
|
23
|
+
transaction do
|
24
|
+
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
|
+
|
26
|
+
# to avoid possible deadlocks we need to ensure that the locking order is always
|
27
|
+
# the same therfore the sort by id.
|
28
|
+
[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)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def add_posting(amount, account, other_account, reference, valuta)
|
37
|
+
posting = postings.build(
|
38
|
+
:amount => amount,
|
39
|
+
:account => account,
|
40
|
+
:other_account => other_account,
|
41
|
+
:reference => reference,
|
42
|
+
:valuta => valuta)
|
43
|
+
|
44
|
+
account.balance += posting.amount
|
45
|
+
account.postings_count += 1
|
46
|
+
account.last_valuta = account.last_valuta ? [account.last_valuta, posting.valuta].max : posting.valuta
|
47
|
+
|
48
|
+
posting.save_without_validation
|
49
|
+
account.save_without_validation
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActsAsAccount
|
2
|
+
class Transfer
|
3
|
+
attr_accessor :amount, :reference, :from, :to, :journal
|
4
|
+
|
5
|
+
def initialize(posting_1, posting_2)
|
6
|
+
@amount, @reference = posting_2.amount, posting_2.reference
|
7
|
+
@from, @to = posting_1.account, posting_2.account
|
8
|
+
@journal = posting_1.journal
|
9
|
+
end
|
10
|
+
|
11
|
+
def referencing_a?(klasse)
|
12
|
+
reference.kind_of?(klasse)
|
13
|
+
end
|
14
|
+
|
15
|
+
def reverse(reference = @reference, valuta = Time.now)
|
16
|
+
@journal.transfer(
|
17
|
+
@amount,
|
18
|
+
@to,
|
19
|
+
@from,
|
20
|
+
reference,
|
21
|
+
valuta)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_account
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 1.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Thies C. Arntzen, Norman Timmler, Matthias Frick, Phillip Oertel
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-12 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
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
|
+
email: thieso@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- .specification
|
32
|
+
- MIT-LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
- Rakefile
|
35
|
+
- VERSION
|
36
|
+
- cucumber.yml
|
37
|
+
- db/database.yml
|
38
|
+
- db/schema.rb
|
39
|
+
- env.sh
|
40
|
+
- features/account/account_creation.feature
|
41
|
+
- features/step_definitions/account_steps.rb
|
42
|
+
- features/support/cheque.rb
|
43
|
+
- features/support/env.rb
|
44
|
+
- features/support/user.rb
|
45
|
+
- features/transfer/journal_creation.feature
|
46
|
+
- features/transfer/transfer.feature
|
47
|
+
- init.rb
|
48
|
+
- lib/acts_as_account.rb
|
49
|
+
- lib/acts_as_account/account.rb
|
50
|
+
- lib/acts_as_account/active_record_extensions.rb
|
51
|
+
- lib/acts_as_account/global_account.rb
|
52
|
+
- lib/acts_as_account/journal.rb
|
53
|
+
- lib/acts_as_account/posting.rb
|
54
|
+
- lib/acts_as_account/transfer.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/thieso2/acts_as_account
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 3
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.3.7
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: acts_as_account implements double entry accounting for Rails models
|
89
|
+
test_files: []
|
90
|
+
|