double_double 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +38 -0
  7. data/Rakefile +10 -0
  8. data/double_double.gemspec +27 -0
  9. data/lib/double_double/account.rb +85 -0
  10. data/lib/double_double/amount.rb +33 -0
  11. data/lib/double_double/asset.rb +25 -0
  12. data/lib/double_double/credit_amount.rb +9 -0
  13. data/lib/double_double/debit_amount.rb +9 -0
  14. data/lib/double_double/equity.rb +26 -0
  15. data/lib/double_double/expense.rb +26 -0
  16. data/lib/double_double/liability.rb +25 -0
  17. data/lib/double_double/revenue.rb +26 -0
  18. data/lib/double_double/transaction.rb +88 -0
  19. data/lib/double_double/transaction_type.rb +13 -0
  20. data/lib/double_double/version.rb +3 -0
  21. data/lib/double_double.rb +21 -0
  22. data/spec/factories/account_factory.rb +27 -0
  23. data/spec/factories/amount_factory.rb +10 -0
  24. data/spec/factories/transaction_factory.rb +9 -0
  25. data/spec/factories/transaction_type_factory.rb +14 -0
  26. data/spec/models/account_spec.rb +70 -0
  27. data/spec/models/amount_spec.rb +9 -0
  28. data/spec/models/asset_spec.rb +12 -0
  29. data/spec/models/credit_amount_spec.rb +46 -0
  30. data/spec/models/debit_amount_spec.rb +46 -0
  31. data/spec/models/equity_spec.rb +12 -0
  32. data/spec/models/expense_spec.rb +12 -0
  33. data/spec/models/liability_spec.rb +12 -0
  34. data/spec/models/revenue_spec.rb +12 -0
  35. data/spec/models/transaction_spec.rb +101 -0
  36. data/spec/spec_helper.rb +87 -0
  37. data/spec/support/all_account_types.rb +50 -0
  38. data/spec/support/left_side_account_types.rb +125 -0
  39. data/spec/support/right_side_account_types.rb +125 -0
  40. 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,3 @@
1
+ module DoubleDouble
2
+ VERSION = "0.0.1"
3
+ 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,10 @@
1
+ FactoryGirl.define do
2
+ factory :amount, :class => DoubleDouble::Amount do |amount|
3
+ end
4
+
5
+ factory :credit_amt, :class => DoubleDouble::CreditAmount do
6
+ end
7
+
8
+ factory :debit_amt, :class => DoubleDouble::DebitAmount do
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ FactoryGirl.define do
2
+ factory :transaction, :class => DoubleDouble::Transaction do
3
+ description { FactoryGirl.generate(:transaction_type_description) }
4
+ end
5
+
6
+ sequence :transaction_description do |n|
7
+ "transaction description #{n}"
8
+ end
9
+ 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,9 @@
1
+ module DoubleDouble
2
+ describe Amount do
3
+
4
+ it "should not allow creating an amount without a subtype" do
5
+ amount = FactoryGirl.build(:amount)
6
+ amount.should_not be_valid
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module DoubleDouble
2
+ describe Asset do
3
+
4
+ it_behaves_like "all account types" do
5
+ let(:account_type) {:asset}
6
+ end
7
+
8
+ it_behaves_like "a left side account type" do
9
+ let(:left_side_account_type) {:asset}
10
+ end
11
+ end
12
+ 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,12 @@
1
+ module DoubleDouble
2
+ describe Equity do
3
+
4
+ it_behaves_like "all account types" do
5
+ let(:account_type) {:equity}
6
+ end
7
+
8
+ it_behaves_like "a right side account type" do
9
+ let(:right_side_account_type) {:equity}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module DoubleDouble
2
+ describe Expense do
3
+
4
+ it_behaves_like "all account types" do
5
+ let(:account_type) {:expense}
6
+ end
7
+
8
+ it_behaves_like "a left side account type" do
9
+ let(:left_side_account_type) {:expense}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module DoubleDouble
2
+ describe Liability do
3
+
4
+ it_behaves_like "all account types" do
5
+ let(:account_type) {:liability}
6
+ end
7
+
8
+ it_behaves_like "a right side account type" do
9
+ let(:right_side_account_type) {:liability}
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module DoubleDouble
2
+ describe Revenue do
3
+
4
+ it_behaves_like "all account types" do
5
+ let(:account_type) {:revenue}
6
+ end
7
+
8
+ it_behaves_like "a right side account type" do
9
+ let(:right_side_account_type) {:revenue}
10
+ end
11
+ end
12
+ 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