double_booked 0.0.2
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.md +79 -0
- data/Rakefile +35 -0
- data/app/assets/javascripts/double_booked/application.js +15 -0
- data/app/assets/stylesheets/double_booked/application.css +13 -0
- data/app/controllers/double_booked/application_controller.rb +4 -0
- data/app/helpers/double_booked/application_helper.rb +4 -0
- data/app/models/account.rb +44 -0
- data/app/models/balance.rb +40 -0
- data/app/models/blank_transaction.rb +17 -0
- data/app/models/credit.rb +25 -0
- data/app/models/debit.rb +15 -0
- data/app/models/detail_account.rb +11 -0
- data/app/models/entry.rb +13 -0
- data/app/models/invoice.rb +82 -0
- data/app/models/invoice_line.rb +26 -0
- data/app/models/invoice_payment.rb +15 -0
- data/app/models/statement.rb +30 -0
- data/app/models/summary_account.rb +11 -0
- data/app/models/transaction.rb +64 -0
- data/app/views/layouts/double_booked/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/lib/double_booked/engine.rb +5 -0
- data/lib/double_booked/version.rb +3 -0
- data/lib/double_booked.rb +4 -0
- data/lib/generators/double_booked/migrations/USAGE +8 -0
- data/lib/generators/double_booked/migrations/migrations_generator.rb +22 -0
- data/lib/generators/double_booked/migrations/templates/double_booked.rb.erb +70 -0
- data/lib/tasks/double_booked_tasks.rake +8 -0
- metadata +99 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Jay McAliley
|
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.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# DoubleBooked
|
2
|
+
|
3
|
+
Flexible double-entry accounting engine for Rails apps using ActiveRecord
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The core of `double_booked` are Accounts and Transactions. The concept of an account is probably familiar to most developers-- think of your checking or savings accounts. A transaction links two accounts together, posting a debit to one, and a credit to another. The credits and debits must be equal in amount. That's it!
|
8
|
+
|
9
|
+
The system also includes invoices, payments, and the ability to mark any eligible transaction as payment for an invoice, as long as the buyer's account is involved in the transaction.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
In your Gemfile, put:
|
14
|
+
```
|
15
|
+
source :rubygems
|
16
|
+
gem 'double_booked'
|
17
|
+
```
|
18
|
+
|
19
|
+
Then on the command line:
|
20
|
+
```bash
|
21
|
+
bundle install
|
22
|
+
rails g double_booked:migrations
|
23
|
+
rake db:migrate
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
To use `double_booked`, you'll need to create a model that is a subclass of
|
29
|
+
`DetailAccount`. A `DetailAccount` represents an account that is directly
|
30
|
+
debited from or credited to, for example a bank account.
|
31
|
+
|
32
|
+
Let's say your users have a TokenAccount record, and are able to award each other tokens from their accounts. You'd set up the models as such:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
class User < ActiveRecord::Base
|
36
|
+
has_one :token_account, :as => :owner
|
37
|
+
end
|
38
|
+
|
39
|
+
class TokenAccount < DetailAccount
|
40
|
+
owned_by :user
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
You may want to set up a special user to issue tokens:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# Setup the token issuer and account (the "bank")
|
48
|
+
token_issuer = User.create :email => 'token_issuer@myapp.com', ...
|
49
|
+
token_bank = token_issuer.create_token_account
|
50
|
+
|
51
|
+
token_bank.current_balance
|
52
|
+
# => 0
|
53
|
+
|
54
|
+
# Issue 10 tokens to the user Jack
|
55
|
+
jack = User.find_by_name "Jack"
|
56
|
+
token_bank.transfer(10).to jack.token_account
|
57
|
+
|
58
|
+
jack.token_account.current_balance
|
59
|
+
# => 10
|
60
|
+
|
61
|
+
token_bank.current_balance
|
62
|
+
# => -10
|
63
|
+
|
64
|
+
# Now have Jack give 2 tokens to Bob
|
65
|
+
bob = User.find_by_name("Bob")
|
66
|
+
|
67
|
+
bob.token_account.current_balance
|
68
|
+
# => 0
|
69
|
+
|
70
|
+
jack.token_account.transfer(10).to bob.token_account
|
71
|
+
|
72
|
+
jack.token_account.current_balance
|
73
|
+
# => 8
|
74
|
+
|
75
|
+
bob.token_account.current_balance
|
76
|
+
# => 2
|
77
|
+
```
|
78
|
+
|
79
|
+
At any time, the total tokens in all accounts should sum up to zero.
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'DoubleBooked'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
require 'rspec/core/rake_task'
|
25
|
+
RSpec::Core::RakeTask.new(:spec)
|
26
|
+
task :default => :spec
|
27
|
+
rescue LoadError => ls
|
28
|
+
puts "rspec failing to load."
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
33
|
+
load 'rails/tasks/engine.rake'
|
34
|
+
|
35
|
+
Bundler::GemHelper.install_tasks
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require jquery
|
14
|
+
//= require jquery_ujs
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class Account < ActiveRecord::Base
|
2
|
+
has_many :balances
|
3
|
+
validate :no_direct_subclass
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :owner_type
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.owned_by(klass)
|
10
|
+
@owner_type = klass.to_s.classify.constantize
|
11
|
+
belongs_to :owner, :polymorphic => true
|
12
|
+
validate :check_owner_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def balance_at(date)
|
16
|
+
balance = balances.where(:evaluated_at => date).first
|
17
|
+
balance ||= Balance.new(:account => self, :evaluated_at => date)
|
18
|
+
end
|
19
|
+
|
20
|
+
def balance_before(date)
|
21
|
+
balances.find :first, :conditions => ["evaluated_at < ?", date],
|
22
|
+
:order => "evaluated_at DESC"
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_balance
|
26
|
+
balance_at(Time.now)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def check_owner_type
|
31
|
+
errors.add(:base, "owner must be an #{self.class.owner_type}") if
|
32
|
+
self.class.owner_type and !(owner.is_a? self.class.owner_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
def no_direct_subclass
|
36
|
+
# FIXME -- refactor Account as a mixin instead of using STI
|
37
|
+
# consider using abstract_class for DetailAccount & SummaryAccount
|
38
|
+
msg = "Record must not be an Account or a direct subclass of it. " +
|
39
|
+
"Subclass the DetailAccount or SummaryAccount class instead."
|
40
|
+
direct_subclass = self.class.superclass == Account || self.class == Account
|
41
|
+
errors.add :base, msg if direct_subclass
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Balance < ActiveRecord::Base
|
2
|
+
belongs_to :account
|
3
|
+
validates_presence_of :account, :evaluated_at, :balance
|
4
|
+
|
5
|
+
def balance
|
6
|
+
super or self.balance = calculate_balance
|
7
|
+
end
|
8
|
+
|
9
|
+
def readonly?
|
10
|
+
!new_record?
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def calculate_balance
|
16
|
+
entries.inject(previous_balance) {|balance, entry| balance + entry.amount}
|
17
|
+
end
|
18
|
+
|
19
|
+
def previous
|
20
|
+
account.balance_before(evaluated_at)
|
21
|
+
end
|
22
|
+
|
23
|
+
def previous_balance
|
24
|
+
previous ? previous.balance : 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def entries
|
28
|
+
account.entries.find(:all, :conditions => entry_conditions,
|
29
|
+
:joins => :transaction)
|
30
|
+
end
|
31
|
+
|
32
|
+
def entry_conditions
|
33
|
+
column = "transactions.created_at"
|
34
|
+
if previous
|
35
|
+
["#{column} > ? AND #{column} <= ?", previous.evaluated_at, evaluated_at]
|
36
|
+
else
|
37
|
+
["#{column} <= ?", evaluated_at]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class BlankTransaction
|
2
|
+
attr_accessor :amount, :account_from, :account_to
|
3
|
+
|
4
|
+
def initialize(amount, account_from)
|
5
|
+
@amount = amount
|
6
|
+
@account_from = account_from
|
7
|
+
end
|
8
|
+
|
9
|
+
def to(account_to, args = {})
|
10
|
+
defaults = {
|
11
|
+
:account_from => @account_from,
|
12
|
+
:account_to => account_to,
|
13
|
+
:amount => @amount
|
14
|
+
}
|
15
|
+
Transaction.create args.merge(defaults)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Credit < Entry
|
2
|
+
|
3
|
+
validate :require_debit
|
4
|
+
validate :sign_convention
|
5
|
+
validate :conservation_principle
|
6
|
+
attr_accessible :amount, :detail_account
|
7
|
+
|
8
|
+
def sign_convention
|
9
|
+
errors.add(:amount, "Credit must be non-negative") unless amount >= 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def debit
|
13
|
+
transaction ? transaction.debit : nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def require_debit
|
17
|
+
errors.add(:base, "Debit must be saved before credit") unless !debit.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def conservation_principle
|
21
|
+
errors.add(:base, "Credit and debit amounts must add up to zero") unless
|
22
|
+
(debit.nil? or amount + debit.amount == 0)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/app/models/debit.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class Debit < Entry
|
2
|
+
|
3
|
+
validate :sign_convention
|
4
|
+
has_one :credit, :through => :transaction
|
5
|
+
attr_accessible :amount, :detail_account
|
6
|
+
|
7
|
+
def sign_convention
|
8
|
+
errors.add(:base, "Debit must be non-positive") unless amount <= 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def balanced?
|
12
|
+
!credit.nil? and (credit.amount + amount == 0)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/app/models/entry.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Entry < ActiveRecord::Base
|
2
|
+
|
3
|
+
belongs_to :detail_account
|
4
|
+
belongs_to :transaction
|
5
|
+
has_one :invoice_line, :as => :line_item
|
6
|
+
validates_presence_of :detail_account, :transaction
|
7
|
+
validates_numericality_of :amount
|
8
|
+
|
9
|
+
def readonly?
|
10
|
+
!new_record?
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Invoice < ActiveRecord::Base
|
2
|
+
belongs_to :buyer_account, :class_name => 'DetailAccount'
|
3
|
+
belongs_to :seller_account, :class_name => 'DetailAccount'
|
4
|
+
has_many :invoice_payments, :as => :auxilliary_model
|
5
|
+
has_many :invoice_lines
|
6
|
+
has_many :line_items, :through => :invoice_lines
|
7
|
+
|
8
|
+
validates_presence_of :buyer_account, :seller_account
|
9
|
+
|
10
|
+
def self.build(entries)
|
11
|
+
accounts = check_accounts entries
|
12
|
+
invoice = Invoice.create! :buyer_account => accounts[:buyer],
|
13
|
+
:seller_account => accounts[:seller]
|
14
|
+
entries.each do |entry|
|
15
|
+
line = invoice.invoice_lines.create :line_item => entry
|
16
|
+
line.save!
|
17
|
+
end
|
18
|
+
invoice.close
|
19
|
+
invoice
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
update_attribute(:closed, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def open?
|
27
|
+
!closed?
|
28
|
+
end
|
29
|
+
|
30
|
+
def paid?
|
31
|
+
amount_owed <= 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def amount_billed
|
35
|
+
line_items.inject(0) {|amount, item| amount + item.amount}
|
36
|
+
end
|
37
|
+
|
38
|
+
def amount_paid
|
39
|
+
invoice_payments(true).inject(0) {|amount, payment| amount + payment.amount}
|
40
|
+
end
|
41
|
+
|
42
|
+
def amount_owed
|
43
|
+
amount_billed - amount_paid
|
44
|
+
end
|
45
|
+
|
46
|
+
def pay_in_full(options = {})
|
47
|
+
pay(amount_owed, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def pay(amount, options = {})
|
51
|
+
options.merge!({:description => "Payment for Invoice ##{formatted_id}",
|
52
|
+
:auxilliary_model => self,
|
53
|
+
:account_from => buyer_account,
|
54
|
+
:account_to => seller_account,
|
55
|
+
:amount => amount})
|
56
|
+
InvoicePayment.create! options
|
57
|
+
end
|
58
|
+
|
59
|
+
def formatted_id
|
60
|
+
"%08i" % id
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def self.check_accounts(entries)
|
65
|
+
accounts, last_accounts = {}, {}
|
66
|
+
entries.each do |entry|
|
67
|
+
accounts = get_accounts entry
|
68
|
+
raise ArgumentError, "All entries must involve the same accounts" unless
|
69
|
+
last_accounts.empty? || accounts == last_accounts
|
70
|
+
last_accounts = accounts
|
71
|
+
end
|
72
|
+
accounts
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.get_accounts(entry)
|
76
|
+
buyer = entry.detail_account
|
77
|
+
seller = entry.transaction.debited_account
|
78
|
+
seller = entry.transaction.credited_account if seller == buyer
|
79
|
+
{:seller => seller, :buyer => buyer}
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class InvoiceLine < ActiveRecord::Base
|
2
|
+
belongs_to :invoice
|
3
|
+
belongs_to :line_item, :class_name => 'Entry'
|
4
|
+
validates_presence_of :invoice, :line_item
|
5
|
+
validate :open_invoice
|
6
|
+
validate :correct_account
|
7
|
+
validates_uniqueness_of :line_item_id
|
8
|
+
|
9
|
+
def paid?
|
10
|
+
invoice.paid?
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def open_invoice
|
16
|
+
return true unless invoice # another validation will catch
|
17
|
+
errors.add(:invoice, "is closed") unless invoice(true).open?
|
18
|
+
end
|
19
|
+
|
20
|
+
def correct_account
|
21
|
+
return true unless invoice && line_item # another validation will catch
|
22
|
+
errors.add(:base, "Line item is applied to the wrong account") unless
|
23
|
+
line_item.detail_account == invoice.buyer_account
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Statement
|
2
|
+
attr_accessor :account, :period_start, :period_end
|
3
|
+
|
4
|
+
def initialize(account, period_start, period_end)
|
5
|
+
@account = account
|
6
|
+
@period_start = period_start
|
7
|
+
@period_end = period_end
|
8
|
+
end
|
9
|
+
|
10
|
+
def entries
|
11
|
+
account.entries.where(entry_conditions).
|
12
|
+
joins(:transaction).
|
13
|
+
order("created_at ASC")
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_balance
|
17
|
+
account.balance_at(period_start).balance
|
18
|
+
end
|
19
|
+
|
20
|
+
def end_balance
|
21
|
+
account.balance_at(period_end).balance
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def entry_conditions
|
26
|
+
column = "transactions.created_at"
|
27
|
+
["#{column} > ? AND #{column} <= ?", period_start, period_end]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class SummaryAccount < Account
|
2
|
+
has_and_belongs_to_many :accounts, :join_table => :account_joins
|
3
|
+
has_many :entries, :through => :accounts
|
4
|
+
validate :no_recursion
|
5
|
+
|
6
|
+
private
|
7
|
+
def no_recursion
|
8
|
+
errors.add(:base, "Summary account cannot summarize itself") if
|
9
|
+
accounts.include? self
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Transaction < ActiveRecord::Base
|
2
|
+
has_one :debit
|
3
|
+
has_one :credit
|
4
|
+
has_one :credited_account, :through => :credit, :source => :detail_account
|
5
|
+
has_one :debited_account, :through => :debit, :source => :detail_account
|
6
|
+
belongs_to :auxilliary_model, :polymorphic => true
|
7
|
+
|
8
|
+
validates_presence_of :description, :account_from, :account_to, :amount
|
9
|
+
validate :sufficient_funds, :if => :require_funds?
|
10
|
+
|
11
|
+
attr_accessor :account_from, :account_to, :amount
|
12
|
+
|
13
|
+
def self.belongs_to_auxilliary_model(model)
|
14
|
+
klass = model.to_s.classify
|
15
|
+
belongs_to klass.underscore.to_sym, :foreign_key => :auxilliary_model_id
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.required_auxilliary_model(model)
|
19
|
+
belongs_to_auxilliary_model(model)
|
20
|
+
validates_presence_of model.to_s.classify.underscore.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def completed?
|
24
|
+
!debit.nil? and !credit.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def amount
|
28
|
+
completed? ? credit.amount : (@amount or 0)
|
29
|
+
end
|
30
|
+
|
31
|
+
def account_from
|
32
|
+
completed? ? debit.detail_account : @account_from
|
33
|
+
end
|
34
|
+
|
35
|
+
def account_to
|
36
|
+
completed? ? credit.detail_account : @account_to
|
37
|
+
end
|
38
|
+
|
39
|
+
def transaction_date
|
40
|
+
super or created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
def readonly?
|
44
|
+
!new_record?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def create
|
49
|
+
# Saving of debit, credit, and transaction should be done in one
|
50
|
+
# atomic commit (the following block is an *SQL* transaction, not related
|
51
|
+
# to our Transaction model)
|
52
|
+
transaction do
|
53
|
+
super
|
54
|
+
create_debit :amount => -amount, :detail_account => account_from
|
55
|
+
create_credit :amount => amount, :detail_account => account_to
|
56
|
+
end
|
57
|
+
completed?
|
58
|
+
end
|
59
|
+
|
60
|
+
def sufficient_funds
|
61
|
+
sufficient = account_from && account_from.current_balance.balance >= amount
|
62
|
+
errors.add :base, "Insufficient funds in debit account" unless sufficient
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>DoubleBooked</title>
|
5
|
+
<%= stylesheet_link_tag "double_booked/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "double_booked/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module DoubleBooked
|
5
|
+
class MigrationsGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
# Implement the required interface for Rails::Generators::Migration.
|
9
|
+
# taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
|
10
|
+
def self.next_migration_number(dirname)
|
11
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
12
|
+
end
|
13
|
+
|
14
|
+
source_root File.expand_path("../templates", __FILE__)
|
15
|
+
|
16
|
+
desc "Creates migrations."
|
17
|
+
|
18
|
+
def create_migrations
|
19
|
+
migration_template "double_booked.rb.erb", "db/migrate/create_double_booked.rb"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class CreateDoubleBookedModels < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
|
4
|
+
create_table :accounts do |t|
|
5
|
+
t.string :type
|
6
|
+
t.string :name
|
7
|
+
t.references :owner, :polymorphic => true
|
8
|
+
end
|
9
|
+
add_index :accounts, [:owner_id, :owner_type]
|
10
|
+
|
11
|
+
create_table :account_joins, :id => false do |t|
|
12
|
+
t.integer :detail_account_id, :references => :accounts
|
13
|
+
t.integer :summary_account_id, :references => :accounts
|
14
|
+
end
|
15
|
+
add_index :account_joins, :detail_account_id
|
16
|
+
add_index :account_joins, :summary_account_id
|
17
|
+
|
18
|
+
create_table :balances do |t|
|
19
|
+
t.integer :account_id
|
20
|
+
t.datetime :evaluated_at
|
21
|
+
t.decimal :balance, :precision => 14, :scale => 2
|
22
|
+
end
|
23
|
+
add_index :balances, :account_id
|
24
|
+
|
25
|
+
create_table :transactions do |t|
|
26
|
+
t.string :type
|
27
|
+
t.string :description
|
28
|
+
t.datetime :transaction_date
|
29
|
+
t.references :auxilliary_model, :polymorphic => true
|
30
|
+
t.boolean :require_funds
|
31
|
+
t.timestamps
|
32
|
+
end
|
33
|
+
add_index :transactions, :transaction_date
|
34
|
+
add_index :transactions, [:auxilliary_model_id, :auxilliary_model_type], :name => :index_transactions_on_auxilliary_model
|
35
|
+
|
36
|
+
create_table :entries do |t|
|
37
|
+
t.string :type
|
38
|
+
t.integer :detail_account_id, :references => :accounts
|
39
|
+
t.references :transaction
|
40
|
+
t.decimal :amount, :precision => 14, :scale => 2
|
41
|
+
end
|
42
|
+
add_index :entries, :detail_account_id
|
43
|
+
add_index :entries, :transaction_id
|
44
|
+
|
45
|
+
create_table :invoices do |t|
|
46
|
+
t.integer :buyer_account_id, :references => :accounts
|
47
|
+
t.integer :seller_account_id, :references => :accounts
|
48
|
+
t.boolean :closed, :default => false
|
49
|
+
end
|
50
|
+
add_index :invoices, :buyer_account_id
|
51
|
+
add_index :invoices, :seller_account_id
|
52
|
+
|
53
|
+
create_table :invoice_lines do |t|
|
54
|
+
t.references :invoice
|
55
|
+
t.integer :line_item_id, :references => :entries
|
56
|
+
end
|
57
|
+
add_index :invoice_lines, :invoice_id
|
58
|
+
add_index :invoice_lines, :line_item_id
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.down
|
62
|
+
drop_table :invoice_lines
|
63
|
+
drop_table :invoices
|
64
|
+
drop_table :entries
|
65
|
+
drop_table :transactions
|
66
|
+
drop_table :balances
|
67
|
+
drop_table :account_joins
|
68
|
+
drop_table :accounts
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: double_booked
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jay McAliley
|
9
|
+
- John McAliley
|
10
|
+
- Jim Van Fleet
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2013-02-26 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rails
|
18
|
+
requirement: &70271980125640 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.0.0
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *70271980125640
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: &70271980125020 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *70271980125020
|
38
|
+
description: Double-entry accounting issues credits and debits, calculates balances,
|
39
|
+
allows for summary accounts and more.
|
40
|
+
email:
|
41
|
+
- jay@logicleague.com
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- app/assets/javascripts/double_booked/application.js
|
47
|
+
- app/assets/stylesheets/double_booked/application.css
|
48
|
+
- app/controllers/double_booked/application_controller.rb
|
49
|
+
- app/helpers/double_booked/application_helper.rb
|
50
|
+
- app/models/account.rb
|
51
|
+
- app/models/balance.rb
|
52
|
+
- app/models/blank_transaction.rb
|
53
|
+
- app/models/credit.rb
|
54
|
+
- app/models/debit.rb
|
55
|
+
- app/models/detail_account.rb
|
56
|
+
- app/models/entry.rb
|
57
|
+
- app/models/invoice.rb
|
58
|
+
- app/models/invoice_line.rb
|
59
|
+
- app/models/invoice_payment.rb
|
60
|
+
- app/models/statement.rb
|
61
|
+
- app/models/summary_account.rb
|
62
|
+
- app/models/transaction.rb
|
63
|
+
- app/views/layouts/double_booked/application.html.erb
|
64
|
+
- config/routes.rb
|
65
|
+
- lib/double_booked/engine.rb
|
66
|
+
- lib/double_booked/version.rb
|
67
|
+
- lib/double_booked.rb
|
68
|
+
- lib/generators/double_booked/migrations/migrations_generator.rb
|
69
|
+
- lib/generators/double_booked/migrations/templates/double_booked.rb.erb
|
70
|
+
- lib/generators/double_booked/migrations/USAGE
|
71
|
+
- lib/tasks/double_booked_tasks.rake
|
72
|
+
- MIT-LICENSE
|
73
|
+
- Rakefile
|
74
|
+
- README.md
|
75
|
+
homepage: https://github.com/logicleague/double_booked
|
76
|
+
licenses: []
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.10
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: Flexible double-entry accounting engine for Rails apps
|
99
|
+
test_files: []
|