borutus 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +346 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/borutus/application.js +16 -0
- data/app/assets/javascripts/borutus/reports.js +5 -0
- data/app/assets/stylesheets/bootstrap-theme.min.css +5 -0
- data/app/assets/stylesheets/bootstrap.min.css +5 -0
- data/app/assets/stylesheets/borutus/application.css +19 -0
- data/app/controllers/borutus/accounts_controller.rb +30 -0
- data/app/controllers/borutus/application_controller.rb +4 -0
- data/app/controllers/borutus/entries_controller.rb +34 -0
- data/app/controllers/borutus/reports_controller.rb +40 -0
- data/app/models/borutus/account.rb +180 -0
- data/app/models/borutus/amount.rb +24 -0
- data/app/models/borutus/amounts_extension.rb +42 -0
- data/app/models/borutus/asset.rb +56 -0
- data/app/models/borutus/credit_amount.rb +10 -0
- data/app/models/borutus/debit_amount.rb +10 -0
- data/app/models/borutus/entry.rb +77 -0
- data/app/models/borutus/equity.rb +56 -0
- data/app/models/borutus/expense.rb +56 -0
- data/app/models/borutus/liability.rb +56 -0
- data/app/models/borutus/no_tenancy.rb +9 -0
- data/app/models/borutus/revenue.rb +56 -0
- data/app/models/borutus/tenancy.rb +15 -0
- data/app/views/borutus/accounts/index.html.erb +26 -0
- data/app/views/borutus/entries/index.html.erb +59 -0
- data/app/views/borutus/reports/_account.html.erb +28 -0
- data/app/views/borutus/reports/balance_sheet.html.erb +21 -0
- data/app/views/borutus/reports/income_statement.html.erb +24 -0
- data/app/views/layouts/borutus/_messages.html.erb +9 -0
- data/app/views/layouts/borutus/_navigation.html.erb +19 -0
- data/app/views/layouts/borutus/_navigation_links.html.erb +5 -0
- data/app/views/layouts/borutus/application.html.erb +19 -0
- data/config/backtrace_silencers.rb +7 -0
- data/config/database.yml +5 -0
- data/config/inflections.rb +10 -0
- data/config/mime_types.rb +5 -0
- data/config/routes.rb +9 -0
- data/config/secret_token.rb +7 -0
- data/config/session_store.rb +8 -0
- data/db/migrate/20160422010135_create_borutus_tables.rb +33 -0
- data/lib/borutus.rb +20 -0
- data/lib/borutus/engine.rb +5 -0
- data/lib/borutus/version.rb +3 -0
- data/lib/generators/borutus/USAGE +13 -0
- data/lib/generators/borutus/add_date_upgrade_generator.rb +11 -0
- data/lib/generators/borutus/base_generator.rb +19 -0
- data/lib/generators/borutus/borutus_generator.rb +12 -0
- data/lib/generators/borutus/templates/tenant_migration.rb +6 -0
- data/lib/generators/borutus/tenancy_generator.rb +12 -0
- data/lib/generators/borutus/upgrade_borutus_generator.rb +12 -0
- data/spec/controllers/accounts_controller_spec.rb +19 -0
- data/spec/controllers/entries_controller_spec.rb +19 -0
- data/spec/controllers/reports_controller_spec.rb +24 -0
- data/spec/factories/account_factory.rb +35 -0
- data/spec/factories/amount_factory.rb +19 -0
- data/spec/factories/entry_factory.rb +11 -0
- data/spec/lib/borutus_spec.rb +0 -0
- data/spec/models/account_spec.rb +140 -0
- data/spec/models/amount_spec.rb +13 -0
- data/spec/models/asset_spec.rb +7 -0
- data/spec/models/credit_amount_spec.rb +7 -0
- data/spec/models/debit_amount_spec.rb +7 -0
- data/spec/models/entry_spec.rb +170 -0
- data/spec/models/equity_spec.rb +7 -0
- data/spec/models/expense_spec.rb +7 -0
- data/spec/models/liability_spec.rb +7 -0
- data/spec/models/revenue_spec.rb +7 -0
- data/spec/models/tenancy_spec.rb +45 -0
- data/spec/rcov.opts +2 -0
- data/spec/routing/accounts_routing_spec.rb +13 -0
- data/spec/routing/entries_routing_spec.rb +13 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/account_shared_examples.rb +60 -0
- data/spec/support/active_support_helpers.rb +13 -0
- data/spec/support/amount_shared_examples.rb +21 -0
- data/spec/support/factory_girl_helpers.rb +8 -0
- data/spec/support/shoulda_matchers.rb +8 -0
- 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,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,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
|
data/spec/rcov.opts
ADDED
@@ -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
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|