has_accounts 0.1.0

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.
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
+