tyche 0.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.markdown +392 -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 +172 -0
  15. data/app/models/plutus/amount.rb +24 -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/balance_finders/base_balance_finder.rb +13 -0
  19. data/app/models/plutus/credit_amount.rb +10 -0
  20. data/app/models/plutus/debit_amount.rb +10 -0
  21. data/app/models/plutus/entry.rb +77 -0
  22. data/app/models/plutus/equity.rb +56 -0
  23. data/app/models/plutus/expense.rb +56 -0
  24. data/app/models/plutus/liability.rb +56 -0
  25. data/app/models/plutus/no_tenancy.rb +9 -0
  26. data/app/models/plutus/revenue.rb +56 -0
  27. data/app/models/plutus/tenancy.rb +15 -0
  28. data/app/views/layouts/plutus/_messages.html.erb +9 -0
  29. data/app/views/layouts/plutus/_navigation.html.erb +19 -0
  30. data/app/views/layouts/plutus/_navigation_links.html.erb +5 -0
  31. data/app/views/layouts/plutus/application.html.erb +19 -0
  32. data/app/views/plutus/accounts/index.html.erb +26 -0
  33. data/app/views/plutus/entries/index.html.erb +59 -0
  34. data/app/views/plutus/reports/_account.html.erb +28 -0
  35. data/app/views/plutus/reports/balance_sheet.html.erb +21 -0
  36. data/app/views/plutus/reports/income_statement.html.erb +24 -0
  37. data/config/backtrace_silencers.rb +7 -0
  38. data/config/database.yml +5 -0
  39. data/config/inflections.rb +10 -0
  40. data/config/mime_types.rb +5 -0
  41. data/config/routes.rb +9 -0
  42. data/config/secret_token.rb +7 -0
  43. data/config/session_store.rb +8 -0
  44. data/db/migrate/20160422010135_create_plutus_tables.rb +35 -0
  45. data/lib/generators/plutus/USAGE +22 -0
  46. data/lib/generators/plutus/add_date_upgrade_generator.rb +11 -0
  47. data/lib/generators/plutus/base_generator.rb +19 -0
  48. data/lib/generators/plutus/plutus_generator.rb +12 -0
  49. data/lib/generators/plutus/templates/tenant_migration.rb +6 -0
  50. data/lib/generators/plutus/tenancy_generator.rb +12 -0
  51. data/lib/generators/plutus/upgrade_plutus_generator.rb +12 -0
  52. data/lib/plutus.rb +20 -0
  53. data/lib/plutus/engine.rb +5 -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 +20 -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 +13 -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 +182 -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 +30 -0
  78. data/spec/support/account_shared_examples.rb +62 -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. data/spec/support/shoulda_matchers.rb +8 -0
  83. metadata +240 -0
