keepr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +62 -0
  7. data/Rakefile +6 -0
  8. data/ci/Gemfile-rails-4-1 +11 -0
  9. data/ci/Gemfile-rails-4-2 +11 -0
  10. data/keepr.gemspec +31 -0
  11. data/lib/generators/keepr/migration/migration_generator.rb +23 -0
  12. data/lib/generators/keepr/migration/templates/migration.rb +72 -0
  13. data/lib/keepr.rb +15 -0
  14. data/lib/keepr/account.rb +120 -0
  15. data/lib/keepr/active_record_extension.rb +27 -0
  16. data/lib/keepr/cost_center.rb +8 -0
  17. data/lib/keepr/group.rb +33 -0
  18. data/lib/keepr/groups_creator.rb +51 -0
  19. data/lib/keepr/groups_creator/asset.txt +36 -0
  20. data/lib/keepr/groups_creator/liability.txt +28 -0
  21. data/lib/keepr/groups_creator/profit_and_loss.txt +31 -0
  22. data/lib/keepr/journal.rb +48 -0
  23. data/lib/keepr/posting.rb +74 -0
  24. data/lib/keepr/tax.rb +13 -0
  25. data/lib/keepr/version.rb +3 -0
  26. data/spec/account_spec.rb +210 -0
  27. data/spec/active_record_extension_spec.rb +60 -0
  28. data/spec/cost_center_spec.rb +16 -0
  29. data/spec/database.yml +3 -0
  30. data/spec/factories/account.rb +7 -0
  31. data/spec/factories/cost_center.rb +6 -0
  32. data/spec/factories/group.rb +6 -0
  33. data/spec/factories/tax.rb +8 -0
  34. data/spec/group_spec.rb +89 -0
  35. data/spec/groups_creator_spec.rb +45 -0
  36. data/spec/journal_spec.rb +111 -0
  37. data/spec/posting_spec.rb +124 -0
  38. data/spec/spec_helper.rb +58 -0
  39. data/spec/support/document.rb +3 -0
  40. data/spec/support/ledger.rb +3 -0
  41. data/spec/support/spec_migration.rb +16 -0
  42. data/spec/tax_spec.rb +35 -0
  43. metadata +213 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 22676859e13394e7e8c59591b1419b2cd0cb18ed
