borutus 0.1.0

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 +21 -0
  3. data/README.md +346 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/borutus/application.js +16 -0
  6. data/app/assets/javascripts/borutus/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/borutus/application.css +19 -0
  10. data/app/controllers/borutus/accounts_controller.rb +30 -0
  11. data/app/controllers/borutus/application_controller.rb +4 -0
  12. data/app/controllers/borutus/entries_controller.rb +34 -0
  13. data/app/controllers/borutus/reports_controller.rb +40 -0
  14. data/app/models/borutus/account.rb +180 -0
  15. data/app/models/borutus/amount.rb +24 -0
  16. data/app/models/borutus/amounts_extension.rb +42 -0
  17. data/app/models/borutus/asset.rb +56 -0
  18. data/app/models/borutus/credit_amount.rb +10 -0
  19. data/app/models/borutus/debit_amount.rb +10 -0
  20. data/app/models/borutus/entry.rb +77 -0
  21. data/app/models/borutus/equity.rb +56 -0
  22. data/app/models/borutus/expense.rb +56 -0
  23. data/app/models/borutus/liability.rb +56 -0
  24. data/app/models/borutus/no_tenancy.rb +9 -0
  25. data/app/models/borutus/revenue.rb +56 -0
  26. data/app/models/borutus/tenancy.rb +15 -0
  27. data/app/views/borutus/accounts/index.html.erb +26 -0
  28. data/app/views/borutus/entries/index.html.erb +59 -0
  29. data/app/views/borutus/reports/_account.html.erb +28 -0
  30. data/app/views/borutus/reports/balance_sheet.html.erb +21 -0
  31. data/app/views/borutus/reports/income_statement.html.erb +24 -0
  32. data/app/views/layouts/borutus/_messages.html.erb +9 -0
  33. data/app/views/layouts/borutus/_navigation.html.erb +19 -0
  34. data/app/views/layouts/borutus/_navigation_links.html.erb +5 -0
  35. data/app/views/layouts/borutus/application.html.erb +19 -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/db/migrate/20160422010135_create_borutus_tables.rb +33 -0
  44. data/lib/borutus.rb +20 -0
  45. data/lib/borutus/engine.rb +5 -0
  46. data/lib/borutus/version.rb +3 -0
  47. data/lib/generators/borutus/USAGE +13 -0
  48. data/lib/generators/borutus/add_date_upgrade_generator.rb +11 -0
  49. data/lib/generators/borutus/base_generator.rb +19 -0
  50. data/lib/generators/borutus/borutus_generator.rb +12 -0
  51. data/lib/generators/borutus/templates/tenant_migration.rb +6 -0
  52. data/lib/generators/borutus/tenancy_generator.rb +12 -0
  53. data/lib/generators/borutus/upgrade_borutus_generator.rb +12 -0
  54. data/spec/controllers/accounts_controller_spec.rb +19 -0
  55. data/spec/controllers/entries_controller_spec.rb +19 -0
  56. data/spec/controllers/reports_controller_spec.rb +24 -0
  57. data/spec/factories/account_factory.rb +35 -0
  58. data/spec/factories/amount_factory.rb +19 -0
  59. data/spec/factories/entry_factory.rb +11 -0
  60. data/spec/lib/borutus_spec.rb +0 -0
  61. data/spec/models/account_spec.rb +140 -0
  62. data/spec/models/amount_spec.rb +13 -0
  63. data/spec/models/asset_spec.rb +7 -0
  64. data/spec/models/credit_amount_spec.rb +7 -0
  65. data/spec/models/debit_amount_spec.rb +7 -0
  66. data/spec/models/entry_spec.rb +170 -0
  67. data/spec/models/equity_spec.rb +7 -0
  68. data/spec/models/expense_spec.rb +7 -0
  69. data/spec/models/liability_spec.rb +7 -0
  70. data/spec/models/revenue_spec.rb +7 -0
  71. data/spec/models/tenancy_spec.rb +45 -0
  72. data/spec/rcov.opts +2 -0
  73. data/spec/routing/accounts_routing_spec.rb +13 -0
  74. data/spec/routing/entries_routing_spec.rb +13 -0
  75. data/spec/spec.opts +4 -0
  76. data/spec/spec_helper.rb +26 -0
  77. data/spec/support/account_shared_examples.rb +60 -0
  78. data/spec/support/active_support_helpers.rb +13 -0
  79. data/spec/support/amount_shared_examples.rb +21 -0
  80. data/spec/support/factory_girl_helpers.rb +8 -0
  81. data/spec/support/shoulda_matchers.rb +8 -0
  82. metadata +243 -0
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe Amount do
5
+
6
+ describe "attributes" do
7
+ it { is_expected.to delegate_method(:name).to(:account).with_prefix }
8
+ end
9
+
10
+ subject { FactoryGirl.build(:amount) }
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 Borutus
4
+ describe Asset do
5
+ it_behaves_like 'a Borutus::Account subtype', kind: :asset, normal_balance: :debit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe CreditAmount do
5
+ it_behaves_like 'a Borutus::Amount subtype', kind: :credit_amount
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe DebitAmount do
5
+ it_behaves_like 'a Borutus::Amount subtype', kind: :debit_amount
6
+ end
7
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
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 Borutus::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 Borutus::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 Borutus::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 Borutus
4
+ describe Equity do
5
+ it_behaves_like 'a Borutus::Account subtype', kind: :equity, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe Expense do
5
+ it_behaves_like 'a Borutus::Account subtype', kind: :expense, normal_balance: :debit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe Liability do
5
+ it_behaves_like 'a Borutus::Account subtype', kind: :liability, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe Revenue do
5
+ it_behaves_like 'a Borutus::Account subtype', kind: :revenue, normal_balance: :credit
6
+ end
7
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
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
+ Borutus.enable_tenancy = true
11
+ Borutus.tenant_class = 'Borutus::Entry'
12
+
13
+ FactoryGirlHelpers.reload()
14
+ Borutus::Asset.new
15
+ end
16
+
17
+ after(:each) do
18
+ if Borutus.const_defined?(:Asset)
19
+ ActiveSupportHelpers.clear_model('Account')
20
+ ActiveSupportHelpers.clear_model('Asset')
21
+ end
22
+
23
+ Borutus.enable_tenancy = false
24
+ Borutus.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
@@ -0,0 +1,2 @@
1
+ --exclude "spec/*,gems/*"
2
+ --rails
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe AccountsController do
5
+ routes { Borutus::Engine.routes }
6
+
7
+ describe "routing" do
8
+ it "recognizes and generates #index" do
9
+ expect(:get => "/accounts").to route_to(:controller => "borutus/accounts", :action => "index")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ module Borutus
4
+ describe EntriesController do
5
+ routes { Borutus::Engine.routes }
6
+
7
+ describe "routing" do
8
+ it "recognizes and generates #index" do
9
+ expect(:get => "/entries").to route_to(:controller => "borutus/entries", :action => "index")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format progress
3
+ --loadby mtime
4
+ --reverse
@@ -0,0 +1,26 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ ENV["RAILS_ENV"] ||= 'test'
5
+ require File.expand_path(File.dirname(__FILE__) + "/../fixture_rails_root/config/environment")
6
+
7
+ require Rails.root.join('db/schema').to_s
8
+ require 'rspec/rails'
9
+
10
+ $: << File.expand_path(File.dirname(__FILE__) + '/../lib/')
11
+ require 'borutus'
12
+ require 'kaminari'
13
+
14
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
15
+
16
+ require 'factory_girl'
17
+ borutus_definitions = File.expand_path(File.join(File.dirname(__FILE__), 'factories'))
18
+ FactoryGirl.definition_file_paths << borutus_definitions
19
+
20
+
21
+ RSpec.configure do |config|
22
+ config.use_transactional_fixtures = true
23
+ config.infer_spec_type_from_file_location!
24
+ end
25
+
26
+ FactoryGirlHelpers.reload()
@@ -0,0 +1,60 @@
1
+ shared_examples_for 'a Borutus::Account subtype' do |elements|
2
+ let(:contra) { false }
3
+ let(:account) { FactoryGirl.create(elements[:kind], contra: contra)}
4
+ subject { account }
5
+
6
+ describe "class methods" do
7
+ subject { account.class }
8
+ its(:balance) { is_expected.to be_kind_of(BigDecimal) }
9
+ describe "trial_balance" do
10
+ it "should raise NoMethodError" do
11
+ expect { subject.trial_balance }.to raise_error NoMethodError
12
+ end
13
+ end
14
+ end
15
+
16
+ describe "instance methods" do
17
+ its(:balance) { is_expected.to be_kind_of(BigDecimal) }
18
+
19
+ it "reports a balance with date range" do
20
+ expect(account.balance(:from_date => "2014-01-01", :to_date => Date.today)).to be_kind_of(BigDecimal)
21
+ end
22
+
23
+ it { is_expected.to respond_to(:credit_entries) }
24
+ it { is_expected.to respond_to(:debit_entries) }
25
+ end
26
+
27
+ it "requires a name" do
28
+ account.name = nil
29
+ expect(account).not_to be_valid
30
+ end
31
+
32
+ # Figure out which way credits and debits should apply
33
+ if elements[:normal_balance] == :debit
34
+ debit_condition = :>
35
+ credit_condition = :<
36
+ else
37
+ credit_condition = :>
38
+ debit_condition = :<
39
+ end
40
+
41
+ describe "when given a debit" do
42
+ before { FactoryGirl.create(:debit_amount, account: account) }
43
+ its(:balance) { is_expected.to be.send(debit_condition, 0) }
44
+
45
+ describe "on a contra account" do
46
+ let(:contra) { true }
47
+ its(:balance) { is_expected.to be.send(credit_condition, 0) }
48
+ end
49
+ end
50
+
51
+ describe "when given a credit" do
52
+ before { FactoryGirl.create(:credit_amount, account: account) }
53
+ its(:balance) { is_expected.to be.send(credit_condition, 0) }
54
+
55
+ describe "on a contra account" do
56
+ let(:contra) { true }
57
+ its(:balance) { is_expected.to be.send(debit_condition, 0) }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveSupportHelpers
2
+ # Helps in removing model, and force-reloading it next time This helper does 2
3
+ # things:
4
+ # * remove from $LOADED_FEATURES so that ruby 'require' reloads file again
5
+ # * remove the constant from active support dependencies
6
+ def self.clear_model(model_name)
7
+ ActiveSupport::Dependencies.remove_constant('Borutus::' + model_name)
8
+
9
+ models_dir = File.dirname(__FILE__) + '/../../app/models/borutus/'
10
+ path = File.expand_path(models_dir + model_name.downcase + '.rb')
11
+ $LOADED_FEATURES.delete(path)
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ shared_examples_for 'a Borutus::Amount subtype' do |elements|
2
+ let(:amount) { FactoryGirl.build(elements[:kind]) }
3
+ subject { amount }
4
+
5
+ it { is_expected.to be_valid }
6
+
7
+ it "should require an amount" do
8
+ amount.amount = nil
9
+ expect(amount).not_to be_valid
10
+ end
11
+
12
+ it "should require a entry" do
13
+ amount.entry = nil
14
+ expect(amount).not_to be_valid
15
+ end
16
+
17
+ it "should require an account" do
18
+ amount.account = nil
19
+ expect(amount).not_to be_valid
20
+ end
21
+ end