@@ -0,0 +1,20 @@
1
+ require 'bigdecimal'
2
+ FactoryBot.define do
3
+ factory :amount, :class => Plutus::Amount do |amount|
4
+ amount.amount BigDecimal.new('473')
5
+ amount.association :entry, :factory => :entry_with_credit_and_debit
6
+ amount.association :account, :factory => :asset
7
+ end
8
+
9
+ factory :credit_amount, :class => Plutus::CreditAmount do |credit_amount|
10
+ credit_amount.amount BigDecimal.new('473')
11
+ credit_amount.association :entry, :factory => :entry_with_credit_and_debit
12
+ credit_amount.association :account, :factory => :revenue
13
+ end
14
+
15
+ factory :debit_amount, :class => Plutus::DebitAmount do |debit_amount|
16
+ debit_amount.amount BigDecimal.new('473')
17
+ debit_amount.association :entry, :factory => :entry_with_credit_and_debit
18
+ debit_amount.association :account, :factory => :asset
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ FactoryBot.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 << FactoryBot.build(:credit_amount, :entry => t)
7
+ t.debit_amounts << FactoryBot.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) { FactoryBot.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) { FactoryBot.create(:account, type: "Finance::Asset") }
12
+ it { is_expected.to be_valid }
13
+
14
+ it "should be unique per name" do
15
+ conflict = FactoryBot.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 = FactoryBot.create(:liability)
41
+ equity = FactoryBot.create(:equity)
42
+ revenue = FactoryBot.create(:revenue)
43
+ contra_asset = FactoryBot.create(:asset, :contra => true)
44
+ contra_expense = FactoryBot.create(:expense, :contra => true)
45
+ # credit amounts
46
+ ca1 = FactoryBot.build(:credit_amount, :account => liability, :amount => 100000)
47
+ ca2 = FactoryBot.build(:credit_amount, :account => equity, :amount => 1000)
48
+ ca3 = FactoryBot.build(:credit_amount, :account => revenue, :amount => 40404)
49
+ ca4 = FactoryBot.build(:credit_amount, :account => contra_asset, :amount => 2)
50
+ ca5 = FactoryBot.build(:credit_amount, :account => contra_expense, :amount => 333)
51
+
52
+ # debit accounts
53
+ asset = FactoryBot.create(:asset)
54
+ expense = FactoryBot.create(:expense)
55
+ contra_liability = FactoryBot.create(:liability, :contra => true)
56
+ contra_equity = FactoryBot.create(:equity, :contra => true)
57
+ contra_revenue = FactoryBot.create(:revenue, :contra => true)
58
+ # debit amounts
59
+ da1 = FactoryBot.build(:debit_amount, :account => asset, :amount => 100000)
60
+ da2 = FactoryBot.build(:debit_amount, :account => expense, :amount => 1000)
61
+ da3 = FactoryBot.build(:debit_amount, :account => contra_liability, :amount => 40404)
62
+ da4 = FactoryBot.build(:debit_amount, :account => contra_equity, :amount => 2)
63
+ da5 = FactoryBot.build(:debit_amount, :account => contra_revenue, :amount => 333)
64
+
65
+ FactoryBot.create(:entry, :credit_amounts => [ca1], :debit_amounts => [da1])
66
+ FactoryBot.create(:entry, :credit_amounts => [ca2], :debit_amounts => [da2])
67
+ FactoryBot.create(:entry, :credit_amounts => [ca3], :debit_amounts => [da3])
68
+ FactoryBot.create(:entry, :credit_amounts => [ca4], :debit_amounts => [da4])
69
+ FactoryBot.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 = FactoryBot.create(:equity)
79
+ asset = FactoryBot.create(:asset)
80
+ expense = FactoryBot.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 = FactoryBot.create(:equity)
107
+ asset = FactoryBot.create(:asset)
108
+ expense = FactoryBot.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,13 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Amount do
5
+ subject { FactoryBot.build(:amount) }
6
+
7
+ describe "attributes" do
8
+ it { is_expected.to delegate_method(:name).to(:account).with_prefix }
9
+ end
10
+
11
+ it { is_expected.not_to be_valid } # construct a child class instead
12
+ end
13
+ 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,182 @@
1
+ require 'spec_helper'
2
+
3
+ module Plutus
4
+ describe Entry do
5
+ describe 'associations' do
6
+ it { is_expected.to belong_to(:commercial_document).optional }
7
+ it { is_expected.to have_many(:amounts).dependent(:destroy) }
8
+ it { is_expected.to have_many(:debit_amounts) }
9
+ it { is_expected.to have_many(:credit_amounts) }
10
+ it { is_expected.to have_many(:debit_accounts) }
11
+ it { is_expected.to have_many(:credit_accounts) }
12
+
13
+
14
+
15
+
16
+ end
17
+ let(:entry) { build(:entry) }
18
+ subject { entry }
19
+
20
+ it { is_expected.not_to be_valid }
21
+
22
+ context "with credit and debit" do
23
+ let(:entry) { build(:entry_with_credit_and_debit) }
24
+ it { is_expected.to be_valid }
25
+
26
+ it "should require a description" do
27
+ entry.description = nil
28
+ expect(entry).not_to be_valid
29
+ end
30
+ end
31
+
32
+ context "with a debit" do
33
+ before {
34
+ entry.debit_amounts << build(:debit_amount, entry: entry)
35
+ }
36
+ it { is_expected.not_to be_valid }
37
+
38
+ context "with an invalid credit" do
39
+ before {
40
+ entry.credit_amounts << build(:credit_amount, entry: entry, amount: nil)
41
+ }
42
+ it { is_expected.not_to be_valid }
43
+ end
44
+ end
45
+
46
+ context "with a credit" do
47
+ before {
48
+ entry.credit_amounts << build(:credit_amount, entry: entry)
49
+ }
50
+ it { is_expected.not_to be_valid }
51
+
52
+ context "with an invalid debit" do
53
+ before {
54
+ entry.debit_amounts << build(:debit_amount, entry: entry, amount: nil)
55
+ }
56
+ it { is_expected.not_to be_valid }
57
+ end
58
+ end
59
+
60
+ context "without a date" do
61
+ let(:entry) { build(:entry_with_credit_and_debit, date: nil) }
62
+
63
+ context "should assign a default date before being saved" do
64
+ before { entry.save! }
65
+ its(:date) { is_expected.to eq(Time.now.to_date) }
66
+ end
67
+ end
68
+
69
+ it "should require the debit and credit amounts to cancel" do
70
+ entry.credit_amounts << build(:credit_amount, :amount => 100, :entry => entry)
71
+ entry.debit_amounts << build(:debit_amount, :amount => 200, :entry => entry)
72
+ expect(entry).not_to be_valid
73
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
74
+ end
75
+
76
+ it "should require the debit and credit amounts to cancel even with fractions" do
77
+ entry = build(:entry)
78
+ entry.credit_amounts << build(:credit_amount, :amount => 100.1, :entry => entry)
79
+ entry.debit_amounts << build(:debit_amount, :amount => 100.2, :entry => entry)
80
+ expect(entry).not_to be_valid
81
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
82
+ end
83
+
84
+ it "should ignore debit and credit amounts marked for destruction to cancel" do
85
+ entry.credit_amounts << build(:credit_amount, :amount => 100, :entry => entry)
86
+ debit_amount = build(:debit_amount, :amount => 100, :entry => entry)
87
+ debit_amount.mark_for_destruction
88
+ entry.debit_amounts << debit_amount
89
+ expect(entry).not_to be_valid
90
+ expect(entry.errors['base']).to eq(["The credit and debit amounts are not equal"])
91
+ end
92
+
93
+ it "should have a polymorphic commercial document associations" do
94
+ mock_document = create(:asset) # one would never do this, but it allows us to not require a migration for the test
95
+ entry = build(:entry_with_credit_and_debit, commercial_document: mock_document)
96
+ entry.save!
97
+ saved_entry = Entry.find(entry.id)
98
+ expect(saved_entry.commercial_document).to eq(mock_document)
99
+ end
100
+
101
+ context "given a set of accounts" do
102
+ let(:mock_document) { create(:asset) }
103
+ let!(:accounts_receivable) { create(:asset, name: "Accounts Receivable") }
104
+ let!(:sales_revenue) { create(:revenue, name: "Sales Revenue") }
105
+ let!(:sales_tax_payable) { create(:liability, name: "Sales Tax Payable") }
106
+
107
+ shared_examples_for 'a built-from-hash Plutus::Entry' do
108
+ its(:credit_amounts) { is_expected.not_to be_empty }
109
+ its(:debit_amounts) { is_expected.not_to be_empty }
110
+ it { is_expected.to be_valid }
111
+
112
+ context "when saved" do
113
+ before { entry.save! }
114
+ its(:id) { is_expected.not_to be_nil }
115
+
116
+ context "when reloaded" do
117
+ let(:saved_transaction) { Entry.find(entry.id) }
118
+ subject { saved_transaction }
119
+ it("should have the correct commercial document") {
120
+ saved_transaction.commercial_document == mock_document
121
+ }
122
+ end
123
+ end
124
+ end
125
+
126
+ describe ".new" do
127
+ let(:entry) { Entry.new(hash) }
128
+ subject { entry }
129
+
130
+ context "when given a credit/debits hash with :account => Account" do
131
+ let(:hash) {
132
+ {
133
+ description: "Sold some widgets",
134
+ commercial_document: mock_document,
135
+ debits: [{account: accounts_receivable, amount: 50}],
136
+ credits: [
137
+ {account: sales_revenue, amount: 45},
138
+ {account: sales_tax_payable, amount: 5}
139
+ ]
140
+ }
141
+ }
142
+ include_examples 'a built-from-hash Plutus::Entry'
143
+ end
144
+
145
+ context "when given a credit/debits hash with :account_name => String" do
146
+ let(:hash) {
147
+ {
148
+ description: "Sold some widgets",
149
+ commercial_document: mock_document,
150
+ debits: [{account_name: accounts_receivable.name, amount: 50}],
151
+ credits: [
152
+ {account_name: sales_revenue.name, amount: 45},
153
+ {account_name: sales_tax_payable.name, amount: 5}
154
+ ]
155
+ }
156
+ }
157
+ include_examples 'a built-from-hash Plutus::Entry'
158
+ end
159
+ end
160
+
161
+ describe ".build" do
162
+ let(:entry) { Entry.build(hash) }
163
+ subject { entry }
164
+
165
+ before { ::ActiveSupport::Deprecation.silenced = true }
166
+ after { ::ActiveSupport::Deprecation.silenced = false }
167
+
168
+ context "when used at all" do
169
+ let(:hash) { Hash.new }
170
+
171
+ it("should be deprecated") {
172
+ # .build is the only thing deprecated
173
+ expect(::ActiveSupport::Deprecation).to receive(:warn).once
174
+ entry
175
+ }
176
+ end
177
+
178
+ end
179
+ end
180
+
181
+ end
182
+ 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
+ FactoryBotHelpers.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
+ FactoryBotHelpers.reload()
27
+ end
28
+
29
+ it 'validate uniqueness of name scoped to tenant' do
30
+ account = FactoryBot.create(:asset, tenant_id: 10)
31
+
32
+ record = FactoryBot.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 = FactoryBot.create(:asset, tenant_id: 10)
39
+
40
+ record = FactoryBot.build(:asset, name: account.name, tenant_id: 11)
41
+ expect(record).to be_valid
42
+ end
43
+ end
44
+ end
45
+ end