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

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