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 +20 -0
- data/README +28 -0
- data/Rakefile +22 -0
- data/app/models/account.rb +113 -0
- data/app/models/bank.rb +9 -0
- data/app/models/bank_account.rb +13 -0
- data/app/models/booking.rb +166 -0
- data/lib/account_scope_extension.rb +11 -0
- data/lib/has_accounts/class_methods.rb +12 -0
- data/lib/has_accounts/core_ext/rounding.rb +43 -0
- data/lib/has_accounts/railtie.rb +7 -0
- data/lib/has_accounts.rb +1 -0
- metadata +79 -0
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
|
data/app/models/bank.rb
ADDED
@@ -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,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
|
data/lib/has_accounts.rb
ADDED
@@ -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
|
+
|