odania_plutus 0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.markdown +394 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/plutus/application.js +16 -0
  6. data/app/assets/javascripts/plutus/reports.js +5 -0
  7. data/app/assets/stylesheets/bootstrap-theme.min.css +5 -0
  8. data/app/assets/stylesheets/bootstrap.min.css +5 -0
  9. data/app/assets/stylesheets/plutus/application.css +19 -0
  10. data/app/controllers/plutus/accounts_controller.rb +30 -0
  11. data/app/controllers/plutus/application_controller.rb +4 -0
  12. data/app/controllers/plutus/entries_controller.rb +34 -0
  13. data/app/controllers/plutus/reports_controller.rb +40 -0
  14. data/app/models/plutus/account.rb +170 -0
  15. data/app/models/plutus/amount.rb +22 -0
  16. data/app/models/plutus/amounts_extension.rb +42 -0
  17. data/app/models/plutus/asset.rb +56 -0
  18. data/app/models/plutus/credit_amount.rb +10 -0
  19. data/app/models/plutus/debit_amount.rb +10 -0
  20. data/app/models/plutus/entry.rb +77 -0
  21. data/app/models/plutus/equity.rb +56 -0
  22. data/app/models/plutus/expense.rb +56 -0
  23. data/app/models/plutus/liability.rb +56 -0
  24. data/app/models/plutus/no_tenancy.rb +9 -0
  25. data/app/models/plutus/revenue.rb +56 -0
  26. data/app/models/plutus/tenancy.rb +15 -0
  27. data/app/views/layouts/plutus/_messages.html.erb +9 -0
  28. data/app/views/layouts/plutus/_navigation.html.erb +19 -0
  29. data/app/views/layouts/plutus/_navigation_links.html.erb +5 -0
  30. data/app/views/layouts/plutus/application.html.erb +19 -0
  31. data/app/views/plutus/accounts/index.html.erb +26 -0
  32. data/app/views/plutus/entries/index.html.erb +59 -0
  33. data/app/views/plutus/reports/_account.html.erb +28 -0
  34. data/app/views/plutus/reports/balance_sheet.html.erb +21 -0
  35. data/app/views/plutus/reports/income_statement.html.erb +24 -0
  36. data/config/backtrace_silencers.rb +7 -0
  37. data/config/database.yml +5 -0
  38. data/config/inflections.rb +10 -0
  39. data/config/mime_types.rb +5 -0
  40. data/config/routes.rb +9 -0
  41. data/config/secret_token.rb +7 -0
  42. data/config/session_store.rb +8 -0
  43. data/lib/generators/plutus/USAGE +22 -0
  44. data/lib/generators/plutus/add_date_upgrade_generator.rb +11 -0
  45. data/lib/generators/plutus/base_generator.rb +19 -0
  46. data/lib/generators/plutus/plutus_generator.rb +12 -0
  47. data/lib/generators/plutus/templates/add_date_migration.rb +6 -0
  48. data/lib/generators/plutus/templates/migration.rb +39 -0
  49. data/lib/generators/plutus/templates/tenant_migration.rb +6 -0
  50. data/lib/generators/plutus/templates/update_migration.rb +17 -0
  51. data/lib/generators/plutus/tenancy_generator.rb +12 -0
  52. data/lib/generators/plutus/upgrade_plutus_generator.rb +12 -0
  53. data/lib/plutus.rb +23 -0
  54. data/lib/plutus/version.rb +3 -0
  55. data/spec/controllers/accounts_controller_spec.rb +19 -0
  56. data/spec/controllers/entries_controller_spec.rb +19 -0
  57. data/spec/controllers/reports_controller_spec.rb +24 -0
  58. data/spec/factories/account_factory.rb +35 -0
  59. data/spec/factories/amount_factory.rb +19 -0
  60. data/spec/factories/entry_factory.rb +11 -0
  61. data/spec/lib/plutus_spec.rb +0 -0
  62. data/spec/models/account_spec.rb +133 -0
  63. data/spec/models/amount_spec.rb +8 -0
  64. data/spec/models/asset_spec.rb +7 -0
  65. data/spec/models/credit_amount_spec.rb +7 -0
  66. data/spec/models/debit_amount_spec.rb +7 -0
  67. data/spec/models/entry_spec.rb +170 -0
  68. data/spec/models/equity_spec.rb +7 -0
  69. data/spec/models/expense_spec.rb +7 -0
  70. data/spec/models/liability_spec.rb +7 -0
  71. data/spec/models/revenue_spec.rb +7 -0
  72. data/spec/models/tenancy_spec.rb +45 -0
  73. data/spec/rcov.opts +2 -0
  74. data/spec/routing/accounts_routing_spec.rb +13 -0
  75. data/spec/routing/entries_routing_spec.rb +13 -0
  76. data/spec/spec.opts +4 -0
  77. data/spec/spec_helper.rb +26 -0
  78. data/spec/support/account_shared_examples.rb +60 -0
  79. data/spec/support/active_support_helpers.rb +13 -0
  80. data/spec/support/amount_shared_examples.rb +21 -0
  81. data/spec/support/factory_girl_helpers.rb +8 -0
  82. metadata +225 -0
