tyche 0.14

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 (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