has_accounts 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,28 @@
1
+ has_accounts
2
+ ============
3
+
4
+ Rails plugin providing financal accounting models and helpers.
5
+
6
+
7
+ Example
8
+ =======
9
+
10
+ There is a new class method made available to ActiveRecord::Base
11
+ by this plugin:
12
+
13
+ * has_accounts(options = {})
14
+
15
+ Use it like this:
16
+
17
+ class Doctor < ActiveRecord::Base
18
+ has_vcards
19
+ end
20
+
21
+ License
22
+ =======
23
+
24
+ Copyright (c) 2008 Agrabah <http://www.agrabah.ch>
25
+ Copyright (c) 2008-2010 Simon Hürlimann <simon.huerlimann@cyt.ch>
26
+ Copyright (c) 2008-2010 ZytoLabor <http://www.zyto-labor.com>
27
+
28
+ Released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the has_accounts plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the has_accounts plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Accounting'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,113 @@
1
+ class Account < ActiveRecord::Base
2
+ # Scopes
3
+ default_scope :order => 'code'
4
+
5
+ # Dummy scope to make scoped_by happy
6
+ scope :by_value_period, scoped
7
+
8
+ # Validation
9
+ validates_presence_of :code, :title
10
+
11
+ # String
12
+ def to_s(format = :default)
13
+ "%s (%s)" % [title, code]
14
+ end
15
+
16
+ # Account Type
17
+ # ============
18
+ belongs_to :account_type
19
+
20
+ def is_asset_account?
21
+ [1, 2, 5].include? account_type_id
22
+ end
23
+
24
+ def is_liability_account?
25
+ [3, 4, 6].include? account_type_id
26
+ end
27
+
28
+ scope :current_assets, where('account_type_id = 1') do
29
+ include AccountScopeExtension
30
+ end
31
+ scope :capital_assets, where('account_type_id = 2') do
32
+ include AccountScopeExtension
33
+ end
34
+ scope :outside_capital, where('account_type_id = 3') do
35
+ include AccountScopeExtension
36
+ end
37
+ scope :equity_capital, where('account_type_id = 4') do
38
+ include AccountScopeExtension
39
+ end
40
+ scope :expenses, where('account_type_id = 5') do
41
+ include AccountScopeExtension
42
+ end
43
+ scope :earnings, where('account_type_id = 6') do
44
+ include AccountScopeExtension
45
+ end
46
+
47
+ # Holder
48
+ # ======
49
+ belongs_to :holder, :polymorphic => true
50
+
51
+ # Bookings
52
+ # ========
53
+ has_many :credit_bookings, :class_name => "Booking", :foreign_key => "credit_account_id"
54
+ has_many :debit_bookings, :class_name => "Booking", :foreign_key => "debit_account_id"
55
+
56
+ def bookings
57
+ Booking.by_account(id)
58
+ end
59
+
60
+ # Helpers
61
+ # =======
62
+ def self.overview(value_range = Date.today, format = :default)
63
+ Account.all.map{|a| a.to_s(value_range, format)}
64
+ end
65
+
66
+ # Calculations
67
+ def turnover(selector = Date.today, inclusive = true)
68
+ if selector.is_a? Range or selector.is_a? Array
69
+ if selector.first.is_a? Booking
70
+ equality = "=" if inclusive
71
+ if selector.first.value_date == selector.last.value_date
72
+ condition = ["date(value_date) = :value_date AND id >#{equality} :first_id AND id <#{equality} :last_id", {
73
+ :value_date => selector.first.value_date,
74
+ :first_id => selector.first.id,
75
+ :last_id => selector.last.id
76
+ }]
77
+ else
78
+ condition = ["(value_date > :first_value_date AND value_date < :latest_value_date) OR (date(value_date) = :first_value_date AND id >#{equality} :first_id) OR (date(value_date) = :latest_value_date AND id <#{equality} :last_id)", {
79
+ :first_value_date => selector.first.value_date,
80
+ :latest_value_date => selector.last.value_date,
81
+ :first_id => selector.first.id,
82
+ :last_id => selector.last.id
83
+ }]
84
+ end
85
+ elsif
86
+ # TODO support inclusive param
87
+ condition = {:value_date => selector}
88
+ end
89
+ else
90
+ if selector.is_a? Booking
91
+ equality = "=" if inclusive
92
+ # date(value_date) is needed on sqlite!
93
+ condition = ["(value_date < :value_date) OR (date(value_date) = :value_date AND id <#{equality} :id)", {:value_date => selector.value_date, :id => selector.id}]
94
+ else
95
+ equality = "=" if inclusive
96
+ condition = ["value_date <#{equality} ?", selector]
97
+ end
98
+ end
99
+
100
+ credit_amount = credit_bookings.where(condition).sum(:amount)
101
+ debit_amount = debit_bookings.where(condition).sum(:amount)
102
+
103
+ [credit_amount || 0.0, debit_amount || 0.0]
104
+ end
105
+
106
+ def saldo(selector = Date.today, inclusive = true)
107
+ credit_amount, debit_amount = turnover(selector, inclusive)
108
+
109
+ amount = credit_amount - debit_amount
110
+
111
+ return is_asset_account? ? amount : -amount
112
+ end
113
+ end
@@ -0,0 +1,9 @@
1
+ class Bank < ActiveRecord::Base
2
+ has_many :bank_accounts
3
+
4
+ has_vcards
5
+
6
+ def to_s
7
+ [vcard.full_name, vcard.locality].compact.join(', ')
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ class BankAccount < Account
2
+ belongs_to :bank
3
+
4
+ # Standard methods
5
+ def to_s(value_range = Date.today, format = :default)
6
+ case format
7
+ when :short
8
+ "#{code}: CHF #{sprintf('%0.2f', saldo(value_range).currency_round)}"
9
+ else
10
+ "#{title} (#{code}) #{bank.to_s} #{number}: CHF #{sprintf('%0.2f', saldo(value_range).currency_round)}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,166 @@
1
+ class Booking < ActiveRecord::Base
2
+ # Validation
3
+ validates_presence_of :debit_account, :credit_account, :title, :amount, :value_date
4
+ validates_time :value_date
5
+
6
+ # Associations
7
+ belongs_to :debit_account, :foreign_key => 'debit_account_id', :class_name => "Account"
8
+ belongs_to :credit_account, :foreign_key => 'credit_account_id', :class_name => "Account"
9
+
10
+ # Scoping
11
+ default_scope order('value_date, id')
12
+
13
+ scope :by_value_date, lambda {|value_date| where(:value_date => value_date) }
14
+ scope :by_value_period, lambda {|from, to| where(:value_date => (from..to)) }
15
+
16
+ scope :by_account, lambda {|account_id|
17
+ { :conditions => ["debit_account_id = :account_id OR credit_account_id = :account_id", {:account_id => account_id}] }
18
+ } do
19
+ # Returns array of all booking titles.
20
+ def titles
21
+ find(:all, :group => :title).map{|booking| booking.title}
22
+ end
23
+
24
+ # Statistics per booking title.
25
+ #
26
+ # The statistics are an array of hashes with keys title, count, sum, average.
27
+ def statistics
28
+ find(:all, :select => "title, count(*) AS count, sum(amount) AS sum, avg(amount) AS avg", :group => :title).map{|stat| stat.attributes}
29
+ end
30
+ end
31
+
32
+ scope :by_text, lambda {|value|
33
+ text = '%' + value + '%'
34
+
35
+ amount = value.delete("'").to_f
36
+ if amount == 0.0
37
+ amount = nil unless value.match(/^[0.]*$/)
38
+ end
39
+
40
+ date = nil
41
+ begin
42
+ date = Date.parse(value)
43
+ rescue ArgumentError
44
+ end
45
+
46
+ where("title LIKE :text OR amount = :amount OR value_date = :value_date", :text => text, :amount => amount, :value_date => date)
47
+ }
48
+
49
+ # Returns array of all years we have bookings for
50
+ def self.fiscal_years
51
+ with_exclusive_scope do
52
+ select("DISTINCT year(value_date) AS year").all.map{|booking| booking.year}
53
+ end
54
+ end
55
+
56
+ def self.scope_by_value_date(value_date)
57
+ scoping = self.default_scoping - [@by_value_scope]
58
+
59
+ @by_value_scope = {:find => {:conditions => {:value_date => value_date}}}
60
+ scoping << @by_value_scope
61
+
62
+ Thread.current["#{self}_scoped_methods"] = nil
63
+ self.default_scoping = scoping
64
+ end
65
+
66
+ def self.filter(controller, &block)
67
+ if controller.value_date_scope
68
+ with_scope(:find => {:conditions => {:value_date => controller.value_date_scope}}, &block)
69
+ else
70
+ block.call
71
+ end
72
+ end
73
+
74
+ # Standard methods
75
+ def to_s(format = :default)
76
+ case format
77
+ when :short
78
+ "%s: %s / %s CHF %s" % [
79
+ value_date ? value_date : '?',
80
+ credit_account ? credit_account.code : '?',
81
+ debit_account ? debit_account.code : '?',
82
+ amount ? "%0.2f" % amount : '?',
83
+ ]
84
+ else
85
+ "%s: %s an %s CHF %s, %s (%s)" % [
86
+ value_date ? value_date : '?',
87
+ credit_account ? "#{credit_account.title} (#{credit_account.code})" : '?',
88
+ debit_account ? "#{debit_account.title} (#{debit_account.code})" : '?',
89
+ amount ? "%0.2f" % amount : '?',
90
+ title.present? ? title : '?',
91
+ comments.present? ? comments : '?'
92
+ ]
93
+ end
94
+ end
95
+
96
+ # Helpers
97
+ def accounted_amount(account)
98
+ if credit_account == account
99
+ balance = -(amount)
100
+ elsif debit_account == account
101
+ balance = amount
102
+ else
103
+ return BigDecimal.new('0')
104
+ end
105
+
106
+ if account.is_asset_account?
107
+ return -(balance)
108
+ else
109
+ return balance
110
+ end
111
+ end
112
+
113
+ def amount_as_string
114
+ '%0.2f' % amount
115
+ end
116
+
117
+ def amount_as_string=(value)
118
+ self.amount = value
119
+ end
120
+
121
+ def rounded_amount
122
+ if amount.nil?
123
+ return 0
124
+ else
125
+ return (amount * 20).round / 20.0
126
+ end
127
+ end
128
+
129
+ # Templates
130
+ def booking_template_id
131
+ nil
132
+ end
133
+
134
+ def booking_template_id=(value)
135
+ end
136
+
137
+ # Reference
138
+ belongs_to :reference, :polymorphic => true
139
+ after_save :notify_references
140
+
141
+ # Safety net for form assignments
142
+ def reference_type=(value)
143
+ write_attribute(:reference_type, value) unless value.blank?
144
+ end
145
+
146
+ scope :by_reference, lambda {|value|
147
+ where(:reference_id => value.id, :reference_type => value.class.base_class)
148
+ } do
149
+ # TODO duplicated in Invoice
150
+ def direct_balance(direct_account)
151
+ balance = 0.0
152
+
153
+ for booking in all
154
+ balance += booking.accounted_amount(direct_account)
155
+ end
156
+
157
+ balance
158
+ end
159
+ end
160
+
161
+ private
162
+ def notify_references
163
+ return unless reference and reference.respond_to?(:booking_saved)
164
+ reference.booking_saved(self)
165
+ end
166
+ end
@@ -0,0 +1,11 @@
1
+ module AccountScopeExtension
2
+ def saldo(selector = Date.today, inclusive = true)
3
+ new_saldo = 0
4
+
5
+ for account in all
6
+ new_saldo += account.saldo(selector, inclusive)
7
+ end
8
+
9
+ return new_saldo
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module HasAccounts
2
+ module ClassMethods
3
+ def has_accounts(options = {})
4
+ class_eval <<-end_eval
5
+ has_many :accounts, :as => 'holder'
6
+ has_one :account, :as => 'holder'
7
+ end_eval
8
+ end
9
+ end
10
+ end
11
+
12
+ ActiveRecord::Base.extend(HasAccounts::ClassMethods)
@@ -0,0 +1,43 @@
1
+ module HasAccounts #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Rounding
4
+ # Rounds the float according to currency rules.
5
+ # Currently targeted to Swiss Francs (CHF), usable
6
+ # for all currencies having 0.05 as smallest unit.
7
+ #
8
+ # x = 1.337
9
+ # x.round # => 1.35
10
+ def currency_round
11
+ if self.nil?
12
+ return 0.0
13
+ else
14
+ return (self * 20).round / 20.0
15
+ end
16
+ end
17
+ end
18
+
19
+ module BigDecimal
20
+ module Rounding
21
+ def currency_round
22
+ if self.nil?
23
+ return BigDecimal.new("0")
24
+ else
25
+ return (self * 20).round / 20
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class Float #:nodoc:
34
+ include HasAccounts::CoreExtensions::Rounding
35
+ end
36
+
37
+ class BigDecimal #:nodoc:
38
+ include HasAccounts::CoreExtensions::BigDecimal::Rounding
39
+ end
40
+
41
+ class Fixnum #:nodoc:
42
+ include HasAccounts::CoreExtensions::Rounding
43
+ end
@@ -0,0 +1,7 @@
1
+ require 'has_accounts'
2
+ require 'rails'
3
+
4
+ module HasAccounts
5
+ class Railtie < Rails::Engine
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ require 'has_accounts/railtie' if defined?(::Rails::Railtie)
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_accounts
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Simon H\xC3\xBCrlimann (CyT)"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-10 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: HasAccounts is a full featured Rails 3 gem providing models for financial accounting.
23
+ email:
24
+ - simon.huerlimann@cyt.ch
25
+ executables: []
26
+
27
+ extensions: []
28
+
29
+ extra_rdoc_files: []
30
+
31
+ files:
32
+ - app/models/bank.rb
33
+ - app/models/account.rb
34
+ - app/models/bank_account.rb
35
+ - app/models/booking.rb
36
+ - lib/account_scope_extension.rb
37
+ - lib/has_accounts/railtie.rb
38
+ - lib/has_accounts/class_methods.rb
39
+ - lib/has_accounts/core_ext/rounding.rb
40
+ - lib/has_accounts.rb
41
+ - MIT-LICENSE
42
+ - Rakefile
43
+ - README
44
+ has_rdoc: true
45
+ homepage:
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options: []
50
+
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.5.2
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: HasAccounts provides models for financial accounting.
78
+ test_files: []
79
+