pacioli 0.0.1

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.
Files changed (42) hide show
  1. data/.gitignore +17 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/config.ru +7 -0
  8. data/lib/generators/active_record/pacioli_generator.rb +30 -0
  9. data/lib/generators/active_record/templates/migration.rb +60 -0
  10. data/lib/pacioli.rb +34 -0
  11. data/lib/pacioli/account.rb +25 -0
  12. data/lib/pacioli/asset_account.rb +9 -0
  13. data/lib/pacioli/company.rb +91 -0
  14. data/lib/pacioli/credit.rb +5 -0
  15. data/lib/pacioli/customer.rb +31 -0
  16. data/lib/pacioli/debit.rb +5 -0
  17. data/lib/pacioli/equity_account.rb +9 -0
  18. data/lib/pacioli/exception.rb +4 -0
  19. data/lib/pacioli/expense_account.rb +9 -0
  20. data/lib/pacioli/income_account.rb +9 -0
  21. data/lib/pacioli/journal_entry.rb +61 -0
  22. data/lib/pacioli/liability_account.rb +9 -0
  23. data/lib/pacioli/posting_rule.rb +34 -0
  24. data/lib/pacioli/transaction.rb +7 -0
  25. data/lib/pacioli/validations/journal_entry_validator.rb +17 -0
  26. data/lib/pacioli/validations/posting_rule_validator.rb +15 -0
  27. data/lib/pacioli/version.rb +3 -0
  28. data/pacioli.gemspec +25 -0
  29. data/spec/internal/config/database.yml +3 -0
  30. data/spec/internal/config/routes.rb +3 -0
  31. data/spec/internal/db/pacioli_test.sqlite +0 -0
  32. data/spec/internal/db/schema.rb +58 -0
  33. data/spec/internal/log/.gitignore +1 -0
  34. data/spec/internal/public/favicon.ico +0 -0
  35. data/spec/lib/pacioli/account_spec.rb +43 -0
  36. data/spec/lib/pacioli/company_spec.rb +19 -0
  37. data/spec/lib/pacioli/customer_spec.rb +23 -0
  38. data/spec/lib/pacioli/journal_entry_spec.rb +199 -0
  39. data/spec/lib/pacioli/posting_rule_spec.rb +40 -0
  40. data/spec/spec_helper.rb +19 -0
  41. data/spec/support/helpers.rb +11 -0
  42. metadata +143 -0
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@pacioli --create
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pacioli.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ gem 'rspec-rails'
9
+ gem 'sqlite3'
10
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jeffrey van Aswegen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Pacioli
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pacioli'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install pacioli
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize!
7
+ run Combustion::Application
@@ -0,0 +1,30 @@
1
+ module Pacioli
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ desc <<-CONTENT
9
+ Copies the pacioli migration file to the migrations
10
+ folder.
11
+
12
+ Please run rake db:migrate once the installer is
13
+ complete.
14
+
15
+ CONTENT
16
+
17
+ def self.next_migration_number(dirname) #:nodoc:
18
+ if ActiveRecord::Base.timestamped_migrations
19
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
20
+ else
21
+ "%.3d" % (current_migration_number(dirname) + 1)
22
+ end
23
+ end
24
+
25
+ def create_migration_file
26
+ migration_template 'migration.rb', 'db/migrate/create_pacioli_tables.rb'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ class CreatePacioliTables < ActiveRecord::Migration
2
+ def self.change
3
+
4
+ create_table :pacioli_companies, :force => true do |t|
5
+ t.integer :companyable_id
6
+ t.string :companyable_type
7
+ t.string :name
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ create_table :pacioli_customers, :force => true do |t|
13
+ t.references :pacioli_company
14
+ t.integer :customerable_id
15
+ t.string :customerable_type
16
+ t.string :name
17
+
18
+ t.timestamps
19
+ end
20
+
21
+ create_table :pacioli_accounts, :force => true do |t|
22
+ t.references :pacioli_company
23
+ t.string :code
24
+ t.string :name
25
+ t.string :description
26
+ t.string :type
27
+
28
+ t.timestamps
29
+ end
30
+
31
+ create_table :pacioli_journal_entries, :force => true do |t|
32
+ t.string :journal_type
33
+ t.string :description
34
+ t.references :pacioli_company
35
+ t.decimal :amount, :precision => 20, :scale => 10
36
+
37
+ t.datetime :dated
38
+ t.timestamps
39
+ end
40
+
41
+ create_table :pacioli_transactions, :force => true do |t|
42
+ t.references :pacioli_journal_entry
43
+ t.references :pacioli_account
44
+ t.references :pacioli_customer
45
+
46
+ t.string :type
47
+ t.decimal :amount, :precision => 20, :scale => 10
48
+
49
+ t.datetime :dated
50
+ t.timestamps
51
+ end
52
+
53
+ create_table :pacioli_posting_rules, :force => true do |t|
54
+ t.references :pacioli_company
55
+ t.string :name
56
+ t.text :rules
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ require "active_record"
2
+ require "pacioli/version"
3
+
4
+ module Pacioli
5
+ def self.table_name_prefix
6
+ 'pacioli_'
7
+ end
8
+
9
+ def self.register_company(&block)
10
+ company = Company.new
11
+ company.instance_eval(&block)
12
+ company.save!
13
+ company
14
+ end
15
+
16
+ end
17
+
18
+ require "pacioli/company"
19
+ require "pacioli/customer"
20
+ require "pacioli/account"
21
+ require "pacioli/asset_account"
22
+ require "pacioli/liability_account"
23
+ require "pacioli/equity_account"
24
+ require "pacioli/income_account"
25
+ require "pacioli/expense_account"
26
+ require "pacioli/journal_entry"
27
+ require "pacioli/transaction"
28
+ require "pacioli/credit"
29
+ require "pacioli/debit"
30
+ require "pacioli/posting_rule"
31
+ require "pacioli/exception"
32
+
33
+ require "pacioli/validations/journal_entry_validator"
34
+ require "pacioli/validations/posting_rule_validator"
@@ -0,0 +1,25 @@
1
+ module Pacioli
2
+ class Account < ActiveRecord::Base
3
+ has_many :transactions, foreign_key: :pacioli_account_id
4
+
5
+ def asset?
6
+ false
7
+ end
8
+
9
+ def liability?
10
+ false
11
+ end
12
+
13
+ def equity?
14
+ false
15
+ end
16
+
17
+ def income?
18
+ false
19
+ end
20
+
21
+ def expense?
22
+ false
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Pacioli
2
+ class AssetAccount < Account
3
+
4
+ def asset?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,91 @@
1
+ module Pacioli
2
+ class Company < ActiveRecord::Base
3
+ belongs_to :companyable, polymorphic: true
4
+ has_many :accounts, foreign_key: :pacioli_company_id, dependent: :destroy
5
+ has_many :chart_of_accounts, through: :accounts
6
+ has_many :journal_entries, foreign_key: :pacioli_company_id, dependent: :destroy
7
+ has_many :posting_rules, foreign_key: :pacioli_company_id, dependent: :destroy
8
+ has_many :customers, foreign_key: :pacioli_company_id, dependent: :destroy
9
+
10
+ def self.for(company)
11
+ Company.where(companyable_type: company.class.name, companyable_id: company.id).first || Company.create!(companyable_type: company.class.name, companyable_id: company.id)
12
+ end
13
+
14
+ def with_name(company_name)
15
+ self.name = company_name
16
+ end
17
+
18
+ def with_source(companyable_object)
19
+ self.companyable = companyable_object
20
+ end
21
+
22
+ def add_asset_account(options={})
23
+ account = AssetAccount.create!(options)
24
+ self.accounts << account
25
+ account
26
+ end
27
+
28
+ def add_equity_account(options={})
29
+ account = EquityAccount.create!(options)
30
+ self.accounts << account
31
+ account
32
+ end
33
+
34
+ def add_liability_account(options={})
35
+ account = LiabilityAccount.create!(options)
36
+ self.accounts << account
37
+ account
38
+ end
39
+
40
+ def add_income_account(options={})
41
+ account = IncomeAccount.create!(options)
42
+ self.accounts << account
43
+ account
44
+ end
45
+
46
+ def add_expense_account(options={})
47
+ account = ExpenseAccount.create!(options)
48
+ self.accounts << account
49
+ account
50
+ end
51
+
52
+ def record_journal_entry(&block)
53
+ self.transaction do
54
+ journal_entry = JournalEntry.new
55
+ self.journal_entries << journal_entry
56
+ journal_entry.instance_eval(&block)
57
+
58
+ journal_entry.execute_posting_rules
59
+
60
+ JournalEntryValidator.for(journal_entry).execute
61
+
62
+ journal_entry.amount = journal_entry.calculate_amount if journal_entry.amount.blank?
63
+ journal_entry.save!
64
+ journal_entry
65
+ end
66
+ end
67
+
68
+ def create_posting_rule(&block)
69
+ self.transaction do
70
+ posting_rule = PostingRule.new
71
+ self.posting_rules << posting_rule
72
+ posting_rule.instance_eval(&block)
73
+
74
+ PostingRuleValidator.for(posting_rule).execute
75
+
76
+ posting_rule.save!
77
+ posting_rule
78
+ end
79
+ end
80
+
81
+ def register_customer(&block)
82
+ self.transaction do
83
+ customer = Customer.new
84
+ self.customers << customer
85
+ customer.instance_eval(&block)
86
+ customer.save!
87
+ customer
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,5 @@
1
+ module Pacioli
2
+ class Credit < Transaction
3
+
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ module Pacioli
2
+ class Customer < ActiveRecord::Base
3
+ belongs_to :customerable, polymorphic: true
4
+ belongs_to :company, foreign_key: :pacioli_company_id
5
+ has_many :transactions, foreign_key: :pacioli_customer_id
6
+
7
+ def self.for(customer)
8
+ Customer.where(customerable_type: customer.class.name, customerable_id: customer.id).first || Customer.create!(customerable_type: customer.class.name, customerable_id: customer.id)
9
+ end
10
+
11
+ def with_name(customer_name)
12
+ self.name = customer_name
13
+ end
14
+
15
+ def with_source(customerable_object)
16
+ self.customerable = customerable_object
17
+ end
18
+
19
+ def debits
20
+ transactions.where(type: 'Pacioli::Debit')
21
+ end
22
+
23
+ def credits
24
+ transactions.where(type: 'Pacioli::Credit')
25
+ end
26
+
27
+ def balance
28
+ debits.sum(&:amount) - credits.sum(&:amount)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module Pacioli
2
+ class Debit < Transaction
3
+
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Pacioli
2
+ class EquityAccount < Account
3
+
4
+ def equity?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module Pacioli
2
+ class AccountsNotBalancedException < Exception; end
3
+ class PostingRuleNotBalancedException < Exception; end
4
+ end
@@ -0,0 +1,9 @@
1
+ module Pacioli
2
+ class ExpenseAccount < Account
3
+
4
+ def expense?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Pacioli
2
+ class IncomeAccount < Account
3
+
4
+ def income?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ module Pacioli
2
+ class JournalEntry < ActiveRecord::Base
3
+ belongs_to :company, foreign_key: :pacioli_company_id
4
+ has_many :transactions, foreign_key: :pacioli_journal_entry_id
5
+
6
+ def with_description(desc)
7
+ self.description = desc
8
+ end
9
+
10
+ def with_amount(amt)
11
+ self.amount = amt
12
+ end
13
+
14
+ def type(type=nil)
15
+ self.journal_type = type
16
+ end
17
+
18
+ def debit(options={})
19
+ account = self.company.accounts.where(name: options[:account]).first
20
+ debit = Debit.new(journal_entry: self, account: account, amount: options[:amount])
21
+ debit.customer = options[:against_customer] if options.has_key? :against_customer
22
+ self.transactions << debit
23
+ end
24
+
25
+ def credit(options={})
26
+ account = self.company.accounts.where(name: options[:account]).first
27
+ credit = Credit.new(journal_entry: self, account: account, amount: options[:amount])
28
+ credit.customer = options[:against_customer] if options.has_key? :against_customer
29
+ self.transactions << credit
30
+ end
31
+
32
+ def balanced?
33
+ debits.sum(&:amount) == credits.sum(&:amount)
34
+ end
35
+
36
+ def debits
37
+ transactions.where(type: 'Pacioli::Debit')
38
+ end
39
+
40
+ def credits
41
+ transactions.where(type: 'Pacioli::Credit')
42
+ end
43
+
44
+ def calculate_amount
45
+ credits.sum(&:amount)
46
+ end
47
+
48
+ def execute_posting_rules
49
+ return if journal_type.blank?
50
+ posting_rule = self.company.posting_rules.where(name: self.journal_type).first
51
+
52
+ posting_rule.rules[:debits].each do |debit|
53
+ self.debit(account: debit[:account], amount: (self.amount / 100) * debit[:percentage])
54
+ end
55
+
56
+ posting_rule.rules[:credits].each do |credit|
57
+ self.credit(account: credit[:account], amount: (self.amount / 100) * credit[:percentage])
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ module Pacioli
2
+ class LiabilityAccount < Account
3
+
4
+ def liability?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ module Pacioli
2
+ class PostingRule < ActiveRecord::Base
3
+ belongs_to :company, foreign_key: :pacioli_company_id
4
+
5
+ serialize :rules
6
+
7
+ def with_name(name)
8
+ self.name = name
9
+ end
10
+
11
+ def debit(account_name, options={})
12
+ self.rules ||= {}
13
+ self.rules[:debits] ||= []
14
+ self.rules[:debits] << prepare_rules(account_name, options)
15
+ end
16
+
17
+ def credit(account_name, options={})
18
+ self.rules ||= {}
19
+ self.rules[:credits] ||= []
20
+ self.rules[:credits] << prepare_rules(account_name, options)
21
+ end
22
+
23
+ def prepare_rules(account_name, options={})
24
+ options[:percentage] = 100 if options.empty?
25
+ options[:account] = account_name
26
+ options
27
+ end
28
+
29
+ def balanced?
30
+ self.rules[:credits].inject(0) { |sum, cr| sum += cr[:percentage] } == self.rules[:debits].inject(0) { |sum, dr| sum += dr[:percentage] }
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module Pacioli
2
+ class Transaction < ActiveRecord::Base
3
+ belongs_to :journal_entry, foreign_key: :pacioli_journal_entry_id
4
+ belongs_to :account, foreign_key: :pacioli_account_id
5
+ belongs_to :customer, foreign_key: :pacioli_customer_id
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module Pacioli
2
+ class JournalEntryValidator
3
+ attr_accessor :journal_entry
4
+
5
+ def self.for(journal_entry)
6
+ validator = new
7
+ validator.journal_entry = journal_entry
8
+ validator
9
+ end
10
+
11
+ def execute
12
+ unless self.journal_entry.balanced?
13
+ raise Pacioli::AccountsNotBalancedException, "The aggregate balance of all accounts having positive balances must be equal to the aggregate balance of all accounts having negative balances."
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Pacioli
2
+ class PostingRuleValidator
3
+ attr_accessor :posting_rule
4
+
5
+ def self.for(posting_rule)
6
+ validator = new
7
+ validator.posting_rule = posting_rule
8
+ validator
9
+ end
10
+
11
+ def execute
12
+ raise Pacioli::PostingRuleNotBalancedException, "The aggregate balance of debits and credits must be equal" unless self.posting_rule.balanced?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Pacioli
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pacioli/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pacioli"
8
+ gem.version = Pacioli::VERSION
9
+ gem.authors = ["Jeffrey van Aswegen"]
10
+ gem.email = ["jeffmess@gmail.com"]
11
+ gem.description = %q{A double-entry bookkeeping system for Ruby on Rails}
12
+ gem.summary = %q{A double-entry bookkeeping system for Ruby on Rails}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "activesupport"
21
+ gem.add_dependency "activerecord", "~> 3.0"
22
+ gem.add_dependency "i18n"
23
+
24
+ gem.add_development_dependency 'combustion', '~> 0.3.1'
25
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/pacioli_test.sqlite
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ #
3
+ end
@@ -0,0 +1,58 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :pacioli_companies, :force => true do |t|
4
+ t.integer :companyable_id
5
+ t.string :companyable_type
6
+ t.string :name
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ create_table :pacioli_customers, :force => true do |t|
12
+ t.references :pacioli_company
13
+ t.integer :customerable_id
14
+ t.string :customerable_type
15
+ t.string :name
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ create_table :pacioli_accounts, :force => true do |t|
21
+ t.references :pacioli_company
22
+ t.string :code
23
+ t.string :name
24
+ t.string :description
25
+ t.string :type
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :pacioli_journal_entries, :force => true do |t|
31
+ t.string :journal_type
32
+ t.string :description
33
+ t.references :pacioli_company
34
+ t.decimal :amount, :precision => 20, :scale => 10
35
+
36
+ t.datetime :dated
37
+ t.timestamps
38
+ end
39
+
40
+ create_table :pacioli_transactions, :force => true do |t|
41
+ t.references :pacioli_journal_entry
42
+ t.references :pacioli_account
43
+ t.references :pacioli_customer
44
+
45
+ t.string :type
46
+ t.decimal :amount, :precision => 20, :scale => 10
47
+
48
+ t.datetime :dated
49
+ t.timestamps
50
+ end
51
+
52
+ create_table :pacioli_posting_rules, :force => true do |t|
53
+ t.references :pacioli_company
54
+ t.string :name
55
+ t.text :rules
56
+ end
57
+
58
+ end
@@ -0,0 +1 @@
1
+ *.log
File without changes
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacioli::Account do
4
+ include Helpers
5
+
6
+ before(:each) do
7
+ tear_it_down
8
+
9
+ @company = Pacioli::register_company do
10
+ with_name "Coca-Cola"
11
+ end
12
+ end
13
+
14
+ context "Create Accounts" do
15
+
16
+ it "should be able to create an asset account" do
17
+ account = @company.add_asset_account name: "Accounts Receivable"
18
+ account.should be_asset
19
+ end
20
+
21
+ it "should be able to create an equity account" do
22
+ account = @company.add_equity_account name: "Shareholders Capital"
23
+ account.should be_equity
24
+ end
25
+
26
+ it "should be able to create a liability account" do
27
+ account = @company.add_liability_account name: "Accounts Payable"
28
+ account.should be_liability
29
+ end
30
+
31
+ it "should be able to create an expense account" do
32
+ account = @company.add_expense_account name: "Entertainment"
33
+ account.should be_expense
34
+ end
35
+
36
+ it "should be able to create an income account" do
37
+ account = @company.add_income_account name: "Sales"
38
+ account.should be_income
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacioli::Company do
4
+ include Helpers
5
+
6
+ before(:each) do
7
+ tear_it_down
8
+
9
+ @company = Pacioli::register_company do
10
+ with_name "Coca-Cola"
11
+ end
12
+ end
13
+
14
+ it "should create a company" do
15
+ Pacioli::Company.all.count.should == 1
16
+ Pacioli::Company.first.name.should == "Coca-Cola"
17
+ end
18
+
19
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacioli::Customer do
4
+ include Helpers
5
+
6
+ before(:each) do
7
+ tear_it_down
8
+
9
+ @company = Pacioli::register_company do
10
+ with_name "Coca-Cola"
11
+ end
12
+
13
+ @customer = @company.register_customer do
14
+ with_name "Leonardo da Vinci"
15
+ end
16
+ end
17
+
18
+ it "should create a customer" do
19
+ Pacioli::Customer.all.count.should == 1
20
+ @customer.name.should == "Leonardo da Vinci"
21
+ @customer.company.should == @company
22
+ end
23
+ end
@@ -0,0 +1,199 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacioli::Account do
4
+ include Helpers
5
+
6
+ before(:each) do
7
+ tear_it_down
8
+
9
+ @company = Pacioli::register_company do
10
+ with_name "Coca-Cola"
11
+ add_asset_account name: "Accounts Receivable", code: "100"
12
+ add_asset_account name: "Cash in Bank", code: "101"
13
+ add_income_account name: "Sales", code: "301"
14
+ add_liability_account name: "Sales Tax", code: "401"
15
+ end
16
+ end
17
+
18
+ context "Recording a journal entry" do
19
+ before(:each) do
20
+ @company.record_journal_entry do
21
+ with_description "Invoice 123 for November Rent"
22
+ debit account: "Accounts Receivable", amount: 4500.00
23
+ credit account: "Sales", amount: 4500.00
24
+ end
25
+ end
26
+
27
+ it "should record a journal entry" do
28
+ Pacioli::JournalEntry.all.count.should == 1
29
+ end
30
+
31
+ it "should record 1 transaction against Accounts Receivable Account" do
32
+ @company.accounts.where(name: "Accounts Receivable").first.transactions.count.should == 1
33
+ end
34
+
35
+ it "should have recorded 1 transaction against the Sales Account" do
36
+ @company.accounts.where(name: "Sales").first.transactions.count.should == 1
37
+ end
38
+
39
+ it "should balance" do
40
+ Pacioli::JournalEntry.last.should be_balanced
41
+ end
42
+
43
+ it "should record the amount of 4500.00 for the journal entry" do
44
+ Pacioli::JournalEntry.last.amount.should == 4500.00
45
+ end
46
+ end
47
+
48
+ context "Recording a journal entry where the T-tables do not balance" do
49
+ it "should raise an AccountsNotBalancedException error" do
50
+ lambda {
51
+ @company.record_journal_entry do
52
+ with_description "Invoice 123 for November Rent"
53
+ debit account: "Accounts Receivable", amount: 4500.00
54
+ end
55
+ }.should raise_error(Pacioli::AccountsNotBalancedException, "The aggregate balance of all accounts having positive balances must be equal to the aggregate balance of all accounts having negative balances.")
56
+
57
+ Pacioli::JournalEntry.all.should be_blank
58
+ end
59
+ end
60
+
61
+ context "Recording specific types of journal entries using posting rules" do
62
+ context "Against 2 accounts" do
63
+ before(:each) do
64
+ @company.create_posting_rule do
65
+ with_name :sale
66
+ debit "Accounts Receivable"
67
+ credit "Sales"
68
+ end
69
+
70
+ @sales = @company.record_journal_entry do
71
+ with_amount 5000.00
72
+ with_description "Invoice 123 for November Rent"
73
+ type :sale
74
+ end
75
+ end
76
+
77
+ it "should create a debit transaction against the Accounts Receivable account for 5000.00" do
78
+ account = @company.accounts.where(name: "Accounts Receivable").first
79
+ account.transactions.count.should == 1
80
+ end
81
+
82
+ it "should create a credit transaction against the Sales account for 5000.00" do
83
+ account = @company.accounts.where(name: "Sales").first
84
+ account.transactions.count.should == 1
85
+ end
86
+
87
+ it "should be balanced" do
88
+ @sales.should be_balanced
89
+ end
90
+ end
91
+
92
+ context "Against multiple accounts" do
93
+ before(:each) do
94
+ @company2 = Pacioli::register_company do
95
+ with_name "Pepsi"
96
+ add_expense_account name: "Computer Equipment"
97
+ add_expense_account name: "Fund Balance"
98
+ add_liability_account name: "Account Payable"
99
+ add_liability_account name: "Capital Equipment"
100
+ end
101
+
102
+ @company2.create_posting_rule do
103
+ with_name :purchase_equipment_and_cover_replacements
104
+ debit "Computer Equipment"
105
+ credit "Account Payable"
106
+ debit "Fund Balance"
107
+ credit "Capital Equipment"
108
+ end
109
+
110
+ @purchase = @company2.record_journal_entry do
111
+ with_amount 10000.00
112
+ with_description "Purchase of Computer Equipment"
113
+ type :purchase_equipment_and_cover_replacements
114
+ end
115
+ end
116
+
117
+ it "should be balanced" do
118
+ @purchase.should be_balanced
119
+ end
120
+
121
+ it "should have 2 debit transactions" do
122
+ @purchase.debits.count.should == 2
123
+ end
124
+
125
+ it "should have 2 credit transactions" do
126
+ @purchase.credits.count.should == 2
127
+ end
128
+ end
129
+
130
+ context "Against multiple accounts and a percentage of an amount" do
131
+ before(:each) do
132
+ @company.create_posting_rule do
133
+ with_name :sale
134
+ debit "Accounts Receivable"
135
+ credit "Sales", percentage: 86
136
+ credit "Sales Tax", percentage: 14
137
+ end
138
+
139
+ @sales = @company.record_journal_entry do
140
+ with_amount 100.00
141
+ with_description "Invoice 123 for November Rent"
142
+ type :sale
143
+ end
144
+ end
145
+
146
+ it "should create a credit transaction against Sales for 86.00 and Sales Taxes for 14.00" do
147
+ @sales.credits.map(&:amount).should == [BigDecimal('86.00'), BigDecimal('14.00')]
148
+ end
149
+
150
+ it "should create a debit transaction against Accounts Receivable" do
151
+ @sales.debits.map(&:amount).should == [BigDecimal('100.00')]
152
+ end
153
+ end
154
+
155
+ context "Recording against a customer" do
156
+ before(:each) do
157
+ @customer = @company.register_customer do
158
+ with_name "Leonardo da Vinci"
159
+ end
160
+ end
161
+
162
+ context "Normal transactions where customer is invoiced" do
163
+ before(:each) do
164
+ customer = @customer
165
+
166
+ @company.record_journal_entry do
167
+ with_description "Invoice 123 for November Rent"
168
+ debit account: "Accounts Receivable", amount: 4500.00, against_customer: customer
169
+ credit account: "Sales", amount: 4500.00
170
+ end
171
+ end
172
+
173
+ it "should have a balance of 4500.00" do
174
+ @customer.balance.should == 4500.00
175
+ end
176
+
177
+ context "Customer makes a payment against the invoice" do
178
+ before(:each) do
179
+ customer = @customer
180
+
181
+ @company.record_journal_entry do
182
+ with_description "PMT Received"
183
+ debit account: "Cash in Bank", amount: 4500.00
184
+ credit account: "Accounts Receivable", amount: 4500.00, against_customer: customer
185
+ end
186
+ end
187
+
188
+ it "should have a balance of 0.00" do
189
+ @customer.balance.should == 0.00
190
+ end
191
+ end
192
+ end
193
+
194
+ context "Transactions using posting rules" do
195
+
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacioli::Account do
4
+ include Helpers
5
+
6
+ before(:each) do
7
+ tear_it_down
8
+
9
+ @company = Pacioli::register_company do
10
+ with_name "Coca-Cola"
11
+ add_asset_account name: "Accounts Receivable", code: "100"
12
+ add_income_account name: "Sales", code: "301"
13
+ end
14
+
15
+ @posting_rule = @company.create_posting_rule do
16
+ with_name :sale
17
+ debit "Accounts Receivable"
18
+ credit "Sales"
19
+ end
20
+ end
21
+
22
+ it "should store the rules" do
23
+ @posting_rule.rules.should == {
24
+ credits: [
25
+ {account: "Sales", percentage: 100}
26
+ ],
27
+ debits: [
28
+ {account: "Accounts Receivable", percentage: 100}
29
+ ]
30
+ }
31
+
32
+ @posting_rule.name.should == :sale
33
+ end
34
+
35
+ it "should be balanced" do
36
+ @posting_rule.should be_balanced
37
+ end
38
+
39
+
40
+ end
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :active_record
7
+
8
+ require 'rspec/rails'
9
+
10
+ require './spec/support/helpers.rb'
11
+
12
+ RSpec.configure do |config|
13
+ config.use_transactional_fixtures = true
14
+ config.color_enabled = true
15
+ config.formatter = :documentation # :progress, :html, :textmate
16
+ #config.include FactoryGirl::Syntax::Methods
17
+ end
18
+
19
+ #FactoryGirl.find_definitions
@@ -0,0 +1,11 @@
1
+ module Helpers
2
+ def tear_it_down
3
+ Pacioli::Company.delete_all
4
+ Pacioli::Account.delete_all
5
+ Pacioli::JournalEntry.delete_all
6
+ Pacioli::Transaction.delete_all
7
+ Pacioli::PostingRule.destroy_all
8
+ Pacioli::Customer.destroy_all
9
+ end
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pacioli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeffrey van Aswegen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-09 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70253548011320 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70253548011320
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ requirement: &70253548010820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70253548010820
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ requirement: &70253548010400 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70253548010400
47
+ - !ruby/object:Gem::Dependency
48
+ name: combustion
49
+ requirement: &70253548009860 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.1
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70253548009860
58
+ description: A double-entry bookkeeping system for Ruby on Rails
59
+ email:
60
+ - jeffmess@gmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - .gitignore
66
+ - .rvmrc
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - config.ru
72
+ - lib/generators/active_record/pacioli_generator.rb
73
+ - lib/generators/active_record/templates/migration.rb
74
+ - lib/pacioli.rb
75
+ - lib/pacioli/account.rb
76
+ - lib/pacioli/asset_account.rb
77
+ - lib/pacioli/company.rb
78
+ - lib/pacioli/credit.rb
79
+ - lib/pacioli/customer.rb
80
+ - lib/pacioli/debit.rb
81
+ - lib/pacioli/equity_account.rb
82
+ - lib/pacioli/exception.rb
83
+ - lib/pacioli/expense_account.rb
84
+ - lib/pacioli/income_account.rb
85
+ - lib/pacioli/journal_entry.rb
86
+ - lib/pacioli/liability_account.rb
87
+ - lib/pacioli/posting_rule.rb
88
+ - lib/pacioli/transaction.rb
89
+ - lib/pacioli/validations/journal_entry_validator.rb
90
+ - lib/pacioli/validations/posting_rule_validator.rb
91
+ - lib/pacioli/version.rb
92
+ - pacioli.gemspec
93
+ - spec/internal/config/database.yml
94
+ - spec/internal/config/routes.rb
95
+ - spec/internal/db/pacioli_test.sqlite
96
+ - spec/internal/db/schema.rb
97
+ - spec/internal/log/.gitignore
98
+ - spec/internal/public/favicon.ico
99
+ - spec/lib/pacioli/account_spec.rb
100
+ - spec/lib/pacioli/company_spec.rb
101
+ - spec/lib/pacioli/customer_spec.rb
102
+ - spec/lib/pacioli/journal_entry_spec.rb
103
+ - spec/lib/pacioli/posting_rule_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/support/helpers.rb
106
+ homepage: ''
107
+ licenses: []
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.6
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: A double-entry bookkeeping system for Ruby on Rails
130
+ test_files:
131
+ - spec/internal/config/database.yml
132
+ - spec/internal/config/routes.rb
133
+ - spec/internal/db/pacioli_test.sqlite
134
+ - spec/internal/db/schema.rb
135
+ - spec/internal/log/.gitignore
136
+ - spec/internal/public/favicon.ico
137
+ - spec/lib/pacioli/account_spec.rb
138
+ - spec/lib/pacioli/company_spec.rb
139
+ - spec/lib/pacioli/customer_spec.rb
140
+ - spec/lib/pacioli/journal_entry_spec.rb
141
+ - spec/lib/pacioli/posting_rule_spec.rb
142
+ - spec/spec_helper.rb
143
+ - spec/support/helpers.rb