borutus 0.2.1 → 0.2.2
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 +4 -4
- data/README.md +62 -64
- data/app/models/borutus/account.rb +97 -86
- data/app/models/borutus/amount.rb +9 -4
- data/app/models/borutus/amounts_extension.rb +1 -1
- data/app/services/borutus/accounts/build_running_balance_query.rb +73 -0
- data/db/migrate/20191025095830_add_borutus_amount_counter_cache.rb +15 -0
- data/lib/borutus.rb +1 -0
- data/lib/borutus/version.rb +1 -1
- data/spec/controllers/accounts_controller_spec.rb +1 -1
- data/spec/controllers/entries_controller_spec.rb +1 -1
- data/spec/controllers/reports_controller_spec.rb +1 -1
- data/spec/factories/account_factory.rb +13 -13
- data/spec/factories/amount_factory.rb +13 -13
- data/spec/factories/entry_factory.rb +11 -8
- data/spec/models/account_spec.rb +98 -53
- data/spec/models/amount_spec.rb +1 -1
- data/spec/models/entry_spec.rb +54 -50
- data/spec/models/tenancy_spec.rb +6 -6
- data/spec/routing/entries_routing_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -14
- data/spec/support/account_shared_examples.rb +3 -3
- data/spec/support/amount_shared_examples.rb +1 -1
- data/spec/support/factory_bot_helpers.rb +8 -0
- metadata +44 -25
- data/spec/support/factory_girl_helpers.rb +0 -8
@@ -1,13 +1,19 @@
|
|
1
1
|
module Borutus
|
2
|
+
|
2
3
|
# The Amount class represents debit and credit amounts in the system.
|
3
4
|
#
|
4
5
|
# @abstract
|
5
|
-
# An amount must be a subclass as either a debit or a credit to be saved
|
6
|
+
# An amount must be a subclass as either a debit or a credit to be saved
|
7
|
+
# to the database.
|
6
8
|
#
|
7
9
|
# @author Michael Bulat
|
8
10
|
class Amount < ActiveRecord::Base
|
9
|
-
|
10
|
-
belongs_to :
|
11
|
+
|
12
|
+
belongs_to :entry, class_name: "Borutus::Entry"
|
13
|
+
belongs_to(:account, {
|
14
|
+
class_name: "Borutus::Account",
|
15
|
+
counter_cache: :borutus_amounts_count,
|
16
|
+
})
|
11
17
|
|
12
18
|
validates_presence_of :type, :amount, :entry, :account
|
13
19
|
# attr_accessible :account, :account_name, :amount, :entry
|
@@ -19,6 +25,5 @@ module Borutus
|
|
19
25
|
self.account = Account.find_by_name!(name)
|
20
26
|
end
|
21
27
|
|
22
|
-
protected
|
23
28
|
end
|
24
29
|
end
|
@@ -30,7 +30,7 @@ module Borutus
|
|
30
30
|
#
|
31
31
|
# Since this does not use the database for sumation, it may be used on non-persisted records.
|
32
32
|
def balance_for_new_record
|
33
|
-
balance = BigDecimal
|
33
|
+
balance = BigDecimal('0')
|
34
34
|
each do |amount_record|
|
35
35
|
if amount_record.amount && !amount_record.marked_for_destruction?
|
36
36
|
balance += amount_record.amount # unless amount_record.marked_for_destruction?
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Borutus
|
2
|
+
module Accounts
|
3
|
+
class BuildRunningBalanceQuery
|
4
|
+
|
5
|
+
STATEMENTS = {
|
6
|
+
coalesce_debit: %{ COALESCE("debit_table"."amount", 0) },
|
7
|
+
coalesce_credit: %{ COALESCE("credit_table"."amount", 0) },
|
8
|
+
amount_sum: %{ SUM("borutus_amounts".amount) AS amount },
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
extend LightService::Action
|
12
|
+
|
13
|
+
expects :account
|
14
|
+
promises :joins_statement, :select_statement, :group_statement
|
15
|
+
|
16
|
+
executed do |c|
|
17
|
+
account = c.account
|
18
|
+
credit_amounts = account.credit_amounts
|
19
|
+
debit_amounts = account.debit_amounts
|
20
|
+
|
21
|
+
credit_table = credit_amounts.joins(:entry).select(
|
22
|
+
:id,
|
23
|
+
:entry_id,
|
24
|
+
STATEMENTS[:amount_sum],
|
25
|
+
).group(:entry_id, :id)
|
26
|
+
|
27
|
+
debit_table = debit_amounts.joins(:entry).select(
|
28
|
+
:id,
|
29
|
+
:entry_id,
|
30
|
+
STATEMENTS[:amount_sum],
|
31
|
+
).group(:entry_id, :id)
|
32
|
+
|
33
|
+
sum_statement = sum_statement_from(account)
|
34
|
+
|
35
|
+
c.joins_statement = joins_statement_from(credit_table, debit_table)
|
36
|
+
c.select_statement = select_statement_from(sum_statement)
|
37
|
+
c.group_statement = group_statement_from
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.sum_statement_from(account)
|
41
|
+
if account.normal_credit_balance
|
42
|
+
%( #{STATEMENTS[:coalesce_credit]} - #{STATEMENTS[:coalesce_debit]} )
|
43
|
+
else
|
44
|
+
%( #{STATEMENTS[:coalesce_debit]} - #{STATEMENTS[:coalesce_credit]} )
|
45
|
+
end
|
46
|
+
end
|
47
|
+
private_class_method :sum_statement_from
|
48
|
+
|
49
|
+
def self.joins_statement_from(credit_table, debit_table)
|
50
|
+
%{
|
51
|
+
LEFT OUTER JOIN (#{credit_table.to_sql}) AS "credit_table" ON "credit_table".entry_id = "borutus_entries".id
|
52
|
+
LEFT OUTER JOIN (#{debit_table.to_sql}) AS "debit_table" ON "debit_table".entry_id = "borutus_entries".id
|
53
|
+
}
|
54
|
+
end
|
55
|
+
private_class_method :joins_statement_from
|
56
|
+
|
57
|
+
def self.select_statement_from(sum_statement)
|
58
|
+
%{
|
59
|
+
"borutus_entries".*,
|
60
|
+
SUM(#{sum_statement}) OVER(ORDER BY "borutus_entries"."created_at") AS balance,
|
61
|
+
#{sum_statement} AS change_amount
|
62
|
+
}
|
63
|
+
end
|
64
|
+
private_class_method :select_statement_from
|
65
|
+
|
66
|
+
def self.group_statement_from
|
67
|
+
%( "borutus_entries".id, "debit_table".amount, "credit_table".amount )
|
68
|
+
end
|
69
|
+
private_class_method :group_statement_from
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddBorutusAmountCounterCache < ActiveRecord::Migration[4.2]
|
2
|
+
|
3
|
+
def up
|
4
|
+
add_column :borutus_accounts, :borutus_amounts_count, :integer
|
5
|
+
|
6
|
+
Borutus::Account.all.pluck(:id).each do |id|
|
7
|
+
Borutus::Account.reset_counters(id, :borutus_amounts)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def down
|
12
|
+
remove_column :borutus_accounts, :borutus_amounts_count
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/lib/borutus.rb
CHANGED
data/lib/borutus/version.rb
CHANGED
@@ -1,32 +1,32 @@
|
|
1
|
-
|
2
|
-
factory :account, :
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :account, class: Borutus::Account do |account|
|
3
3
|
account.name
|
4
|
-
account.contra false
|
4
|
+
account.contra { false }
|
5
5
|
end
|
6
6
|
|
7
|
-
factory :asset, :
|
7
|
+
factory :asset, class: Borutus::Asset do |account|
|
8
8
|
account.name
|
9
|
-
account.contra false
|
9
|
+
account.contra { false }
|
10
10
|
end
|
11
11
|
|
12
|
-
factory :equity, :
|
12
|
+
factory :equity, class: Borutus::Equity do |account|
|
13
13
|
account.name
|
14
|
-
account.contra false
|
14
|
+
account.contra { false }
|
15
15
|
end
|
16
16
|
|
17
|
-
factory :expense, :
|
17
|
+
factory :expense, class: Borutus::Expense do |account|
|
18
18
|
account.name
|
19
|
-
account.contra false
|
19
|
+
account.contra { false }
|
20
20
|
end
|
21
21
|
|
22
|
-
factory :liability, :
|
22
|
+
factory :liability, class: Borutus::Liability do |account|
|
23
23
|
account.name
|
24
|
-
account.contra false
|
24
|
+
account.contra { false }
|
25
25
|
end
|
26
26
|
|
27
|
-
factory :revenue, :
|
27
|
+
factory :revenue, class: Borutus::Revenue do |account|
|
28
28
|
account.name
|
29
|
-
account.contra false
|
29
|
+
account.contra { false }
|
30
30
|
end
|
31
31
|
|
32
32
|
sequence :name do |n|
|
@@ -1,19 +1,19 @@
|
|
1
|
-
|
2
|
-
factory :amount, :
|
3
|
-
amount.amount BigDecimal
|
4
|
-
amount.association :entry, :
|
5
|
-
amount.association :account, :
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :amount, class: Borutus::Amount do |amount|
|
3
|
+
amount.amount { BigDecimal("473") }
|
4
|
+
amount.association :entry, factory: :entry_with_credit_and_debit
|
5
|
+
amount.association :account, factory: :asset
|
6
6
|
end
|
7
7
|
|
8
|
-
factory :credit_amount, :
|
9
|
-
credit_amount.amount BigDecimal
|
10
|
-
credit_amount.association :entry, :
|
11
|
-
credit_amount.association :account, :
|
8
|
+
factory :credit_amount, class: Borutus::CreditAmount do |credit_amount|
|
9
|
+
credit_amount.amount { BigDecimal("473") }
|
10
|
+
credit_amount.association :entry, factory: :entry_with_credit_and_debit
|
11
|
+
credit_amount.association :account, factory: :revenue
|
12
12
|
end
|
13
13
|
|
14
|
-
factory :debit_amount, :
|
15
|
-
debit_amount.amount BigDecimal
|
16
|
-
debit_amount.association :entry, :
|
17
|
-
debit_amount.association :account, :
|
14
|
+
factory :debit_amount, class: Borutus::DebitAmount do |debit_amount|
|
15
|
+
debit_amount.amount { BigDecimal("473") }
|
16
|
+
debit_amount.association :entry, factory: :entry_with_credit_and_debit
|
17
|
+
debit_amount.association :account, factory: :asset
|
18
18
|
end
|
19
19
|
end
|
@@ -1,11 +1,14 @@
|
|
1
|
-
|
2
|
-
factory :entry, :
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
FactoryBot.define do
|
2
|
+
factory :entry, class: Borutus::Entry do
|
3
|
+
description { "factory description" }
|
4
|
+
end
|
5
|
+
|
6
|
+
factory :entry_with_credit_and_debit, class: Borutus::Entry do
|
7
|
+
description { "factory description" }
|
8
|
+
|
9
|
+
after(:build) do |entry|
|
10
|
+
entry.credit_amounts << FactoryBot.build(:credit_amount, entry: entry)
|
11
|
+
entry.debit_amounts << FactoryBot.build(:debit_amount, entry: entry)
|
9
12
|
end
|
10
13
|
end
|
11
14
|
end
|
data/spec/models/account_spec.rb
CHANGED
@@ -1,39 +1,40 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
module Borutus
|
4
4
|
describe Account do
|
5
|
-
|
5
|
+
|
6
|
+
let(:account) { FactoryBot.build(:account) }
|
6
7
|
subject { account }
|
7
8
|
|
8
|
-
it { is_expected.not_to be_valid }
|
9
|
+
it { is_expected.not_to be_valid } # must construct a child type instead
|
9
10
|
|
10
11
|
describe ".types" do
|
11
12
|
it "lists the available types" do
|
12
|
-
expect(described_class.types)
|
13
|
-
to match_array([Asset, Equity, Expense, Liability, Revenue])
|
13
|
+
expect(described_class.types)
|
14
|
+
.to match_array([Asset, Equity, Expense, Liability, Revenue])
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
describe ".entries.with_running_balance" do
|
18
|
-
let(:mock_document) {
|
19
|
+
let(:mock_document) { FactoryBot.create(:asset) }
|
19
20
|
let!(:accounts_receivable) do
|
20
|
-
|
21
|
+
FactoryBot.create(:asset, name: "Accounts Receivable")
|
21
22
|
end
|
22
23
|
let!(:sales_revenue) do
|
23
|
-
|
24
|
+
FactoryBot.create(:revenue, name: "Sales Revenue")
|
24
25
|
end
|
25
26
|
let!(:sales_tax_payable) do
|
26
|
-
|
27
|
+
FactoryBot.create(:liability, name: "Sales Tax Payable")
|
27
28
|
end
|
28
29
|
let!(:entry_1) do
|
29
30
|
Borutus::Entry.new({
|
30
31
|
description: "Sold some widgets",
|
31
32
|
commercial_document: mock_document,
|
32
|
-
debits: [{account: accounts_receivable, amount: 50}],
|
33
|
+
debits: [{ account: accounts_receivable, amount: 50 }],
|
33
34
|
credits: [
|
34
|
-
{account: sales_revenue, amount: 45},
|
35
|
-
{account: sales_tax_payable, amount: 5}
|
36
|
-
]
|
35
|
+
{ account: sales_revenue, amount: 45 },
|
36
|
+
{ account: sales_tax_payable, amount: 5 },
|
37
|
+
],
|
37
38
|
})
|
38
39
|
end
|
39
40
|
let!(:entry_2) do
|
@@ -41,20 +42,20 @@ module Borutus
|
|
41
42
|
description: "Cancel Accounts receivable some widgets again",
|
42
43
|
commercial_document: mock_document,
|
43
44
|
debits: [
|
44
|
-
{account: accounts_receivable, amount: -30},
|
45
|
+
{ account: accounts_receivable, amount: -30 },
|
45
46
|
],
|
46
47
|
credits: [
|
47
|
-
{account: sales_revenue, amount: -25},
|
48
|
-
{account: sales_tax_payable, amount: -5},
|
49
|
-
]
|
48
|
+
{ account: sales_revenue, amount: -25 },
|
49
|
+
{ account: sales_tax_payable, amount: -5 },
|
50
|
+
],
|
50
51
|
})
|
51
52
|
end
|
52
53
|
let!(:entry_3) do
|
53
54
|
Borutus::Entry.new({
|
54
55
|
description: "Cancel Accounts receivable",
|
55
56
|
commercial_document: mock_document,
|
56
|
-
debits: [{account: sales_tax_payable, amount: 15}],
|
57
|
-
credits: [{account: accounts_receivable, amount: 15}],
|
57
|
+
debits: [{ account: sales_tax_payable, amount: 15 }],
|
58
|
+
credits: [{ account: accounts_receivable, amount: 15 }],
|
58
59
|
})
|
59
60
|
end
|
60
61
|
|
@@ -84,12 +85,56 @@ module Borutus
|
|
84
85
|
end
|
85
86
|
end
|
86
87
|
|
88
|
+
describe ".with_amounts" do
|
89
|
+
let(:mock_document) { FactoryBot.create(:asset) }
|
90
|
+
let!(:cash) do
|
91
|
+
FactoryBot.create(:asset, name: "Cash")
|
92
|
+
end
|
93
|
+
let!(:accounts_receivable) do
|
94
|
+
FactoryBot.create(:asset, name: "Accounts Receivable")
|
95
|
+
end
|
96
|
+
let!(:sales_revenue) do
|
97
|
+
FactoryBot.create(:revenue, name: "Sales Revenue")
|
98
|
+
end
|
99
|
+
let!(:sales_tax_payable) do
|
100
|
+
FactoryBot.create(:liability, name: "Sales Tax Payable")
|
101
|
+
end
|
102
|
+
let!(:entry_1) do
|
103
|
+
Borutus::Entry.new({
|
104
|
+
description: "Sold some widgets",
|
105
|
+
commercial_document: mock_document,
|
106
|
+
debits: [{ account: accounts_receivable, amount: 50 }],
|
107
|
+
credits: [
|
108
|
+
{ account: sales_revenue, amount: 45 },
|
109
|
+
{ account: sales_tax_payable, amount: 5 },
|
110
|
+
],
|
111
|
+
})
|
112
|
+
end
|
113
|
+
|
114
|
+
it "only returns with borutus_amounts_count > 0" do
|
115
|
+
entry_1.save
|
116
|
+
|
117
|
+
accounts = Borutus::Account.with_amounts
|
118
|
+
expect(accounts.count).to eq 3
|
119
|
+
|
120
|
+
account_names = accounts.pluck(:name)
|
121
|
+
|
122
|
+
[
|
123
|
+
"Accounts Receivable",
|
124
|
+
"Sales Revenue",
|
125
|
+
"Sales Tax Payable",
|
126
|
+
].each do |account_name|
|
127
|
+
expect(account_names).to include account_name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
87
132
|
describe "when using a child type" do
|
88
|
-
let(:account) {
|
133
|
+
let(:account) { FactoryBot.create(:account, type: "Finance::Asset") }
|
89
134
|
it { is_expected.to be_valid }
|
90
135
|
|
91
136
|
it "should be unique per name" do
|
92
|
-
conflict =
|
137
|
+
conflict = FactoryBot.build(:account, name: account.name, type: account.type)
|
93
138
|
expect(conflict).not_to be_valid
|
94
139
|
expect(conflict.errors[:name]).to eq(["has already been taken"])
|
95
140
|
end
|
@@ -114,36 +159,36 @@ module Borutus
|
|
114
159
|
context "when given correct entries" do
|
115
160
|
before {
|
116
161
|
# credit accounts
|
117
|
-
liability =
|
118
|
-
equity =
|
119
|
-
revenue =
|
120
|
-
contra_asset =
|
121
|
-
contra_expense =
|
162
|
+
liability = FactoryBot.create(:liability)
|
163
|
+
equity = FactoryBot.create(:equity)
|
164
|
+
revenue = FactoryBot.create(:revenue)
|
165
|
+
contra_asset = FactoryBot.create(:asset, contra: true)
|
166
|
+
contra_expense = FactoryBot.create(:expense, contra: true)
|
122
167
|
# credit amounts
|
123
|
-
ca1 =
|
124
|
-
ca2 =
|
125
|
-
ca3 =
|
126
|
-
ca4 =
|
127
|
-
ca5 =
|
168
|
+
ca1 = FactoryBot.build(:credit_amount, account: liability, amount: 100_000)
|
169
|
+
ca2 = FactoryBot.build(:credit_amount, account: equity, amount: 1000)
|
170
|
+
ca3 = FactoryBot.build(:credit_amount, account: revenue, amount: 40_404)
|
171
|
+
ca4 = FactoryBot.build(:credit_amount, account: contra_asset, amount: 2)
|
172
|
+
ca5 = FactoryBot.build(:credit_amount, account: contra_expense, amount: 333)
|
128
173
|
|
129
174
|
# debit accounts
|
130
|
-
asset =
|
131
|
-
expense =
|
132
|
-
contra_liability =
|
133
|
-
contra_equity =
|
134
|
-
contra_revenue =
|
175
|
+
asset = FactoryBot.create(:asset)
|
176
|
+
expense = FactoryBot.create(:expense)
|
177
|
+
contra_liability = FactoryBot.create(:liability, contra: true)
|
178
|
+
contra_equity = FactoryBot.create(:equity, contra: true)
|
179
|
+
contra_revenue = FactoryBot.create(:revenue, contra: true)
|
135
180
|
# debit amounts
|
136
|
-
da1 =
|
137
|
-
da2 =
|
138
|
-
da3 =
|
139
|
-
da4 =
|
140
|
-
da5 =
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
181
|
+
da1 = FactoryBot.build(:debit_amount, account: asset, amount: 100_000)
|
182
|
+
da2 = FactoryBot.build(:debit_amount, account: expense, amount: 1000)
|
183
|
+
da3 = FactoryBot.build(:debit_amount, account: contra_liability, amount: 40_404)
|
184
|
+
da4 = FactoryBot.build(:debit_amount, account: contra_equity, amount: 2)
|
185
|
+
da5 = FactoryBot.build(:debit_amount, account: contra_revenue, amount: 333)
|
186
|
+
|
187
|
+
FactoryBot.create(:entry, credit_amounts: [ca1], debit_amounts: [da1])
|
188
|
+
FactoryBot.create(:entry, credit_amounts: [ca2], debit_amounts: [da2])
|
189
|
+
FactoryBot.create(:entry, credit_amounts: [ca3], debit_amounts: [da3])
|
190
|
+
FactoryBot.create(:entry, credit_amounts: [ca4], debit_amounts: [da4])
|
191
|
+
FactoryBot.create(:entry, credit_amounts: [ca5], debit_amounts: [da5])
|
147
192
|
}
|
148
193
|
|
149
194
|
it { is_expected.to eq(0) }
|
@@ -152,9 +197,9 @@ module Borutus
|
|
152
197
|
|
153
198
|
describe "#amounts" do
|
154
199
|
it "returns all credit and debit amounts" do
|
155
|
-
equity =
|
156
|
-
asset =
|
157
|
-
expense =
|
200
|
+
equity = FactoryBot.create(:equity)
|
201
|
+
asset = FactoryBot.create(:asset)
|
202
|
+
expense = FactoryBot.create(:expense)
|
158
203
|
|
159
204
|
investment = Entry.new(
|
160
205
|
description: "Initial investment",
|
@@ -180,9 +225,9 @@ module Borutus
|
|
180
225
|
|
181
226
|
describe "#entries" do
|
182
227
|
it "returns all credit and debit entries" do
|
183
|
-
equity =
|
184
|
-
asset =
|
185
|
-
expense =
|
228
|
+
equity = FactoryBot.create(:equity)
|
229
|
+
asset = FactoryBot.create(:asset)
|
230
|
+
expense = FactoryBot.create(:expense)
|
186
231
|
|
187
232
|
investment = Entry.new(
|
188
233
|
description: "Initial investment",
|