spatten-bookkeeper 0.2.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/init.rb +1 -0
- data/lib/bookkeeper.rb +8 -0
- data/lib/bookkeeper/account.rb +95 -0
- data/lib/bookkeeper/account/asset.rb +3 -0
- data/lib/bookkeeper/account/expense.rb +3 -0
- data/lib/bookkeeper/account/liability.rb +3 -0
- data/lib/bookkeeper/account/revenue.rb +3 -0
- data/lib/bookkeeper/asset_type.rb +3 -0
- data/lib/bookkeeper/asset_type/cad.rb +2 -0
- data/lib/bookkeeper/asset_type/usd.rb +2 -0
- data/lib/bookkeeper/batch.rb +62 -0
- data/lib/bookkeeper/journal.rb +29 -0
- data/lib/bookkeeper/journal/bill.rb +32 -0
- data/lib/bookkeeper/journal/deposit.rb +7 -0
- data/lib/bookkeeper/journal/disbursement.rb +7 -0
- data/lib/bookkeeper/journal/invoice.rb +4 -0
- data/lib/bookkeeper/journal/transfer.rb +7 -0
- data/lib/bookkeeper/paypal_transaction.rb +12 -0
- data/lib/bookkeeper/paypal_transaction/masspay_subpayment.rb +17 -0
- data/lib/bookkeeper/paypal_transaction/single.rb +14 -0
- data/lib/bookkeeper/posting.rb +32 -0
- data/lib/immutable-attribute-plugin/MIT-LICENSE +20 -0
- data/lib/immutable-attribute-plugin/README +35 -0
- data/lib/immutable-attribute-plugin/Rakefile +22 -0
- data/lib/immutable-attribute-plugin/init.rb +4 -0
- data/lib/immutable-attribute-plugin/install.rb +1 -0
- data/lib/immutable-attribute-plugin/lib/ensures_immutability_of.rb +43 -0
- data/lib/immutable-attribute-plugin/tasks/ensures_immutability_of_tasks.rake +4 -0
- data/lib/immutable-attribute-plugin/test/fixtures/accounts.yml +6 -0
- data/lib/immutable-attribute-plugin/test/fixtures/infos.yml +6 -0
- data/lib/immutable-attribute-plugin/test/rails_root/README +211 -0
- data/lib/immutable-attribute-plugin/test/rails_root/Rakefile +10 -0
- data/lib/immutable-attribute-plugin/test/rails_root/app/controllers/application.rb +7 -0
- data/lib/immutable-attribute-plugin/test/rails_root/app/helpers/application_helper.rb +3 -0
- data/lib/immutable-attribute-plugin/test/rails_root/app/models/account.rb +6 -0
- data/lib/immutable-attribute-plugin/test/rails_root/app/models/info.rb +3 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/boot.rb +39 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/database.yml +18 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environment.rb +11 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/development.rb +21 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/mysql.rb +0 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/postgresql.rb +0 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/production.rb +18 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/sqlite.rb +0 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/sqlite3.rb +0 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/environments/test.rb +19 -0
- data/lib/immutable-attribute-plugin/test/rails_root/config/routes.rb +23 -0
- data/lib/immutable-attribute-plugin/test/rails_root/db/migrate/001_create_accounts.rb +12 -0
- data/lib/immutable-attribute-plugin/test/rails_root/db/migrate/002_create_infos.rb +13 -0
- data/lib/immutable-attribute-plugin/test/rails_root/script/console +3 -0
- data/lib/immutable-attribute-plugin/test/rails_root/script/runner +3 -0
- data/lib/immutable-attribute-plugin/test/rails_root/test/test_helper.rb +28 -0
- data/lib/immutable-attribute-plugin/test/rails_root/vendor/plugins/phone_validation/init.rb +2 -0
- data/lib/immutable-attribute-plugin/test/test_helper.rb +26 -0
- data/lib/immutable-attribute-plugin/test/unit/plugin_test.rb +137 -0
- data/lib/immutable-attribute-plugin/uninstall.rb +1 -0
- data/rails/init.rb +1 -0
- data/rails_generators/USAGE +3 -0
- data/rails_generators/bookkeeper_generator.rb +17 -0
- data/rails_generators/templates/fixtures.yml +1 -0
- data/rails_generators/templates/migration.rb +66 -0
- metadata +140 -0
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
data/lib/bookkeeper.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'immutable-attribute-plugin', 'init')
|
4
|
+
|
5
|
+
# require everything in lib/
|
6
|
+
Find.find(File.join(File.dirname(__FILE__), 'bookkeeper')) do |file|
|
7
|
+
require file if !File.directory?(file) && File.extname(file) == '.rb'
|
8
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class Account < ActiveRecord::Base
|
2
|
+
acts_as_tree
|
3
|
+
|
4
|
+
has_many :postings
|
5
|
+
belongs_to :accountable, :polymorphic => true
|
6
|
+
|
7
|
+
validate :validate_account_created_through_a_subclass
|
8
|
+
validate :validate_subaccount_same_type_as_parent, :if => :is_subaccount
|
9
|
+
validate :validate_name_not_set, :if => :accountable_has_own_name
|
10
|
+
validates_presence_of :name, :unless => :accountable_has_own_name
|
11
|
+
|
12
|
+
def debit(amount, options = {})
|
13
|
+
new_posting(:debit, amount, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def credit(amount, options = {})
|
17
|
+
new_posting(:credit, amount, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def deposit(transaction)
|
21
|
+
gross = transaction.notification.amount.cents.to_f / 100
|
22
|
+
|
23
|
+
journal = Journal::Deposit.new(:transactable => transaction)
|
24
|
+
|
25
|
+
journal.postings |= transaction.to_postings
|
26
|
+
journal.postings << self.credit(gross)
|
27
|
+
|
28
|
+
journal.save!
|
29
|
+
end
|
30
|
+
|
31
|
+
def account_type
|
32
|
+
self.class::ACCOUNT_TYPE
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
accountable_has_own_name ? accountable.name : self[:name]
|
37
|
+
end
|
38
|
+
|
39
|
+
def postings_with_recursion
|
40
|
+
(self_and_all_children.collect { |account| account.postings_without_recursion }).flatten
|
41
|
+
end
|
42
|
+
alias_method_chain :postings, :recursion
|
43
|
+
|
44
|
+
def balance
|
45
|
+
postings.inject(0) { |balance, p|
|
46
|
+
amount_from_account_perspective = [:asset,:expense].include?(account_type) ? -p.amount : p.amount
|
47
|
+
balance + amount_from_account_perspective
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def is_subaccount
|
52
|
+
self != self.root
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy
|
56
|
+
raise ActiveRecord::IndestructibleRecord
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def accounts_payable; roots.find_by_name('Accounts Payable'); end
|
61
|
+
def accounts_receivable; roots.find_by_name('Accounts Receivable'); end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def new_posting(type, amount, params)
|
66
|
+
params.assert_valid_keys(:asset_type)
|
67
|
+
|
68
|
+
params.merge!(:account => self)
|
69
|
+
|
70
|
+
case type
|
71
|
+
when :debit
|
72
|
+
params.merge!(:amount => -amount)
|
73
|
+
when :credit
|
74
|
+
params.merge!(:amount => amount)
|
75
|
+
end
|
76
|
+
|
77
|
+
Posting.new(params)
|
78
|
+
end
|
79
|
+
|
80
|
+
def accountable_has_own_name
|
81
|
+
!self.accountable.name.nil? unless self.accountable.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_account_created_through_a_subclass
|
85
|
+
errors.add_to_base 'Accounts may only be created by instantiating a subclass of Account.' if self.class == Account
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_subaccount_same_type_as_parent
|
89
|
+
errors.add_to_base 'Subaccounts must be of the same type as their parent.' if self.class != self.parent.class
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_name_not_set
|
93
|
+
errors.add :name, 'should not be set on an account with an accountable that has a name.'
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Batch < ActiveRecord::Base
|
2
|
+
has_many :journals
|
3
|
+
|
4
|
+
def pay(payment_transaction)
|
5
|
+
# Get all of the bills associated with this batch
|
6
|
+
bills = journals
|
7
|
+
|
8
|
+
# What kind of fee have we paid as part of this payment?
|
9
|
+
fee = BigDecimal(payment_transaction.notification.fee)
|
10
|
+
if fee > 0
|
11
|
+
logger.info "Recording the MassPay fee for MassPay subpayment #{payment_transaction.transaction_id}"
|
12
|
+
fee_journal = Journal::Disbursement.new(:transactable => payment_transaction)
|
13
|
+
|
14
|
+
fee_journal.postings << payment_transaction.payer_account.credit(fee)
|
15
|
+
fee_journal.postings << Account.paypal_fees_account.debit(fee)
|
16
|
+
|
17
|
+
fee_journal.save!
|
18
|
+
end
|
19
|
+
|
20
|
+
# How much cash have we paid as part of this payment?
|
21
|
+
payment_dollars = BigDecimal(payment_transaction.notification.gross)
|
22
|
+
|
23
|
+
while bills.length > 0
|
24
|
+
bill = bills.shift
|
25
|
+
amount_to_pay = bill.amount - bill.amount_paid
|
26
|
+
|
27
|
+
logger.info "We have #{payment_dollars} left, and #{amount_to_pay} to pay for bill #{bill.id}, with #{bills.length} bills left to pay."
|
28
|
+
if(payment_dollars < amount_to_pay)
|
29
|
+
warn_mispayment(:under, bill.id, (amount_to_pay - payment_dollars))
|
30
|
+
amount_to_pay = payment_dollars
|
31
|
+
elsif(bills.length == 0 and payment_dollars > amount_to_pay)
|
32
|
+
warn_mispayment(:over, bill.id, (payment_dollars - amount_to_pay))
|
33
|
+
amount_to_pay = payment_dollars
|
34
|
+
end
|
35
|
+
|
36
|
+
if(amount_to_pay > 0)
|
37
|
+
# Record a bill payment in the ledger
|
38
|
+
logger.info "Recording a bill payment for bill #{bill.id}"
|
39
|
+
|
40
|
+
journal = Journal::Disbursement.new(:transactable => payment_transaction)
|
41
|
+
|
42
|
+
journal.postings << payment_transaction.payer_account.credit(amount_to_pay)
|
43
|
+
journal.postings << Account.accounts_payable.debit(amount_to_pay)
|
44
|
+
|
45
|
+
bill.payments << journal
|
46
|
+
bill.save!
|
47
|
+
|
48
|
+
payment_dollars -= amount_to_pay
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def warn_mispayment(extreme, bill_id, amount)
|
55
|
+
case extreme
|
56
|
+
when :over
|
57
|
+
logger.warn "About to overpay bill #{bill_id} by $#{amount}."
|
58
|
+
when :under
|
59
|
+
logger.warn "About to underpay bill #{bill_id} by $#{amount}."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Journal < ActiveRecord::Base
|
2
|
+
has_many :postings
|
3
|
+
belongs_to :transactable, :polymorphic => true
|
4
|
+
belongs_to :batch
|
5
|
+
|
6
|
+
ensures_immutability_of :all
|
7
|
+
|
8
|
+
validates_size_of :postings, :minimum => 2
|
9
|
+
validates_associated :postings
|
10
|
+
validate :validate_postings_sum_to_zero
|
11
|
+
|
12
|
+
def destroy
|
13
|
+
raise ActiveRecord::IndestructibleRecord
|
14
|
+
end
|
15
|
+
|
16
|
+
def transactable_type=(sType)
|
17
|
+
super(sType.to_s.classify.constantize.base_class.to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def validate_postings_sum_to_zero
|
22
|
+
errors.add :postings, 'must sum to zero.' unless postings_sum_to_zero
|
23
|
+
end
|
24
|
+
|
25
|
+
def postings_sum_to_zero
|
26
|
+
sum = self.postings.inject(0) { |sum, posting| sum + posting.amount }
|
27
|
+
sum == 0
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Journal::Bill < Journal
|
2
|
+
has_many :payments, :as => :payable, :class_name => '::Journal'
|
3
|
+
belongs_to :creditor, :polymorphic => true
|
4
|
+
|
5
|
+
def amount
|
6
|
+
postings.find(:first, :conditions => {:account_id => Account.accounts_payable}).amount
|
7
|
+
end
|
8
|
+
|
9
|
+
def amount_paid
|
10
|
+
payments.inject(0) { |sum, payment| sum - payment.postings.find(:first, :conditions => {:account_id => Account.accounts_payable}).amount }
|
11
|
+
end
|
12
|
+
|
13
|
+
def paid_in_full?
|
14
|
+
amount_paid >= amount
|
15
|
+
end
|
16
|
+
|
17
|
+
def overpaid?
|
18
|
+
amount_paid > amount
|
19
|
+
end
|
20
|
+
|
21
|
+
def pay(payment_transaction)
|
22
|
+
gross = payment_transaction.notification.amount.cents.to_f / 100
|
23
|
+
|
24
|
+
journal = Journal::Disbursement.new(:transactable => payment_transaction)
|
25
|
+
|
26
|
+
journal.postings |= payment_transaction.to_postings
|
27
|
+
journal.postings << Account.accounts_payable.debit(gross)
|
28
|
+
|
29
|
+
payments << journal
|
30
|
+
save!
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class PaypalTransaction < ActiveRecord::Base
|
2
|
+
include ActiveMerchant::Billing::Integrations
|
3
|
+
|
4
|
+
serialize :notification, Paypal::Notification
|
5
|
+
|
6
|
+
has_one :journal, :as => :transactable
|
7
|
+
|
8
|
+
private
|
9
|
+
def account
|
10
|
+
@account ||= Account::Asset.find_by_account_number(self.notification.account)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class PaypalTransaction::MasspaySubpayment < PaypalTransaction
|
2
|
+
|
3
|
+
def to_postings
|
4
|
+
gross = self.notification.amount.cents.to_f / 100
|
5
|
+
fee = self.notification.fee.to_f
|
6
|
+
|
7
|
+
postings = []
|
8
|
+
postings << payer_account.credit(gross + fee)
|
9
|
+
postings << Account.paypal_fees_account.debit(fee) if(fee > 0)
|
10
|
+
|
11
|
+
postings
|
12
|
+
end
|
13
|
+
|
14
|
+
def payer_account
|
15
|
+
@payer_account ||= Account::Asset.find_by_account_number(self.notification.params['payer_email'])
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class PaypalTransaction::Single < PaypalTransaction
|
2
|
+
|
3
|
+
def to_postings
|
4
|
+
gross = self.notification.amount.cents.to_f / 100
|
5
|
+
fee = self.notification.fee.to_f
|
6
|
+
|
7
|
+
postings = []
|
8
|
+
postings << account.debit(gross - fee)
|
9
|
+
postings << Account.paypal_fees_account.debit(fee) if(fee > 0)
|
10
|
+
|
11
|
+
postings
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Posting < ActiveRecord::Base
|
2
|
+
belongs_to :account
|
3
|
+
belongs_to :journal
|
4
|
+
belongs_to :asset_type
|
5
|
+
|
6
|
+
ensures_immutability_of :all
|
7
|
+
|
8
|
+
before_validation_on_create :default_to_us_dollars, :unless => :asset_type
|
9
|
+
|
10
|
+
validates_presence_of :account
|
11
|
+
validates_presence_of :asset_type
|
12
|
+
validate :validate_belongs_to_a_journal
|
13
|
+
|
14
|
+
def destroy
|
15
|
+
raise ActiveRecord::IndestructibleRecord
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def validate_belongs_to_a_journal
|
20
|
+
parent_journal = self.journal
|
21
|
+
|
22
|
+
if !parent_journal
|
23
|
+
ObjectSpace.each_object(Journal) {|j| parent_journal = j if j.postings.include?(self)}
|
24
|
+
end
|
25
|
+
|
26
|
+
errors.add :journal, 'must be set.' if !parent_journal
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_to_us_dollars
|
30
|
+
self.asset_type = AssetType::USD.find(:first)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Wesley Moxam - Savvica Inc.
|
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.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
EnsuresImmutabilityOf
|
2
|
+
=====================
|
3
|
+
|
4
|
+
There are many cases where a model attribute should not be changed once it's set. This plugin makes it dead simple.
|
5
|
+
|
6
|
+
Example
|
7
|
+
=======
|
8
|
+
|
9
|
+
class Account < ActiveRecord::Base
|
10
|
+
ensures_immutability_of :username, :email
|
11
|
+
end
|
12
|
+
|
13
|
+
account = Account.create(:username => 'jgreen')
|
14
|
+
...
|
15
|
+
account.update(:username => 'jgreen') # raises ActiveRecord::ImmutableAttributeError
|
16
|
+
|
17
|
+
Entire models can be made immutable by passing the symbol :all to ensures_immutability_of
|
18
|
+
|
19
|
+
class Account < ActiveRecord::Base
|
20
|
+
ensures_immutability_of :all
|
21
|
+
end
|
22
|
+
|
23
|
+
Collections can all be immutable as well (thanks to Dmitry Ratnikov on #rubyonrails)
|
24
|
+
|
25
|
+
class Account < ActiveRecord::Base
|
26
|
+
has_many :infos
|
27
|
+
ensures_immutability_of :username, :email, :infos # note: this must come after the has_many declaration, otherwise
|
28
|
+
# AR will overwrite the setter
|
29
|
+
end
|
30
|
+
|
31
|
+
account = Account.create(:username => 'wmoxam')
|
32
|
+
account.infos = Info.find(:all, :conditions => ["id < ?", 3]
|
33
|
+
account.infos = Info.find(:all, :conditions => ["id > ?", 3] # raises ActiveRecord::ImmutableAttributeError
|
34
|
+
|
35
|
+
Copyright (c) 2007-2008 Wesley Moxam - Savvica Inc, released under the MIT license
|