4
+ data.tar.gz: 305b1e01ebb0d82b85b05823c7f48313d276a535
5
+ SHA512:
6
+ metadata.gz: f1bf2927bffefa894f91ef505973958195a922d3cb0486974986a65722e81ac6260cb237b18723fdec717517a6b9f987503a9f2ff93553e919e024addaaaa00d
7
+ data.tar.gz: 642fc11d40a0ad3cf0a5e9336abaf1d719030851d2d54a10ab259ee15ba28a0cdea9d685441aa4b5657a6162ed41434a5abc838d4dc3a53e43602cf36765decd
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.1.5
5
+ - 2.2.0
6
+ gemfile:
7
+ - ci/Gemfile-rails-4-1
8
+ - ci/Gemfile-rails-4-2
9
+ before_install: gem update bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in keepr.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-2015 Georg Ledermann
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Keepr
2
+
3
+ This Ruby gem provides a double entry accounting system for use in any Rails application. It stores all the data via ActiveRecord in the SQL database.
4
+
5
+ [![Build Status](https://travis-ci.org/ledermann/keepr.svg?branch=master)](https://travis-ci.org/ledermann/keepr)
6
+
7
+
8
+ ## Features
9
+
10
+ * Journal entries with two or more postings
11
+ * Accounts (including subaccounts and groups)
12
+ * Tax
13
+ * Cost center
14
+ * Balance sheet
15
+ * Profit and loss statement
16
+
17
+
18
+ ## Dependencies
19
+
20
+ * Ruby 1.9.3 or later
21
+ * Rails 4.1 or 4.2
22
+
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ gem 'keepr'
29
+
30
+ And then execute:
31
+
32
+ $ bundle
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install keepr
37
+
38
+
39
+ ## Usage
40
+
41
+ TODO: Write usage instructions here
42
+
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
51
+
52
+
53
+ ## Similar projects
54
+
55
+ * https://github.com/mbulat/plutus
56
+ * https://github.com/betterplace/acts_as_account
57
+ * https://github.com/steveluscher/bookkeeper
58
+ * https://github.com/mstrauss/double-entry-accounting
59
+ * https://github.com/logicleague/double_booked
60
+ * https://github.com/telent/pacioli
61
+ * https://github.com/astrails/deb
62
+ * https://github.com/bigfleet/accountable
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.1.0'
4
+ gem 'ancestry'
5
+ gem 'sqlite3'
6
+ gem 'rake'
7
+ gem 'rspec'
8
+ gem 'simplecov'
9
+ gem 'coveralls'
10
+ gem 'database_cleaner'
11
+ gem 'factory_girl'
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.2.0'
4
+ gem 'ancestry'
5
+ gem 'sqlite3'
6
+ gem 'rake'
7
+ gem 'rspec'
8
+ gem 'simplecov'
9
+ gem 'coveralls'
10
+ gem 'database_cleaner'
11
+ gem 'factory_girl'
data/keepr.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'keepr/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "keepr"
8
+ spec.version = Keepr::VERSION
9
+ spec.authors = 'Georg Ledermann'
10
+ spec.email = 'mail@georg-ledermann.de'
11
+ spec.description = %q{Double entry bookkeeping with Rails}
12
+ spec.summary = %q{Some basic ActiveRecord models to build a double entry bookkeeping application}
13
+ spec.homepage = 'https://github.com/ledermann/keepr'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 1.9.3'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'activerecord', '>= 4.1'
23
+ spec.add_dependency 'ancestry'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.3'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'sqlite3'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'database_cleaner'
30
+ spec.add_development_dependency 'factory_girl'
31
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Keepr
5
+ class MigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc 'Generates migration for keepr'
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ def create_migration_file
12
+ migration_template 'migration.rb', 'db/migrate/keepr_migration'
13
+ end
14
+
15
+ def self.next_migration_number(dirname)
16
+ if ActiveRecord::Base.timestamped_migrations
17
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
18
+ else
19
+ "%.3d" % (current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ class KeeprMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :keepr_groups, force: true do |t|
4
+ t.integer :target, :null => false
5
+ t.string :number
6
+ t.string :name, :null => false
7
+ t.boolean :is_result, :null => false, :default => false
8
+ t.string :ancestry
9
+ end
10
+ add_index :keepr_groups, :ancestry
11
+
12
+ create_table :keepr_taxes, force: true do |t|
13
+ t.string :name, :null => false
14
+ t.string :description
15
+ t.decimal :value, :precision => 8, :scale => 2, :null => false
16
+ t.references :keepr_account, :null => false
17
+ end
18
+ add_index :keepr_taxes, :keepr_account_id
19
+
20
+ create_table :keepr_cost_centers, force: true do |t|
21
+ t.string :number, :null => false
22
+ t.string :name, :null => false
23
+ t.text :note
24
+ end
25
+
26
+ create_table :keepr_accounts, force: true do |t|
27
+ t.integer :number, :null => false
28
+ t.string :ancestry
29
+ t.string :name, :null => false
30
+ t.integer :kind, :null => false
31
+ t.references :keepr_group
32
+ t.references :accountable, :polymorphic => true
33
+ t.references :keepr_tax
34
+ t.datetime :created_at
35
+ t.datetime :updated_at
36
+ end
37
+ add_index :keepr_accounts, :number
38
+ add_index :keepr_accounts, :ancestry
39
+ add_index :keepr_accounts, [:accountable_type, :accountable_id]
40
+ add_index :keepr_accounts, :keepr_group_id
41
+ add_index :keepr_accounts, :keepr_tax_id
42
+
43
+ create_table :keepr_journals, force: true do |t|
44
+ t.string :number
45
+ t.date :date, :null => false
46
+ t.string :subject
47
+ t.references :accountable, :polymorphic => true
48
+ t.text :note
49
+ t.datetime :created_at
50
+ t.datetime :updated_at
51
+ end
52
+ add_index :keepr_journals, :date
53
+ add_index :keepr_journals, [:accountable_type, :accountable_id], :name => 'index_keepr_journals_on_accountable'
54
+
55
+ create_table :keepr_postings, force: true do |t|
56
+ t.references :keepr_account, :null => false
57
+ t.references :keepr_journal, :null => false
58
+ t.decimal :amount, :precision => 8, :scale => 2, :null => false
59
+ t.references :keepr_cost_center
60
+ end
61
+ add_index :keepr_postings, :keepr_account_id
62
+ add_index :keepr_postings, :keepr_journal_id
63
+ add_index :keepr_postings, :keepr_cost_center_id
64
+ end
65
+
66
+ def self.down
67
+ drop_table :keepr_postings
68
+ drop_table :keepr_journals
69
+ drop_table :keepr_accounts
70
+ drop_table :keepr_groups
71
+ end
72
+ end
data/lib/keepr.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'ancestry'
2
+
3
+ require 'keepr/version'
4
+ require 'keepr/group'
5
+ require 'keepr/groups_creator'
6
+ require 'keepr/cost_center'
7
+ require 'keepr/tax'
8
+ require 'keepr/account'
9
+ require 'keepr/posting'
10
+ require 'keepr/journal'
11
+ require 'keepr/active_record_extension'
12
+
13
+ class ActiveRecord::Base
14
+ include Keepr::ActiveRecordExtension
15
+ end
@@ -0,0 +1,120 @@
1
+ class Keepr::Account < ActiveRecord::Base
2
+ self.table_name = 'keepr_accounts'
3
+
4
+ has_ancestry :orphan_strategy => :restrict
5
+
6
+ enum :kind => [ :asset, :liability, :revenue, :expense, :neutral ]
7
+
8
+ validates_presence_of :number, :name
9
+ validates_uniqueness_of :number
10
+ validate :group_validation
11
+ validate :tax_validation
12
+
13
+ has_many :keepr_postings, :class_name => 'Keepr::Posting', :foreign_key => 'keepr_account_id', :dependent => :restrict_with_error
14
+ has_many :keepr_taxes, :class_name => 'Keepr::Tax', :foreign_key => 'keepr_account_id', :dependent => :restrict_with_error
15
+
16
+ belongs_to :keepr_tax, :class_name => 'Keepr::Tax'
17
+ belongs_to :keepr_group, :class_name => 'Keepr::Group'
18
+ belongs_to :accountable, :polymorphic => true
19
+
20
+ default_scope { order(:number) }
21
+
22
+ def self.with_sums(date=nil)
23
+ scope = select('keepr_accounts.*, SUM(amount) AS sum_amount').
24
+ group('keepr_accounts.id').
25
+ joins('LEFT JOIN keepr_postings ON keepr_postings.keepr_account_id = keepr_accounts.id')
26
+
27
+ if date
28
+ scope = scope.joins('LEFT JOIN keepr_journals ON keepr_journals.id = keepr_postings.keepr_journal_id')
29
+
30
+ if date.is_a?(Date)
31
+ scope = scope.where("keepr_journals.id IS NULL OR keepr_journals.date <= '#{date.to_s(:db)}'")
32
+ elsif date.is_a?(Range)
33
+ scope = scope.where("keepr_journals.id IS NULL OR (keepr_journals.date BETWEEN '#{date.first.to_s(:db)}' AND '#{date.last.to_s(:db)}')")
34
+ else
35
+ raise ArgumentError
36
+ end
37
+ end
38
+
39
+ scope
40
+ end
41
+
42
+ def self.merged_with_sums(date=nil)
43
+ accounts = with_sums(date).to_a
44
+
45
+ # Sum up child accounts to parent
46
+ position = 0
47
+ while account = accounts[position] do
48
+ if account.parent_id
49
+ if parent_account = accounts.find { |a| a.id == account.parent_id }
50
+ parent_account.sum_amount ||= 0
51
+ parent_account.sum_amount += account.sum_amount
52
+ accounts.delete_at(position)
53
+ else
54
+ raise
55
+ end
56
+ else
57
+ position += 1
58
+ end
59
+ end
60
+
61
+ accounts
62
+ end
63
+
64
+ def profit_and_loss?
65
+ revenue? || expense?
66
+ end
67
+
68
+ def keepr_postings
69
+ Keepr::Posting.
70
+ joins(:keepr_account).
71
+ where(subtree_conditions)
72
+ end
73
+
74
+ def balance(date=nil)
75
+ if date
76
+ if date.is_a?(Date)
77
+ keepr_postings.joins(:keepr_journal).where("keepr_journals.date <= '#{date.to_s(:db)}'").sum(:amount)
78
+ elsif date.is_a?(Range)
79
+ keepr_postings.joins(:keepr_journal).where("keepr_journals.date BETWEEN '#{date.first.to_s(:db)}' AND '#{date.last.to_s(:db)}'").sum(:amount)
80
+ else
81
+ raise ArgumentError
82
+ end
83
+ else
84
+ keepr_postings.sum(:amount)
85
+ end
86
+ end
87
+
88
+ def number_as_string
89
+ if number < 1000
90
+ "%04d" % number
91
+ else
92
+ number.to_s
93
+ end
94
+ end
95
+
96
+ def to_s
97
+ "#{number_as_string} (#{name})"
98
+ end
99
+
100
+ private
101
+ def group_validation
102
+ if keepr_group.present?
103
+ if asset?
104
+ errors.add(:kind, 'does match group') unless keepr_group.asset?
105
+ elsif liability?
106
+ errors.add(:kind, 'does match group') unless keepr_group.liability?
107
+ elsif profit_and_loss?
108
+ errors.add(:kind, 'does match group') unless keepr_group.profit_and_loss?
109
+ else
110
+ errors.add(:kind, 'conflicts with group')
111
+ end
112
+
113
+ errors.add(:keepr_group_id, 'is a result group') if keepr_group.is_result
114
+ end
115
+ end
116
+
117
+ def tax_validation
118
+ errors.add(:keepr_tax_id, 'circular reference') if keepr_tax && keepr_tax.keepr_account == self
119
+ end
120
+ end
@@ -0,0 +1,27 @@
1
+ module Keepr::ActiveRecordExtension
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def has_keepr_account
8
+ has_one :keepr_account, :class_name => 'Keepr::Account', :as => :accountable
9
+ end
10
+
11
+ def is_keepr_accountable
12
+ has_many :keepr_journals, :class_name => 'Keepr::Journal', :as => :accountable
13
+
14
+ class_eval <<-EOT
15
+ def keepr_booked?
16
+ keepr_journals.exists?
17
+ end
18
+
19
+ scope :keepr_unbooked, -> {
20
+ joins('LEFT JOIN keepr_journals ON keepr_journals.accountable_id = #{table_name}.id AND keepr_journals.accountable_type="#{base_class.name}"').
21
+ where('keepr_journals.id' => nil)
22
+ }
23
+ scope :keepr_booked, -> { joins(:keepr_journals) }
24
+ EOT
25
+ end
26
+ end
27
+ end