double_double 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +38 -0
- data/Rakefile +10 -0
- data/double_double.gemspec +27 -0
- data/lib/double_double/account.rb +85 -0
- data/lib/double_double/amount.rb +33 -0
- data/lib/double_double/asset.rb +25 -0
- data/lib/double_double/credit_amount.rb +9 -0
- data/lib/double_double/debit_amount.rb +9 -0
- data/lib/double_double/equity.rb +26 -0
- data/lib/double_double/expense.rb +26 -0
- data/lib/double_double/liability.rb +25 -0
- data/lib/double_double/revenue.rb +26 -0
- data/lib/double_double/transaction.rb +88 -0
- data/lib/double_double/transaction_type.rb +13 -0
- data/lib/double_double/version.rb +3 -0
- data/lib/double_double.rb +21 -0
- data/spec/factories/account_factory.rb +27 -0
- data/spec/factories/amount_factory.rb +10 -0
- data/spec/factories/transaction_factory.rb +9 -0
- data/spec/factories/transaction_type_factory.rb +14 -0
- data/spec/models/account_spec.rb +70 -0
- data/spec/models/amount_spec.rb +9 -0
- data/spec/models/asset_spec.rb +12 -0
- data/spec/models/credit_amount_spec.rb +46 -0
- data/spec/models/debit_amount_spec.rb +46 -0
- data/spec/models/equity_spec.rb +12 -0
- data/spec/models/expense_spec.rb +12 -0
- data/spec/models/liability_spec.rb +12 -0
- data/spec/models/revenue_spec.rb +12 -0
- data/spec/models/transaction_spec.rb +101 -0
- data/spec/spec_helper.rb +87 -0
- data/spec/support/all_account_types.rb +50 -0
- data/spec/support/left_side_account_types.rb +125 -0
- data/spec/support/right_side_account_types.rb +125 -0
- metadata +216 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
module DoubleDouble
|
2
|
+
|
3
|
+
class TransactionType < ActiveRecord::Base
|
4
|
+
self.table_name = 'double_double_transaction_types'
|
5
|
+
|
6
|
+
has_many :transactions
|
7
|
+
attr_accessible :number, :description
|
8
|
+
|
9
|
+
validates_numericality_of :number, greater_than: 0
|
10
|
+
validates_length_of :description, minimum: 6
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'money'
|
3
|
+
|
4
|
+
require 'double_double/version'
|
5
|
+
|
6
|
+
# Accounts
|
7
|
+
require 'double_double/account'
|
8
|
+
require 'double_double/asset'
|
9
|
+
require 'double_double/equity'
|
10
|
+
require 'double_double/expense'
|
11
|
+
require 'double_double/liability'
|
12
|
+
require 'double_double/revenue'
|
13
|
+
|
14
|
+
# Amounts
|
15
|
+
require 'double_double/amount'
|
16
|
+
require 'double_double/credit_amount'
|
17
|
+
require 'double_double/debit_amount'
|
18
|
+
|
19
|
+
# Transactions
|
20
|
+
require 'double_double/transaction'
|
21
|
+
require 'double_double/transaction_type'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :account, :class => DoubleDouble::Account do |account|
|
3
|
+
account.name { FactoryGirl.generate(:account_name) }
|
4
|
+
account.number { FactoryGirl.generate(:account_number)}
|
5
|
+
account.contra false
|
6
|
+
|
7
|
+
factory :asset, :class => DoubleDouble::Asset
|
8
|
+
factory :equity, :class => DoubleDouble::Equity
|
9
|
+
factory :expense, :class => DoubleDouble::Expense
|
10
|
+
factory :liability, :class => DoubleDouble::Liability
|
11
|
+
factory :revenue, :class => DoubleDouble::Revenue
|
12
|
+
|
13
|
+
factory :not_asset, :class => DoubleDouble::Liability
|
14
|
+
factory :not_equity, :class => DoubleDouble::Asset
|
15
|
+
factory :not_expense, :class => DoubleDouble::Liability
|
16
|
+
factory :not_liability, :class => DoubleDouble::Asset
|
17
|
+
factory :not_revenue, :class => DoubleDouble::Asset
|
18
|
+
end
|
19
|
+
|
20
|
+
sequence :account_name do |n|
|
21
|
+
"account name #{n}"
|
22
|
+
end
|
23
|
+
|
24
|
+
sequence :account_number do |n|
|
25
|
+
8000 + n
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :transaction_type, :class => DoubleDouble::TransactionType do |type|
|
3
|
+
type.description { FactoryGirl.generate(:transaction_type_description) }
|
4
|
+
type.number { FactoryGirl.generate(:transaction_type_number) }
|
5
|
+
end
|
6
|
+
|
7
|
+
sequence :transaction_type_description do |n|
|
8
|
+
"transaction type description #{n}"
|
9
|
+
end
|
10
|
+
|
11
|
+
sequence :transaction_type_number do |n|
|
12
|
+
9000 + n
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module DoubleDouble
|
2
|
+
describe Account do
|
3
|
+
|
4
|
+
it "should not allow creating an account without a subtype" do
|
5
|
+
account = FactoryGirl.build(:account)
|
6
|
+
account.should_not be_valid
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be unique per name" do
|
10
|
+
FactoryGirl.create(:asset, :name => "Test1")
|
11
|
+
account = FactoryGirl.build(:asset, :name => "Test1")
|
12
|
+
account.should_not be_valid
|
13
|
+
account.errors[:name].should == ["has already been taken"]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be unique per number" do
|
17
|
+
FactoryGirl.create(:asset, :number => 88)
|
18
|
+
account = FactoryGirl.build(:asset, :number => 88)
|
19
|
+
account.should_not be_valid
|
20
|
+
account.errors[:number].should == ["has already been taken"]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not have a balance method" do
|
24
|
+
lambda{Account.balance}.should raise_error(NoMethodError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should have a trial balance" do
|
28
|
+
Account.should respond_to(:trial_balance)
|
29
|
+
Account.trial_balance.should be_kind_of(Money)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should report a trial balance of 0 with correct transactions (with a contrived example of transactions)" do
|
33
|
+
# credit accounts
|
34
|
+
liability = FactoryGirl.create(:liability)
|
35
|
+
equity = FactoryGirl.create(:equity)
|
36
|
+
revenue = FactoryGirl.create(:revenue)
|
37
|
+
contra_asset = FactoryGirl.create(:asset, :contra => true)
|
38
|
+
contra_expense = FactoryGirl.create(:expense, :contra => true)
|
39
|
+
# debit accounts
|
40
|
+
asset = FactoryGirl.create(:asset)
|
41
|
+
expense = FactoryGirl.create(:expense)
|
42
|
+
contra_liability = FactoryGirl.create(:liability, :contra => true)
|
43
|
+
contra_equity = FactoryGirl.create(:equity, :contra => true)
|
44
|
+
contra_revenue = FactoryGirl.create(:revenue, :contra => true)
|
45
|
+
|
46
|
+
t = FactoryGirl.build(:transaction)
|
47
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: 100_000, account: liability)
|
48
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: 100_000, account: asset)
|
49
|
+
t.save
|
50
|
+
t = FactoryGirl.build(:transaction)
|
51
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: 1_000, account: equity)
|
52
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: 1_000, account: expense)
|
53
|
+
t.save
|
54
|
+
t = FactoryGirl.build(:transaction)
|
55
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: 40_404, account: revenue)
|
56
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: 40_404, account: contra_liability)
|
57
|
+
t.save
|
58
|
+
t = FactoryGirl.build(:transaction)
|
59
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: 2, account: contra_asset)
|
60
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: 2, account: contra_equity)
|
61
|
+
t.save
|
62
|
+
t = FactoryGirl.build(:transaction)
|
63
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: 333, account: contra_expense)
|
64
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: 333, account: contra_revenue)
|
65
|
+
t.save
|
66
|
+
|
67
|
+
Account.trial_balance.should == 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module DoubleDouble
|
2
|
+
describe CreditAmount do
|
3
|
+
|
4
|
+
it "should not be valid without an amount" do
|
5
|
+
expect {
|
6
|
+
FactoryGirl.build(:credit_amt, amount: nil, transaction: FactoryGirl.build(:transaction))
|
7
|
+
}.to raise_error(ArgumentError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should not be valid without a transaction" do
|
11
|
+
acct = FactoryGirl.create(:asset)
|
12
|
+
credit_amount = FactoryGirl.build(:credit_amt, transaction: nil, account: acct, amount: Money.new(20))
|
13
|
+
credit_amount.should_not be_valid
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should not be valid without an account" do
|
17
|
+
t = FactoryGirl.build(:transaction)
|
18
|
+
credit_amount = FactoryGirl.build(:credit_amt, account: nil, transaction: t, amount: Money.new(20))
|
19
|
+
credit_amount.should_not be_valid
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be sensitive to project_id when calculating balances, if supplied" do
|
23
|
+
acct_1 = FactoryGirl.create(:asset)
|
24
|
+
other_acct = FactoryGirl.create(:not_asset)
|
25
|
+
t = FactoryGirl.build(:transaction)
|
26
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: acct_1, project_id: 77)
|
27
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: other_acct)
|
28
|
+
t.save
|
29
|
+
t = FactoryGirl.build(:transaction)
|
30
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(321), account: acct_1, project_id: 77)
|
31
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(321), account: other_acct)
|
32
|
+
t.save
|
33
|
+
t = FactoryGirl.build(:transaction)
|
34
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(275), account: acct_1, project_id: 82)
|
35
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(275), account: other_acct)
|
36
|
+
t.save
|
37
|
+
t = FactoryGirl.build(:transaction)
|
38
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(999), account: acct_1)
|
39
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(999), account: other_acct)
|
40
|
+
t.save
|
41
|
+
acct_1.credits_balance({project_id: 77}).should == Money.new(123 + 321)
|
42
|
+
acct_1.credits_balance({project_id: 82}).should == Money.new(275)
|
43
|
+
acct_1.credits_balance.should == Money.new(123 + 321 + 275 + 999)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module DoubleDouble
|
2
|
+
describe DebitAmount do
|
3
|
+
|
4
|
+
it "should not be valid without an amount" do
|
5
|
+
expect {
|
6
|
+
FactoryGirl.build(:debit_amt, amount: nil, transaction: FactoryGirl.build(:transaction))
|
7
|
+
}.to raise_error(ArgumentError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should not be valid without a transaction" do
|
11
|
+
acct = FactoryGirl.create(:asset)
|
12
|
+
debit_amount = FactoryGirl.build(:debit_amt, transaction: nil, account: acct, amount: Money.new(20))
|
13
|
+
debit_amount.should_not be_valid
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should not be valid without an account" do
|
17
|
+
t = FactoryGirl.build(:transaction)
|
18
|
+
debit_amount = FactoryGirl.build(:debit_amt, account: nil, transaction: t, amount: Money.new(20))
|
19
|
+
debit_amount.should_not be_valid
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be sensitive to project_id when calculating balances, if supplied" do
|
23
|
+
acct_1 = FactoryGirl.create(:asset)
|
24
|
+
other_acct = FactoryGirl.create(:not_asset)
|
25
|
+
t = FactoryGirl.build(:transaction)
|
26
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: acct_1, project_id: 77)
|
27
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: other_acct)
|
28
|
+
t.save
|
29
|
+
t = FactoryGirl.build(:transaction)
|
30
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(321), account: acct_1, project_id: 77)
|
31
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(321), account: other_acct)
|
32
|
+
t.save
|
33
|
+
t = FactoryGirl.build(:transaction)
|
34
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(275), account: acct_1, project_id: 82)
|
35
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(275), account: other_acct)
|
36
|
+
t.save
|
37
|
+
t = FactoryGirl.build(:transaction)
|
38
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(999), account: acct_1)
|
39
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(999), account: other_acct)
|
40
|
+
t.save
|
41
|
+
acct_1.debits_balance({project_id: 77}).should == Money.new(123 + 321)
|
42
|
+
acct_1.debits_balance({project_id: 82}).should == Money.new(275)
|
43
|
+
acct_1.debits_balance.should == Money.new(123 + 321 + 275 + 999)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module DoubleDouble
|
2
|
+
describe Transaction do
|
3
|
+
|
4
|
+
before(:each) do
|
5
|
+
@acct = FactoryGirl.create(:asset)
|
6
|
+
@other_acct = FactoryGirl.create(:not_asset)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should create a transaction" do
|
10
|
+
-> {
|
11
|
+
t = FactoryGirl.build(:transaction)
|
12
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: @acct)
|
13
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: @other_acct)
|
14
|
+
t.save!
|
15
|
+
}.should change(DoubleDouble::Transaction, :count).by(1)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should not be valid without a credit amount" do
|
19
|
+
t = FactoryGirl.build(:transaction)
|
20
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: @other_acct)
|
21
|
+
t.should_not be_valid
|
22
|
+
t.errors['base'].should include("Transaction must have at least one credit amount")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not be valid with an invalid credit amount" do
|
26
|
+
-> {
|
27
|
+
t = FactoryGirl.build(:transaction)
|
28
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: nil, account: @acct)
|
29
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: @other_acct)
|
30
|
+
t.save
|
31
|
+
}.should raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not be valid without a debit amount" do
|
35
|
+
t = FactoryGirl.build(:transaction)
|
36
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: @acct)
|
37
|
+
t.should_not be_valid
|
38
|
+
t.errors['base'].should include("Transaction must have at least one debit amount")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should not be valid with an invalid debit amount" do
|
42
|
+
-> {
|
43
|
+
t = FactoryGirl.build(:transaction)
|
44
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: @acct)
|
45
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: nil, account: @other_acct)
|
46
|
+
t.save
|
47
|
+
}.should raise_error(ArgumentError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should not be valid without a description" do
|
51
|
+
t = FactoryGirl.build(:transaction, description: nil)
|
52
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: @acct)
|
53
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: @other_acct)
|
54
|
+
t.save
|
55
|
+
t.should_not be_valid
|
56
|
+
t.errors[:description].should == ["can't be blank"]
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should require the debit and credit amounts to cancel" do
|
60
|
+
t = FactoryGirl.build(:transaction)
|
61
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(555), account: @acct)
|
62
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(666), account: @other_acct)
|
63
|
+
t.save
|
64
|
+
t.should_not be_valid
|
65
|
+
t.errors['base'].should == ["The credit and debit amounts are not equal"]
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should have a polymorphic commercial document associations" do
|
69
|
+
mock_document = FactoryGirl.create(:asset) # one would never do this, but it allows us to not require a migration for the test
|
70
|
+
|
71
|
+
t = FactoryGirl.build(:transaction)
|
72
|
+
t.credit_amounts << FactoryGirl.create(:credit_amt, transaction: t, amount: Money.new(123), account: @acct)
|
73
|
+
t.debit_amounts << FactoryGirl.create(:debit_amt, transaction: t, amount: Money.new(123), account: @other_acct)
|
74
|
+
t.commercial_document = mock_document
|
75
|
+
t.save
|
76
|
+
|
77
|
+
saved_transaction = Transaction.find(t.id)
|
78
|
+
saved_transaction.commercial_document.should == mock_document
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should allow building a transaction and credit and debits with a hash" do
|
82
|
+
FactoryGirl.create(:asset, :name => "Accounts Receivable")
|
83
|
+
FactoryGirl.create(:revenue, :name => "Sales Revenue")
|
84
|
+
FactoryGirl.create(:liability, :name => "Sales Tax Payable")
|
85
|
+
mock_document = FactoryGirl.create(:asset)
|
86
|
+
transaction = Transaction.build(
|
87
|
+
description: "Sold some widgets",
|
88
|
+
commercial_document: mock_document,
|
89
|
+
debits: [
|
90
|
+
{account: "Accounts Receivable", amount: 50}],
|
91
|
+
credits: [
|
92
|
+
{account: "Sales Revenue", amount: 45},
|
93
|
+
{account: "Sales Tax Payable", amount: 5}])
|
94
|
+
transaction.should be_valid
|
95
|
+
transaction.save
|
96
|
+
saved_transaction = DoubleDouble::Transaction.find(transaction.id)
|
97
|
+
saved_transaction.commercial_document.should == mock_document
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|