double_entry 0.0.1.pre → 0.1.0.pre.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +5 -6
  3. data/.rspec +1 -0
  4. data/.travis.yml +19 -0
  5. data/.yardopts +2 -0
  6. data/Gemfile +0 -1
  7. data/LICENSE.md +19 -0
  8. data/README.md +221 -14
  9. data/Rakefile +12 -0
  10. data/double_entry.gemspec +30 -15
  11. data/gemfiles/Gemfile.rails-3.2.0 +5 -0
  12. data/gemfiles/Gemfile.rails-4.0.0 +5 -0
  13. data/gemfiles/Gemfile.rails-4.1.0 +5 -0
  14. data/lib/active_record/locking_extensions.rb +61 -0
  15. data/lib/double_entry.rb +267 -2
  16. data/lib/double_entry/account.rb +82 -0
  17. data/lib/double_entry/account_balance.rb +31 -0
  18. data/lib/double_entry/aggregate.rb +118 -0
  19. data/lib/double_entry/aggregate_array.rb +65 -0
  20. data/lib/double_entry/configurable.rb +52 -0
  21. data/lib/double_entry/day_range.rb +38 -0
  22. data/lib/double_entry/hour_range.rb +40 -0
  23. data/lib/double_entry/line.rb +147 -0
  24. data/lib/double_entry/line_aggregate.rb +37 -0
  25. data/lib/double_entry/line_check.rb +118 -0
  26. data/lib/double_entry/locking.rb +187 -0
  27. data/lib/double_entry/month_range.rb +92 -0
  28. data/lib/double_entry/reporting.rb +16 -0
  29. data/lib/double_entry/time_range.rb +55 -0
  30. data/lib/double_entry/time_range_array.rb +43 -0
  31. data/lib/double_entry/transfer.rb +70 -0
  32. data/lib/double_entry/version.rb +3 -1
  33. data/lib/double_entry/week_range.rb +99 -0
  34. data/lib/double_entry/year_range.rb +39 -0
  35. data/lib/generators/double_entry/install/install_generator.rb +22 -0
  36. data/lib/generators/double_entry/install/templates/migration.rb +68 -0
  37. data/script/jack_hammer +201 -0
  38. data/script/setup.sh +8 -0
  39. data/spec/active_record/locking_extensions_spec.rb +54 -0
  40. data/spec/double_entry/account_balance_spec.rb +8 -0
  41. data/spec/double_entry/account_spec.rb +23 -0
  42. data/spec/double_entry/aggregate_array_spec.rb +75 -0
  43. data/spec/double_entry/aggregate_spec.rb +168 -0
  44. data/spec/double_entry/double_entry_spec.rb +391 -0
  45. data/spec/double_entry/line_aggregate_spec.rb +8 -0
  46. data/spec/double_entry/line_check_spec.rb +88 -0
  47. data/spec/double_entry/line_spec.rb +72 -0
  48. data/spec/double_entry/locking_spec.rb +154 -0
  49. data/spec/double_entry/month_range_spec.rb +131 -0
  50. data/spec/double_entry/reporting_spec.rb +25 -0
  51. data/spec/double_entry/time_range_array_spec.rb +149 -0
  52. data/spec/double_entry/time_range_spec.rb +43 -0
  53. data/spec/double_entry/week_range_spec.rb +88 -0
  54. data/spec/generators/double_entry/install/install_generator_spec.rb +33 -0
  55. data/spec/spec_helper.rb +47 -0
  56. data/spec/support/accounts.rb +26 -0
  57. data/spec/support/blueprints.rb +34 -0
  58. data/spec/support/database.example.yml +16 -0
  59. data/spec/support/database.travis.yml +18 -0
  60. data/spec/support/double_entry_spec_helper.rb +19 -0
  61. data/spec/support/reporting_configuration.rb +6 -0
  62. data/spec/support/schema.rb +71 -0
  63. metadata +277 -18
  64. data/LICENSE.txt +0 -22
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+ describe DoubleEntry::Reporting do
4
+
5
+ describe "::configure" do
6
+ describe "start_of_business" do
7
+ subject(:start_of_business) { DoubleEntry::Reporting.configuration.start_of_business }
8
+
9
+ context "configured to 2011-03-12" do
10
+ before do
11
+ DoubleEntry::Reporting.configure do |config|
12
+ config.start_of_business = Time.new(2011, 3, 12)
13
+ end
14
+ end
15
+
16
+ it { should eq Time.new(2011, 3, 12) }
17
+ end
18
+
19
+ context "not configured" do
20
+ it { should eq Time.new(1970, 1, 1) }
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,149 @@
1
+ require 'spec_helper'
2
+ describe DoubleEntry::TimeRangeArray do
3
+ describe '.make' do
4
+ subject(:time_range_array) { DoubleEntry::TimeRangeArray.make(range_type, start, finish) }
5
+
6
+ context 'for "hour" range type' do
7
+ let(:range_type) { 'hour' }
8
+
9
+ context 'given start is "2007-05-03 15:00:00" and finish is "2007-05-03 18:00:00"' do
10
+ let(:start) { '2007-05-03 15:00:00' }
11
+ let(:finish) { '2007-05-03 18:00:00' }
12
+ it { should eq [
13
+ DoubleEntry::HourRange.from_time(Time.new(2007, 5, 3, 15)),
14
+ DoubleEntry::HourRange.from_time(Time.new(2007, 5, 3, 16)),
15
+ DoubleEntry::HourRange.from_time(Time.new(2007, 5, 3, 17)),
16
+ DoubleEntry::HourRange.from_time(Time.new(2007, 5, 3, 18)),
17
+ ] }
18
+ end
19
+
20
+ context 'given start and finish are nil' do
21
+ it 'should raise an error' do
22
+ expect { DoubleEntry::TimeRangeArray.make(range_type, nil, nil) }.
23
+ to raise_error 'Must specify range for hour-by-hour reports'
24
+ end
25
+ end
26
+ end
27
+
28
+ context 'for "day" range type' do
29
+ let(:range_type) { 'day' }
30
+
31
+ context 'given start is "2007-05-03" and finish is "2007-05-07"' do
32
+ let(:start) { '2007-05-03' }
33
+ let(:finish) { '2007-05-07' }
34
+ it { should eq [
35
+ DoubleEntry::DayRange.from_time(Time.new(2007, 5, 3)),
36
+ DoubleEntry::DayRange.from_time(Time.new(2007, 5, 4)),
37
+ DoubleEntry::DayRange.from_time(Time.new(2007, 5, 5)),
38
+ DoubleEntry::DayRange.from_time(Time.new(2007, 5, 6)),
39
+ DoubleEntry::DayRange.from_time(Time.new(2007, 5, 7)),
40
+ ] }
41
+ end
42
+
43
+ context 'given start and finish are nil' do
44
+ it 'should raise an error' do
45
+ expect { DoubleEntry::TimeRangeArray.make(range_type, nil, nil) }.
46
+ to raise_error 'Must specify range for day-by-day reports'
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'for "week" range type' do
52
+ let(:range_type) { 'week' }
53
+
54
+ context 'given start is "2007-05-03" and finish is "2007-05-24"' do
55
+ let(:start) { '2007-05-03' }
56
+ let(:finish) { '2007-05-24' }
57
+ it { should eq [
58
+ DoubleEntry::WeekRange.from_time(Time.new(2007, 5, 3)),
59
+ DoubleEntry::WeekRange.from_time(Time.new(2007, 5, 10)),
60
+ DoubleEntry::WeekRange.from_time(Time.new(2007, 5, 17)),
61
+ DoubleEntry::WeekRange.from_time(Time.new(2007, 5, 24)),
62
+ ] }
63
+ end
64
+
65
+ context 'given start and finish are nil' do
66
+ it 'should raise an error' do
67
+ expect { DoubleEntry::TimeRangeArray.make(range_type, nil, nil) }.
68
+ to raise_error 'Must specify range for week-by-week reports'
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'for "month" range type' do
74
+ let(:range_type) { 'month' }
75
+
76
+ context 'given start is "2007-05-03" and finish is "2007-08-24"' do
77
+ let(:start) { '2007-05-03' }
78
+ let(:finish) { '2007-08-24' }
79
+ it { should eq [
80
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 5)),
81
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 6)),
82
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 7)),
83
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 8)),
84
+ ] }
85
+ end
86
+
87
+ context 'given finish is nil' do
88
+ let(:start) { '2006-08-03' }
89
+ let(:finish) { nil }
90
+
91
+ context 'and the date is "2007-04-13"' do
92
+ before { Timecop.freeze(Time.new(2007, 4, 13)) }
93
+
94
+ it { should eq [
95
+ DoubleEntry::MonthRange.from_time(Time.new(2006, 8)),
96
+ DoubleEntry::MonthRange.from_time(Time.new(2006, 9)),
97
+ DoubleEntry::MonthRange.from_time(Time.new(2006, 10)),
98
+ DoubleEntry::MonthRange.from_time(Time.new(2006, 11)),
99
+ DoubleEntry::MonthRange.from_time(Time.new(2006, 12)),
100
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 1)),
101
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 2)),
102
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 3)),
103
+ DoubleEntry::MonthRange.from_time(Time.new(2007, 4)),
104
+ ] }
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'for "year" range type' do
110
+ let(:range_type) { 'year' }
111
+
112
+ context 'given start is "2007-05-03" and finish is "2008-08-24"' do
113
+ let(:start) { '2007-05-03' }
114
+ let(:finish) { '2008-08-24' }
115
+
116
+ context 'and the date is "2009-11-23"' do
117
+ before { Timecop.freeze(Time.new(2009, 11, 23)) }
118
+
119
+ it 'takes no notice of start and finish' do
120
+ should eq [
121
+ DoubleEntry::YearRange.from_time(Time.new(2007)),
122
+ DoubleEntry::YearRange.from_time(Time.new(2008)),
123
+ DoubleEntry::YearRange.from_time(Time.new(2009)),
124
+ ]
125
+ end
126
+
127
+ context 'given finish is nil' do
128
+ let(:start) { '2006-08-03' }
129
+ let(:finish) { nil }
130
+ it {
131
+ should eq [
132
+ DoubleEntry::YearRange.from_time(Time.new(2006)),
133
+ DoubleEntry::YearRange.from_time(Time.new(2007)),
134
+ DoubleEntry::YearRange.from_time(Time.new(2008)),
135
+ DoubleEntry::YearRange.from_time(Time.new(2009)),
136
+ ]
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ context 'given an invalid range type "ueue"' do
144
+ it 'should raise an error' do
145
+ expect { DoubleEntry::TimeRangeArray.make('ueue') }.to raise_error ArgumentError
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe DoubleEntry::TimeRange do
5
+ it 'should correctly calculate a month range' do
6
+ ar = DoubleEntry::TimeRange.make(:year => 2009, :month => 10)
7
+ expect(ar.start.to_s).to eq Time.mktime(2009, 10, 1, 0, 0, 0).to_s
8
+ expect(ar.finish.to_s).to eq Time.mktime(2009, 10, 31, 23, 59, 59).to_s
9
+ end
10
+
11
+ it "should correctly calculate the beginning of the financial year" do
12
+ range = DoubleEntry::TimeRange.make(:year => 2009, :month => 6).beginning_of_financial_year
13
+ expect(range.month).to eq 7
14
+ expect(range.year).to eq 2008
15
+ range = DoubleEntry::TimeRange.make(:year => 2009, :month => 7).beginning_of_financial_year
16
+ expect(range.month).to eq 7
17
+ expect(range.year).to eq 2009
18
+ end
19
+
20
+ it "should correctly calculate the current week range for New Year's Day" do
21
+ Timecop.freeze Time.mktime(2009, 1, 1) do
22
+ expect(DoubleEntry::WeekRange.current.week).to eq 1
23
+ end
24
+ end
25
+
26
+ it "should correctly calculate the current week range for the first Sunday in the year after New Years" do
27
+ Timecop.freeze Time.mktime(2009, 1, 4) do
28
+ expect(DoubleEntry::WeekRange.current.week).to eq 1
29
+ end
30
+ end
31
+
32
+ it "should correctly calculate the current week range for the first Monday in the year after New Years" do
33
+ Timecop.freeze Time.mktime(2009, 1, 5) do
34
+ expect(DoubleEntry::WeekRange.current.week).to eq 2
35
+ end
36
+ end
37
+
38
+ it "should correctly calculate the current week range for my birthday" do
39
+ Timecop.freeze Time.mktime(2009, 3, 27) do
40
+ expect(DoubleEntry::WeekRange.current.week).to eq 13
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe DoubleEntry::WeekRange do
5
+
6
+ it "should start week 1 of a year in the first week that has any day in the year" do
7
+ range = DoubleEntry::WeekRange.new(:year => 2011, :week => 1)
8
+ expect(range.start).to eq Time.parse("2010-12-27 00:00:00")
9
+ end
10
+
11
+ it "should handle times in the last week of the year properly" do
12
+ range = DoubleEntry::WeekRange.from_time(Time.parse("2010-12-29 11:30:00"))
13
+ expect(range.year).to eq 2011
14
+ expect(range.week).to eq 1
15
+ expect(range.start).to eq Time.parse("2010-12-27 00:00:00")
16
+ end
17
+
18
+ describe "::from_time" do
19
+ subject(:from_time) { DoubleEntry::WeekRange.from_time(given_time) }
20
+
21
+ context "given the Time 31st March 2012" do
22
+ let(:given_time) { Time.new(2012, 3, 31) }
23
+ its(:year) { should eq 2012 }
24
+ its(:week) { should eq 14 }
25
+ end
26
+
27
+ context "given the Date 31st March 2012" do
28
+ let(:given_time) { Date.parse("2012-03-31") }
29
+ its(:year) { should eq 2012 }
30
+ its(:week) { should eq 14 }
31
+ end
32
+ end
33
+
34
+ describe "::reportable_weeks" do
35
+ subject(:reportable_weeks) { DoubleEntry::WeekRange.reportable_weeks }
36
+
37
+ context "The date is 1st Feb 1970" do
38
+ before { Timecop.freeze(Time.new(1970, 2, 1)) }
39
+
40
+ it { should eq [
41
+ DoubleEntry::WeekRange.new(year: 1970, week: 1),
42
+ DoubleEntry::WeekRange.new(year: 1970, week: 2),
43
+ DoubleEntry::WeekRange.new(year: 1970, week: 3),
44
+ DoubleEntry::WeekRange.new(year: 1970, week: 4),
45
+ DoubleEntry::WeekRange.new(year: 1970, week: 5),
46
+ ] }
47
+
48
+ context "My business started on 25th Jan 1970" do
49
+ before do
50
+ DoubleEntry::Reporting.configure do |config|
51
+ config.start_of_business = Time.new(1970, 1, 25)
52
+ end
53
+ end
54
+
55
+ it { should eq [
56
+ DoubleEntry::WeekRange.new(year: 1970, week: 4),
57
+ DoubleEntry::WeekRange.new(year: 1970, week: 5),
58
+ ] }
59
+ end
60
+ end
61
+
62
+ context "The date is 1st Jan 1970" do
63
+ before { Timecop.freeze(Time.new(1970, 1, 1)) }
64
+
65
+ it { should eq [ DoubleEntry::WeekRange.new(year: 1970, week: 1) ] }
66
+ end
67
+
68
+ context "Given a start time of 3rd Dec 1982" do
69
+ subject(:reportable_weeks) { DoubleEntry::WeekRange.reportable_weeks(from: Time.new(1982, 12, 3)) }
70
+
71
+ context "The date is 12nd Jan 1983" do
72
+ before { Timecop.freeze(Time.new(1983, 2, 2)) }
73
+ it { should eq [
74
+ DoubleEntry::WeekRange.new(year: 1982, week: 49),
75
+ DoubleEntry::WeekRange.new(year: 1982, week: 50),
76
+ DoubleEntry::WeekRange.new(year: 1982, week: 51),
77
+ DoubleEntry::WeekRange.new(year: 1982, week: 52),
78
+ DoubleEntry::WeekRange.new(year: 1983, week: 1),
79
+ DoubleEntry::WeekRange.new(year: 1983, week: 2),
80
+ DoubleEntry::WeekRange.new(year: 1983, week: 3),
81
+ DoubleEntry::WeekRange.new(year: 1983, week: 4),
82
+ DoubleEntry::WeekRange.new(year: 1983, week: 5),
83
+ DoubleEntry::WeekRange.new(year: 1983, week: 6),
84
+ ] }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'action_controller'
3
+ require 'generator_spec/test_case'
4
+ require 'generators/double_entry/install/install_generator'
5
+
6
+
7
+ describe DoubleEntry::Generators::InstallGenerator do
8
+ include GeneratorSpec::TestCase
9
+
10
+ destination File.expand_path("../../../../../tmp", __FILE__)
11
+
12
+ before do
13
+ prepare_destination
14
+ run_generator
15
+ end
16
+
17
+ specify do
18
+ expect(destination_root).to have_structure {
19
+ directory "db" do
20
+ directory "migrate" do
21
+ migration "create_double_entry_tables" do
22
+ contains 'class CreateDoubleEntryTable'
23
+ contains 'create_table "double_entry_account_balances"'
24
+ contains 'create_table "double_entry_lines"'
25
+ contains 'create_table "double_entry_line_aggregates"'
26
+ contains 'create_table "double_entry_line_checks"'
27
+ end
28
+ end
29
+ end
30
+ }
31
+ end
32
+
33
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ require 'bundler/setup'
3
+ require 'active_record'
4
+ require 'active_support'
5
+
6
+ ENV['DB'] ||= 'mysql'
7
+ ActiveRecord::Base.establish_connection YAML.load_file(File.expand_path("../support/database.yml", __FILE__))[ENV['DB']]
8
+
9
+ FileUtils.mkdir_p 'log'
10
+ FileUtils.rm 'log/test.log', :force => true
11
+
12
+ # Buffered Logger was deprecated in ActiveSupport 4.0.0 and was removed in 4.1.0
13
+ # Logger was added in ActiveSupport 4.0.0
14
+ if defined? ActiveSupport::Logger
15
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new('log/test.log')
16
+ else
17
+ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new('log/test.log')
18
+ end
19
+
20
+ I18n.config.enforce_available_locales = false
21
+
22
+ require 'double_entry'
23
+ require 'rspec'
24
+ require 'rspec/its'
25
+ require 'database_cleaner'
26
+ require 'machinist/active_record'
27
+ require 'timecop'
28
+
29
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each { |f| require f }
30
+
31
+ RSpec.configure do |config|
32
+ config.include DoubleEntrySpecHelper
33
+
34
+ config.before(:suite) do
35
+ DatabaseCleaner.strategy = :deletion
36
+ DatabaseCleaner.clean_with(:deletion)
37
+ end
38
+
39
+ config.before(:each) do
40
+ DatabaseCleaner.start
41
+ end
42
+
43
+ config.after(:each) do
44
+ Timecop.return
45
+ DatabaseCleaner.clean
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ # These make it easier to quickly set up account balances for testing.
3
+
4
+ # user scoping magic, accepts a User, Fixnum, or String
5
+ user_scope = lambda do |user_identifier|
6
+ if user_identifier.is_a?(Fixnum) or user_identifier.is_a?(String)
7
+ user_identifier
8
+ elsif user_identifier.is_a?(User)
9
+ user_identifier.id
10
+ else
11
+ raise "unknown type expected fixnum, string or user, got: #{user_identifier.inspect}"
12
+ end
13
+ end
14
+
15
+ # A set of accounts to test with
16
+ DoubleEntry.accounts = DoubleEntry::Account::Set.new.tap do |accounts|
17
+ accounts << DoubleEntry::Account.new(:identifier => :savings, :scope_identifier => user_scope, :positive_only => true)
18
+ accounts << DoubleEntry::Account.new(:identifier => :checking, :scope_identifier => user_scope, :positive_only => true)
19
+ accounts << DoubleEntry::Account.new(:identifier => :test, :scope_identifier => user_scope)
20
+ end
21
+
22
+ # A set of allowed transfers between accounts
23
+ DoubleEntry.transfers = DoubleEntry::Transfer::Set.new.tap do |transfers|
24
+ transfers << DoubleEntry::Transfer.new(:from => :test, :to => :savings, :code => :bonus)
25
+ transfers << DoubleEntry::Transfer.new(:from => :test, :to => :checking, :code => :pay)
26
+ end
@@ -0,0 +1,34 @@
1
+ class UserBlueprint < Machinist::ActiveRecord::Blueprint
2
+ def make!(attributes = {})
3
+ savings_balance = attributes.delete(:savings_balance)
4
+ checking_balance = attributes.delete(:checking_balance)
5
+ user = super(attributes)
6
+ if savings_balance
7
+ DoubleEntry.transfer(
8
+ savings_balance,
9
+ :from => DoubleEntry.account(:test, :scope => user),
10
+ :to => DoubleEntry.account(:savings, :scope => user),
11
+ :code => :bonus,
12
+ )
13
+ end
14
+ if checking_balance
15
+ DoubleEntry.transfer(
16
+ checking_balance,
17
+ :from => DoubleEntry.account(:test, :scope => user),
18
+ :to => DoubleEntry.account(:checking, :scope => user),
19
+ :code => :pay,
20
+ )
21
+ end
22
+ user
23
+ end
24
+ end
25
+
26
+ class User < ActiveRecord::Base
27
+ def self.blueprint_class
28
+ UserBlueprint
29
+ end
30
+ end
31
+
32
+ User.blueprint do
33
+ username { "user#{sn}" }
34
+ end