@@ -0,0 +1,19 @@
1
+ FactoryGirl.define do
2
+ factory :amount, :class => Plutus::Amount do |amount|
3
+ amount.amount BigDecimal.new('473')
4
+ amount.association :entry, :factory => :entry_with_credit_and_debit
5
+ amount.association :account, :factory => :asset
6
+ end
7
+
8
+ factory :credit_amount, :class => Plutus::CreditAmount do |credit_amount|
9
+ credit_amount.amount BigDecimal.new('473')
10
+ credit_amount.association :entry, :factory => :entry_with_credit_and_debit
11
+ credit_amount.association :account, :factory => :revenue
12
+ end
13
+
14
+ factory :debit_amount, :class => Plutus::DebitAmount do |debit_amount|
15
+ debit_amount.amount BigDecimal.new('473')
16
+ debit_amount.association :entry, :factory => :entry_with_credit_and_debit
17
+ debit_amount.association :account, :factory => :asset
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ FactoryGirl.define do
2
+ factory :entry, :class => Plutus::Entry do |entry|
3
+ entry.description 'factory description'
4
+ factory :entry_with_credit_and_debit, :class => Plutus::Entry do |entry_cd|
5
+ entry_cd.after_build do |t|
6
+ t.credit_amounts << FactoryGirl.build(:credit_amount, :entry => t)
7
+ t.debit_amounts << FactoryGirl.build(:debit_amount, :entry => t)
8
+ end
9
+ end
10
+ end
11
+ end
File without changes
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Account do
5
+ let(:account) { FactoryGirl.build(:account) }
6
+ subject { account }
7
+
8
+ it { is_expected.not_to be_valid } # must construct a child type instead
9
+
10
+ describe "when using a child type" do
11
+ let(:account) { FactoryGirl.create(:account, type: "Finance::Asset") }
12
+ it { is_expected.to be_valid }
13
+
14
+ it "should be unique per name" do
15
+ conflict = FactoryGirl.build(:account, name: account.name, type: account.type)
16
+ expect(conflict).not_to be_valid
17
+ expect(conflict.errors[:name]).to eq(["has already been taken"])
18
+ end
19
+ end
20
+
21
+ it "calling the instance method #balance should raise a NoMethodError" do
22
+ expect { subject.balance }.to raise_error NoMethodError, "undefined method 'balance'"
23
+ end
24
+
25
+ it "calling the class method ::balance should raise a NoMethodError" do
26
+ expect { subject.class.balance }.to raise_error NoMethodError, "undefined method 'balance'"
27
+ end
28
+
29
+ describe ".trial_balance" do
30
+ subject { Account.trial_balance }
31
+ it { is_expected.to be_kind_of BigDecimal }
32
+
33
+ context "when given no entries" do
34
+ it { is_expected.to eq(0) }
35
+ end
36
+
37
+ context "when given correct entries" do
38
+ before {
39
+ # credit accounts
40
+ liability = FactoryGirl.create(:liability)
41
+ equity = FactoryGirl.create(:equity)
42
+ revenue = FactoryGirl.create(:revenue)
43
+ contra_asset = FactoryGirl.create(:asset, :contra => true)
44
+ contra_expense = FactoryGirl.create(:expense, :contra => true)
45
+ # credit amounts
46
+ ca1 = FactoryGirl.build(:credit_amount, :account => liability, :amount => 100000)
47
+ ca2 = FactoryGirl.build(:credit_amount, :account => equity, :amount => 1000)
48
+ ca3 = FactoryGirl.build(:credit_amount, :account => revenue, :amount => 40404)
49
+ ca4 = FactoryGirl.build(:credit_amount, :account => contra_asset, :amount => 2)
50
+ ca5 = FactoryGirl.build(:credit_amount, :account => contra_expense, :amount => 333)
51
+
52
+ # debit accounts
53
+ asset = FactoryGirl.create(:asset)
54
+ expense = FactoryGirl.create(:expense)
55
+ contra_liability = FactoryGirl.create(:liability, :contra => true)
56
+ contra_equity = FactoryGirl.create(:equity, :contra => true)
57
+ contra_revenue = FactoryGirl.create(:revenue, :contra => true)
58
+ # debit amounts
59
+ da1 = FactoryGirl.build(:debit_amount, :account => asset, :amount => 100000)
60
+ da2 = FactoryGirl.build(:debit_amount, :account => expense, :amount => 1000)
61
+ da3 = FactoryGirl.build(:debit_amount, :account => contra_liability, :amount => 40404)
62
+ da4 = FactoryGirl.build(:debit_amount, :account => contra_equity, :amount => 2)
63
+ da5 = FactoryGirl.build(:debit_amount, :account => contra_revenue, :amount => 333)
64
+
65
+ FactoryGirl.create(:entry, :credit_amounts => [ca1], :debit_amounts => [da1])
66
+ FactoryGirl.create(:entry, :credit_amounts => [ca2], :debit_amounts => [da2])
67
+ FactoryGirl.create(:entry, :credit_amounts => [ca3], :debit_amounts => [da3])
68
+ FactoryGirl.create(:entry, :credit_amounts => [ca4], :debit_amounts => [da4])
69
+ FactoryGirl.create(:entry, :credit_amounts => [ca5], :debit_amounts => [da5])
70
+ }
71
+
72
+ it { is_expected.to eq(0) }
73
+ end
74
+ end
75
+
76
+ describe "#amounts" do
77
+ it "returns all credit and debit amounts" do
78
+ equity = FactoryGirl.create(:equity)
79
+ asset = FactoryGirl.create(:asset)
80
+ expense = FactoryGirl.create(:expense)
81
+
82
+ investment = Entry.new(
83
+ description: "Initial investment",
84
+ date: Date.today,
85
+ debits: [{ account_name: equity.name, amount: 1000 }],
86
+ credits: [{ account_name: asset.name, amount: 1000 }],
87
+ )
88
+ investment.save
89
+
90
+ purchase = Entry.new(
91
+ description: "First computer",
92
+ date: Date.today,
93
+ debits: [{ account_name: asset.name, amount: 900 }],
94
+ credits: [{ account_name: expense.name, amount: 900 }],
95
+ )
96
+ purchase.save
97
+
98
+ expect(equity.amounts.size).to eq 1
99
+ expect(asset.amounts.size).to eq 2
100
+ expect(expense.amounts.size).to eq 1
101
+ end
102
+ end
103
+
104
+ describe "#entries" do
105
+ it "returns all credit and debit entries" do
106
+ equity = FactoryGirl.create(:equity)
107
+ asset = FactoryGirl.create(:asset)
108
+ expense = FactoryGirl.create(:expense)
109
+
110
+ investment = Entry.new(
111
+ description: "Initial investment",
112
+ date: Date.today,
113
+ debits: [{ account_name: equity.name, amount: 1000 }],
114
+ credits: [{ account_name: asset.name, amount: 1000 }],
115
+ )
116
+ investment.save
117
+
118
+ purchase = Entry.new(
119
+ description: "First computer",
120
+ date: Date.today,
121
+ debits: [{ account_name: asset.name, amount: 900 }],
122
+ credits: [{ account_name: expense.name, amount: 900 }],
123
+ )
124
+ purchase.save
125
+
126
+ expect(equity.entries.size).to eq 1
127
+ expect(asset.entries.size).to eq 2
128
+ expect(expense.entries.size).to eq 1
129
+ end
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Amount do
5
+ subject { FactoryGirl.build(:amount) }
6
+ it { is_expected.not_to be_valid } # construct a child class instead
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Asset do
5
+ it_behaves_like 'a Plutus::Account subtype', kind: :asset, normal_balance: :debit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe CreditAmount do
5
+ it_behaves_like 'a Plutus::Amount subtype', kind: :credit_amount
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe DebitAmount do
5
+ it_behaves_like 'a Plutus::Amount subtype', kind: :debit_amount
6
+ end
7
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Entry do
5
+ let(:entry) { FactoryGirl.build(:entry) }
6
+ subject { entry }
7
+
8
+ it { is_expected.not_to be_valid }
9
+
10
+ context "with credit and debit" do
11
+ let(:entry) { FactoryGirl.build(:entry_with_credit_and_debit) }
12
+ it { is_expected.to be_valid }
13
+
14
+ it "should require a description" do
15
+ entry.description = nil
16
+ expect(entry).not_to be_valid
17
+ end
18
+ end
19
+
20
+ context "with a debit" do
21
+ before {
22
+ entry.debit_amounts << FactoryGirl.build(:debit_amount, entry: entry)
23
+ }
24
+ it { is_expected.not_to be_valid }
25
+
26
+ context "with an invalid credit" do
27
+ before {
28
+ entry.credit_amounts << FactoryGirl.build(:credit_amount, entry: entry, amount: nil)
29
+ }
30
+ it { is_expected.not_to be_valid }
31
+ end
32
+ end
33
+
34
+ context "with a credit" do
35
+ before {
36
+ entry.credit_amounts << FactoryGirl.build(:credit_amount, entry: entry)
37
+ }
38
+ it { is_expected.not_to be_valid }
39
+
40
+ context "with an invalid debit" do
41
+ before {
42
+ entry.debit_amounts << FactoryGirl.build(:debit_amount, entry: entry, amount: nil)
43
+ }
44
+ it { is_expected.not_to be_valid }
45
+ end
46
+ end
47
+
48
+ context "without a date" do
49
+ let(:entry) { FactoryGirl.build(:entry_with_credit_and_debit, date: nil) }
50
+
51
+ context "should assign a default date before being saved" do
52
+ before { entry.save! }
53
+ its(:date) { is_expected.to eq(Time.now.to_date) }
54
+ end
55
+ end
56
+
57
+ it "should require the debit and credit amounts to cancel" do
58
+ entry.credit_amounts << FactoryGirl.build(:credit_amount, :amount => 100, :entry => entry)
59
+ entry.debit_amounts << FactoryGirl.build(:debit_amount, :amount => 200, :entry => entry)
60
+ expect(entry).not_to be_valid
61
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
62
+ end
63
+
64
+ it "should require the debit and credit amounts to cancel even with fractions" do
65
+ entry = FactoryGirl.build(:entry)
66
+ entry.credit_amounts << FactoryGirl.build(:credit_amount, :amount => 100.1, :entry => entry)
67
+ entry.debit_amounts << FactoryGirl.build(:debit_amount, :amount => 100.2, :entry => entry)
68
+ expect(entry).not_to be_valid
69
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
70
+ end
71
+
72
+ it "should ignore debit and credit amounts marked for destruction to cancel" do
73
+ entry.credit_amounts << FactoryGirl.build(:credit_amount, :amount => 100, :entry => entry)
74
+ debit_amount = FactoryGirl.build(:debit_amount, :amount => 100, :entry => entry)
75
+ debit_amount.mark_for_destruction
76
+ entry.debit_amounts << debit_amount
77
+ expect(entry).not_to be_valid
78
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
79
+ end
80
+
81
+ it "should have a polymorphic commercial document associations" do
82
+ mock_document = FactoryGirl.create(:asset) # one would never do this, but it allows us to not require a migration for the test
83
+ entry = FactoryGirl.build(:entry_with_credit_and_debit, commercial_document: mock_document)
84
+ entry.save!
85
+ saved_entry = Entry.find(entry.id)
86
+ expect(saved_entry.commercial_document).to eq(mock_document)
87
+ end
88
+
89
+ context "given a set of accounts" do
90
+ let(:mock_document) { FactoryGirl.create(:asset) }
91
+ let!(:accounts_receivable) { FactoryGirl.create(:asset, name: "Accounts Receivable") }
92
+ let!(:sales_revenue) { FactoryGirl.create(:revenue, name: "Sales Revenue") }
93
+ let!(:sales_tax_payable) { FactoryGirl.create(:liability, name: "Sales Tax Payable") }
94
+
95
+ shared_examples_for 'a built-from-hash Plutus::Entry' do
96
+ its(:credit_amounts) { is_expected.not_to be_empty }
97
+ its(:debit_amounts) { is_expected.not_to be_empty }
98
+ it { is_expected.to be_valid }
99
+
100
+ context "when saved" do
101
+ before { entry.save! }
102
+ its(:id) { is_expected.not_to be_nil }
103
+
104
+ context "when reloaded" do
105
+ let(:saved_transaction) { Entry.find(entry.id) }
106
+ subject { saved_transaction }
107
+ it("should have the correct commercial document") {
108
+ saved_transaction.commercial_document == mock_document
109
+ }
110
+ end
111
+ end
112
+ end
113
+
114
+ describe ".new" do
115
+ let(:entry) { Entry.new(hash) }
116
+ subject { entry }
117
+
118
+ context "when given a credit/debits hash with :account => Account" do
119
+ let(:hash) {
120
+ {
121
+ description: "Sold some widgets",
122
+ commercial_document: mock_document,
123
+ debits: [{account: accounts_receivable, amount: 50}],
124
+ credits: [
125
+ {account: sales_revenue, amount: 45},
126
+ {account: sales_tax_payable, amount: 5}
127
+ ]
128
+ }
129
+ }
130
+ include_examples 'a built-from-hash Plutus::Entry'
131
+ end
132
+
133
+ context "when given a credit/debits hash with :account_name => String" do
134
+ let(:hash) {
135
+ {
136
+ description: "Sold some widgets",
137
+ commercial_document: mock_document,
138
+ debits: [{account_name: accounts_receivable.name, amount: 50}],
139
+ credits: [
140
+ {account_name: sales_revenue.name, amount: 45},
141
+ {account_name: sales_tax_payable.name, amount: 5}
142
+ ]
143
+ }
144
+ }
145
+ include_examples 'a built-from-hash Plutus::Entry'
146
+ end
147
+ end
148
+
149
+ describe ".build" do
150
+ let(:entry) { Entry.build(hash) }
151
+ subject { entry }
152
+
153
+ before { ::ActiveSupport::Deprecation.silenced = true }
154
+ after { ::ActiveSupport::Deprecation.silenced = false }
155
+
156
+ context "when used at all" do
157
+ let(:hash) { Hash.new }
158
+
159
+ it("should be deprecated") {
160
+ # .build is the only thing deprecated
161
+ expect(::ActiveSupport::Deprecation).to receive(:warn).once
162
+ entry
163
+ }
164
+ end
165
+
166
+ end
167
+ end
168
+
169
+ end
170
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Equity do
5
+ it_behaves_like 'a Plutus::Account subtype', kind: :equity, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Expense do
5
+ it_behaves_like 'a Plutus::Account subtype', kind: :expense, normal_balance: :debit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Liability do
5
+ it_behaves_like 'a Plutus::Account subtype', kind: :liability, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Revenue do
5
+ it_behaves_like 'a Plutus::Account subtype', kind: :revenue, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Account do
5
+ describe 'tenancy support' do
6
+ before(:each) do
7
+ ActiveSupportHelpers.clear_model('Account')
8
+ ActiveSupportHelpers.clear_model('Asset')
9
+
10
+ Plutus.enable_tenancy = true
11
+ Plutus.tenant_class = 'Plutus::Entry'
12
+
13
+ FactoryGirlHelpers.reload()
14
+ Plutus::Asset.new
15
+ end
16
+
17
+ after(:each) do
18
+ if Plutus.const_defined?(:Asset)
19
+ ActiveSupportHelpers.clear_model('Account')
20
+ ActiveSupportHelpers.clear_model('Asset')
21
+ end
22
+
23
+ Plutus.enable_tenancy = false
24
+ Plutus.tenant_class = nil
25
+
26
+ FactoryGirlHelpers.reload()
27
+ end
28
+
29
+ it 'validate uniqueness of name scoped to tenant' do
30
+ account = FactoryGirl.create(:asset, tenant_id: 10)
31
+
32
+ record = FactoryGirl.build(:asset, name: account.name, tenant_id: 10)
33
+ expect(record).not_to be_valid
34
+ expect(record.errors[:name]).to eq(['has already been taken'])
35
+ end
36
+
37
+ it 'allows same name scoped under a different tenant' do
38
+ account = FactoryGirl.create(:asset, tenant_id: 10)
39
+
40
+ record = FactoryGirl.build(:asset, name: account.name, tenant_id: 11)
41
+ expect(record).to be_valid
42
+ end
43
+ end
44
+ end
45
+ end