odania_plutus 0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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