uomi 0.2.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +6 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +10 -0
  5. data/README.md +35 -0
  6. data/Rakefile +8 -0
  7. data/config.ru +7 -0
  8. data/invoicing.gemspec +27 -0
  9. data/lib/generators/active_record/invoicing_generator.rb +30 -0
  10. data/lib/generators/active_record/templates/migration.rb +84 -0
  11. data/lib/invoicing/buyer.rb +10 -0
  12. data/lib/invoicing/credit_note.rb +70 -0
  13. data/lib/invoicing/credit_note_credit_transaction.rb +6 -0
  14. data/lib/invoicing/credit_note_invoice.rb +6 -0
  15. data/lib/invoicing/credit_transaction.rb +9 -0
  16. data/lib/invoicing/debit_transaction.rb +9 -0
  17. data/lib/invoicing/exception.rb +4 -0
  18. data/lib/invoicing/invoice.rb +233 -0
  19. data/lib/invoicing/invoice_adjustment.rb +59 -0
  20. data/lib/invoicing/invoice_decorator.rb +6 -0
  21. data/lib/invoicing/invoiceable.rb +22 -0
  22. data/lib/invoicing/late_payment.rb +11 -0
  23. data/lib/invoicing/line_item.rb +17 -0
  24. data/lib/invoicing/line_item_type.rb +6 -0
  25. data/lib/invoicing/overdue_invoice.rb +16 -0
  26. data/lib/invoicing/payment_reference.rb +10 -0
  27. data/lib/invoicing/seller.rb +10 -0
  28. data/lib/invoicing/transaction.rb +13 -0
  29. data/lib/invoicing/version.rb +3 -0
  30. data/lib/invoicing.rb +46 -0
  31. data/spec/README +0 -0
  32. data/spec/internal/config/database.yml.sample +3 -0
  33. data/spec/internal/config/routes.rb +3 -0
  34. data/spec/internal/db/schema.rb +81 -0
  35. data/spec/internal/log/.gitignore +1 -0
  36. data/spec/internal/public/favicon.ico +0 -0
  37. data/spec/lib/invoicing/credit_note_spec.rb +140 -0
  38. data/spec/lib/invoicing/invoice_adjustment_spec.rb +136 -0
  39. data/spec/lib/invoicing/invoice_late_payment_spec.rb +10 -0
  40. data/spec/lib/invoicing/invoice_spec.rb +419 -0
  41. data/spec/lib/invoicing/line_item_spec.rb +14 -0
  42. data/spec/lib/invoicing/overdue_invoice_spec.rb +67 -0
  43. data/spec/spec_helper.rb +20 -0
  44. data/spec/support/helpers.rb +20 -0
  45. metadata +185 -0
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spec/internal/db/*.sqlite
6
+ spec/internal/config/database.yml
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@invoicing --create
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ before_script:
3
+ - cp spec/internal/config/database.yml.sample spec/internal/config/database.yml
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in invoicing.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ gem 'rspec-rails'
9
+ gem 'sqlite3'
10
+ end
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+
2
+ [![Build Status](https://secure.travis-ci.org/tehtorq/invoicing.png)](http://travis-ci.org/tehtorq/invoicing)
3
+
4
+ Basic Usage
5
+
6
+ seller = Seller.find(3)
7
+ book = Book.find(1) # implements CostItem
8
+ decorations = {whatever_you_want: 'here'}
9
+
10
+ invoice = Invoicing::generate do
11
+ from seller
12
+ line_item book
13
+ due Time.now + 7.days
14
+ payment_reference "REF2345"
15
+ decorate_with decorations
16
+ end
17
+
18
+ Invoice Numbering:
19
+
20
+ A default invoice number will be set with the format INV[invoice id].
21
+
22
+ A custom invoice number can be specified as follows:
23
+
24
+ invoice = Invoicing::generate do
25
+ numbered "CUSTOMREF123"
26
+ end
27
+
28
+ You can specify a custom invoice number containing the invoice id as follows:
29
+
30
+ invoice = Invoicing::generate do
31
+ numbered "CUSTOMREF{id}"
32
+ end
33
+
34
+
35
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ # If you want to make this the default task
7
+ task :default => :spec
8
+ require "bundler/gem_tasks"
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize!
7
+ run Combustion::Application
data/invoicing.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "invoicing/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "uomi"
7
+ s.version = Invoicing::VERSION
8
+ s.authors = ["Douglas Anderson", "Jeffrey van Aswegen"]
9
+ s.email = ["i.am.douglas.anderson@gmail.com", "jeffmess@gmail.com"]
10
+ s.homepage = 'https://github.com/tehtorq/invoicing'
11
+ s.summary = %q{ An invoicing gem. }
12
+ s.description = %q{ Manage invoices. }
13
+
14
+ s.rubyforge_project = "uomi"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "activesupport"
22
+ s.add_dependency "activerecord", "~> 3.0"
23
+ s.add_dependency "i18n"
24
+ s.add_dependency "workflow", '= 0.8.7'
25
+
26
+ s.add_development_dependency 'combustion', '~> 0.3.1'
27
+ end
@@ -0,0 +1,30 @@
1
+ module Invoicing
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 invoicing 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_invoicing_tables.rb'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ class CreateInvoicingTables < ActiveRecord::Migration
2
+ def self.change
3
+
4
+ create_table "invoicing_late_payments", :force => true do |t|
5
+ t.integer "invoice_id"
6
+ t.integer "amount"
7
+ t.datetime "penalty_date"
8
+ t.boolean "processed"
9
+ t.timestamps
10
+ end
11
+
12
+ create_table "invoicing_line_items", :force => true do |t|
13
+ t.integer "invoice_id"
14
+ t.string "description"
15
+ t.integer "amount"
16
+ t.integer "tax"
17
+ t.integer "invoiceable_id"
18
+ t.string "invoiceable_type"
19
+ t.integer "line_item_type_id"
20
+ t.timestamps
21
+ end
22
+
23
+ create_table "invoicing_transactions", :force => true do |t|
24
+ t.integer "invoice_id"
25
+ t.string "type"
26
+ t.integer "amount"
27
+ t.timestamps
28
+ end
29
+
30
+ create_table "invoicing_invoices", :force => true do |t|
31
+ t.integer "seller_id"
32
+ t.integer "buyer_id"
33
+ t.string "invoice_number"
34
+ t.datetime "due_date"
35
+ t.datetime "issued_at"
36
+ t.integer "total"
37
+ t.integer "tax"
38
+ t.integer "balance"
39
+ t.string "type"
40
+ t.string "workflow_state"
41
+ t.timestamps
42
+ end
43
+
44
+ create_table "invoicing_payment_references", :force => true do |t|
45
+ t.integer "invoice_id"
46
+ t.string "reference"
47
+ t.timestamps
48
+ end
49
+
50
+ create_table "invoicing_sellers", :force => true do |t|
51
+ t.integer "sellerable_id"
52
+ t.string "sellerable_type"
53
+ t.timestamps
54
+ end
55
+
56
+ create_table "invoicing_buyers", :force => true do |t|
57
+ t.integer "buyerable_id"
58
+ t.string "buyerable_type"
59
+ t.timestamps
60
+ end
61
+
62
+ create_table "invoicing_invoice_decorators", :force => true do |t|
63
+ t.integer "invoice_id"
64
+ t.text "data"
65
+ t.timestamps
66
+ end
67
+
68
+ create_table "invoicing_credit_note_invoices", :force => true do |t|
69
+ t.integer "invoice_id"
70
+ t.integer "credit_note_id"
71
+ end
72
+
73
+ create_table "invoicing_credit_note_credit_transactions", :force => true do |t|
74
+ t.integer "credit_note_id"
75
+ t.integer "transaction_id"
76
+ end
77
+
78
+ create_table "invoicing_line_item_types", :force => true do |t|
79
+ t.string "name"
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,10 @@
1
+ module Invoicing
2
+ class Buyer < ActiveRecord::Base
3
+ has_many :invoices
4
+ belongs_to :buyerable, polymorphic: true
5
+
6
+ def self.for(buyerable)
7
+ Buyer.where(buyerable_type: buyerable.class.name, buyerable_id: buyerable.id).first || Buyer.create!(buyerable_type: buyerable.class.name, buyerable_id: buyerable.id)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,70 @@
1
+ module Invoicing
2
+ class CreditNote < Invoice
3
+ alias_attribute :receipt_number, :invoice_number
4
+
5
+ has_one :credit_note_invoice, dependent: :destroy
6
+ has_one :invoice, through: :credit_note_invoice
7
+ has_many :credit_note_credit_transactions, dependent: :destroy
8
+
9
+ def issue(issued_at = Time.now)
10
+ self.issued_at = issued_at
11
+ create_initial_transaction!
12
+ record_transaction_against_invoice!
13
+ record_credit_notes!
14
+ end
15
+
16
+ def record_transaction_against_invoice!
17
+ raise RuntimeError, "You must allocate a credit note against an invoice" if invoice.blank?
18
+ raise RuntimeError, "You must allocate a credit note against an issued invoice" unless invoice.issued?
19
+
20
+ invoice.add_credit_transaction(amount: total)
21
+ invoice.save!
22
+ CreditNoteCreditTransaction.create!(transaction: invoice.transactions.last, credit_note_id: self.id)
23
+ end
24
+
25
+ def credit(options={})
26
+ add_line_item(
27
+ invoiceable: options[:line_item].invoiceable,
28
+ amount: options[:amount] || 0,
29
+ tax: options[:tax] || 0,
30
+ description: options[:description] || "Credit note against #{options[:line_item].description}" #?
31
+ )
32
+ end
33
+
34
+ def against_invoice(invoice)
35
+ raise RuntimeError, "You must allocate a credit note against an invoice" if invoice.blank?
36
+ raise RuntimeError, "You must allocate a credit note against an issued invoice" unless invoice.issued?
37
+
38
+ self.credit_note_invoice = CreditNoteInvoice.new(invoice_id: invoice.id)
39
+ self.buyer = invoice.buyer
40
+ self.seller = invoice.seller
41
+ end
42
+
43
+ def record_credit_notes!
44
+ line_items.each do |line_item|
45
+ invoiceable = line_item.invoiceable
46
+ next if invoiceable.blank?
47
+ invoiceable.handle_credit(line_item.amount) if invoiceable.respond_to?(:handle_credit)
48
+ invoiceable.save!
49
+ end
50
+ end
51
+
52
+ def annul(params={})
53
+ record_amount_against_invoice(params[:amount], params[:against_invoice]) if params[:against_invoice]
54
+ end
55
+
56
+ def default_numbering_prefix
57
+ "CN"
58
+ end
59
+
60
+ def create_initial_transaction!
61
+ if total > 0
62
+ add_credit_transaction amount: total
63
+ else
64
+ add_debit_transaction amount: total
65
+ end
66
+
67
+ save!
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,6 @@
1
+ module Invoicing
2
+ class CreditNoteCreditTransaction < ActiveRecord::Base
3
+ belongs_to :credit_note
4
+ belongs_to :transaction
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Invoicing
2
+ class CreditNoteInvoice < ActiveRecord::Base
3
+ belongs_to :credit_note
4
+ belongs_to :invoice
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Invoicing
2
+ class CreditTransaction < Transaction
3
+
4
+ def credit?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Invoicing
2
+ class DebitTransaction < Transaction
3
+
4
+ def debit?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module Invoicing
2
+ class CannotVoidDocumentException < Exception; end
3
+ class CannotAdjustIssuedDocument < Exception; end
4
+ end
@@ -0,0 +1,233 @@
1
+ module Invoicing
2
+ class Invoice < ActiveRecord::Base
3
+ include ::Workflow
4
+ has_many :line_items, dependent: :destroy
5
+ has_many :transactions, dependent: :destroy
6
+ has_many :payment_references, dependent: :destroy
7
+ has_one :late_payment, dependent: :destroy
8
+ belongs_to :seller
9
+ belongs_to :buyer
10
+ has_one :invoice_decorator, dependent: :destroy
11
+
12
+ validates_uniqueness_of :invoice_number, scope: [:seller_id]
13
+
14
+ before_save :calculate_totals, :calculate_balance
15
+ after_create :set_invoice_number!
16
+
17
+ alias :decorator :invoice_decorator
18
+
19
+ workflow do
20
+ state :draft do
21
+ event :issue, transitions_to: :issued
22
+ event :void, transitions_to: :voided
23
+ end
24
+
25
+ state :issued do
26
+ event :settle, transitions_to: :settled
27
+ event :void, transitions_to: :voided
28
+ end
29
+
30
+ state :settled
31
+ state :voided
32
+ end
33
+
34
+ def issue(&block)
35
+ self.issued_at = Time.now
36
+ instance_eval(&block) if block_given?
37
+ create_initial_transaction!
38
+ mark_items_invoiced!
39
+ self
40
+ end
41
+
42
+ def void
43
+ raise CannotVoidDocumentException, "Cannot void a document that has a transaction recorded against it!" if transactions.many?
44
+ annul_remaining_amount! unless self.draft?
45
+ mark_items_uninvoiced!
46
+ self
47
+ end
48
+
49
+ def add_line_item(params)
50
+ self.line_items << LineItem.new(params)
51
+ end
52
+
53
+ def remove_line_item(item)
54
+ line_items.delete(item)
55
+ end
56
+
57
+ def add_debit_transaction(params)
58
+ self.transactions << DebitTransaction.new(params)
59
+ end
60
+
61
+ def add_credit_transaction(params)
62
+ self.transactions << CreditTransaction.new(params)
63
+ end
64
+
65
+ def calculate_totals
66
+ self.total = line_items.inject(0) {|res, item| res + item.amount.to_i}
67
+ self.tax = line_items.inject(0) {|res, item| res + item.tax.to_i}
68
+ end
69
+
70
+ def annul_remaining_amount!
71
+ add_credit_transaction amount: balance.abs
72
+ end
73
+
74
+ def create_initial_transaction!
75
+ if total < 0
76
+ add_credit_transaction amount: total
77
+ else
78
+ add_debit_transaction amount: total
79
+ end
80
+ end
81
+
82
+ def credit_notes
83
+ CreditNoteInvoice.where(invoice_id: id).map(&:credit_note)
84
+ end
85
+
86
+ def default_numbering_prefix
87
+ "INV"
88
+ end
89
+
90
+ def set_invoice_number!
91
+ self.invoice_number ||= "#{default_numbering_prefix}#{id}"
92
+ self.invoice_number.gsub!("{id}", "#{id}")
93
+ save!
94
+ end
95
+
96
+ def debit_transactions
97
+ transactions.select{|t| t.is_a? DebitTransaction}
98
+ end
99
+
100
+ def credit_transactions
101
+ transactions.select{|t| t.is_a? CreditTransaction}
102
+ end
103
+
104
+ def calculate_balance
105
+ self.balance = (0 - debit_transactions.sum(&:amount)) + credit_transactions.sum(&:amount)
106
+ settle! if should_settle?
107
+ end
108
+
109
+ def should_settle?
110
+ issued? && balance_zero?
111
+ end
112
+
113
+ def net_total
114
+ total - tax
115
+ end
116
+
117
+ def balance_zero?
118
+ balance == 0
119
+ end
120
+
121
+ def owing?
122
+ balance < 0
123
+ end
124
+
125
+ def due_date_past?
126
+ due_date.to_date < Date.today
127
+ end
128
+
129
+ def overdue?
130
+ owing? and due_date_past?
131
+ end
132
+
133
+ def self.owing
134
+ where("balance < ?", 0)
135
+ end
136
+
137
+ def self.issued
138
+ where(workflow_state: "issued")
139
+ end
140
+
141
+ def self.draft
142
+ where(workflow_state: "draft")
143
+ end
144
+
145
+ def self.settled
146
+ where(workflow_state: "settled")
147
+ end
148
+
149
+ def self.voided
150
+ where(workflow_state: "voided")
151
+ end
152
+
153
+ def add_payment_reference(params)
154
+ self.payment_references << PaymentReference.new(params)
155
+ end
156
+
157
+ def remove_payment_reference(payment_reference)
158
+ payment_references.delete(payment_reference)
159
+ end
160
+
161
+ def self.for_payment_reference(reference)
162
+ PaymentReference.where(reference: reference).map(&:invoice)
163
+ end
164
+
165
+ def mark_items_invoiced!
166
+ line_items.map(&:invoiceable).compact.each do |item|
167
+ item.mark_invoiced(self) if item.respond_to?(:mark_invoiced)
168
+ end
169
+ end
170
+
171
+ def mark_items_uninvoiced!
172
+ line_items.map(&:invoiceable).compact.each do |item|
173
+ item.mark_uninvoiced(self) if item.respond_to?(:mark_uninvoiced)
174
+ end
175
+ end
176
+
177
+ def line_item(cost_item)
178
+ if cost_item.is_a? Hash
179
+ add_line_item(
180
+ amount: cost_item[:amount] || 0,
181
+ tax: cost_item[:tax] || 0,
182
+ description: cost_item[:description] || 'Line Item',
183
+ line_item_type_id: cost_item[:line_item_type_id]
184
+ )
185
+ else
186
+ add_line_item(
187
+ invoiceable: cost_item,
188
+ amount: cost_item.amount || 0,
189
+ tax: cost_item.tax || 0,
190
+ description: cost_item.description || 'Line Item',
191
+ line_item_type_id: cost_item.respond_to?(:line_item_type_id) ? cost_item.line_item_type_id : 0
192
+ )
193
+ end
194
+ end
195
+
196
+ def payment_reference(reference)
197
+ add_payment_reference(reference: reference)
198
+ end
199
+
200
+ def due(due_date)
201
+ self.due_date = due_date
202
+ end
203
+
204
+ def to(buyerable)
205
+ self.buyer = Buyer.for(buyerable)
206
+ end
207
+
208
+ def from(sellerable)
209
+ self.seller = Seller.for(sellerable)
210
+ end
211
+
212
+ def decorate_with(decorations)
213
+ if self.invoice_decorator
214
+ self.invoice_decorator.data = decorations
215
+ else
216
+ self.invoice_decorator = InvoiceDecorator.new(data: decorations)
217
+ end
218
+ end
219
+
220
+ def numbered(invoice_number)
221
+ self.invoice_number = invoice_number
222
+ end
223
+
224
+ def adjust(&block)
225
+ adjustment = InvoiceAdjustment.new(self)
226
+ adjustment.instance_eval(&block)
227
+ adjustment.persist!
228
+ adjustment.invoice
229
+ end
230
+
231
+ end
232
+
233
+ end
@@ -0,0 +1,59 @@
1
+ module Invoicing
2
+
3
+ class InvoiceAdjustment
4
+
5
+ attr_accessor :invoice
6
+
7
+ def initialize(invoice)
8
+ raise CannotAdjustIssuedDocument unless invoice.draft?
9
+ self.invoice = invoice
10
+ end
11
+
12
+ def due(due_date)
13
+ invoice.due(due_date)
14
+ end
15
+
16
+ def add_line_item(params)
17
+ invoice.line_item(params)
18
+ end
19
+
20
+ def edit_line_item(item, params)
21
+ raise CannotEditNonExistantLineItem if invoice.line_items.find(item.id).blank?
22
+ self.invoice.line_items.for(item).update_attributes(params)
23
+ end
24
+
25
+ def remove_line_item(item)
26
+ invoice.line_items.delete(item)
27
+ end
28
+
29
+ def add_payment_reference(payment_reference)
30
+ invoice.payment_reference(payment_reference)
31
+ end
32
+
33
+ def remove_payment_reference(payment_reference)
34
+ invoice.remove_payment_reference(payment_reference)
35
+ end
36
+
37
+ def to(buyerable)
38
+ invoice.to(buyerable)
39
+ end
40
+
41
+ def numbered(invoice_number)
42
+ invoice.numbered(invoice_number)
43
+ end
44
+
45
+ def decorate_with(decorations)
46
+ invoice.decorate_with(decorations)
47
+ end
48
+
49
+ def persist!
50
+ # this method makes me a little sad.
51
+ self.invoice.transaction do
52
+ self.invoice.invoice_decorator.save
53
+ self.invoice.line_items.map(&:reload)
54
+ self.invoice.save!
55
+ end
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,6 @@
1
+ module Invoicing
2
+ class InvoiceDecorator < ActiveRecord::Base
3
+ belongs_to :invoice
4
+ serialize :data
5
+ end
6
+ end
@@ -0,0 +1,22 @@
1
+ # implement this module and override behaviour on items which will be invoiced
2
+
3
+ module Invoicing
4
+ module Invoiceable
5
+
6
+ attr_accessor :invoiced, :invoice_id, :amount, :tax, :line_item_type_id
7
+
8
+ def description
9
+ "Invoiceable Item"
10
+ end
11
+
12
+ def handle_credit(amount)
13
+ end
14
+
15
+ def mark_invoiced(invoice)
16
+ end
17
+
18
+ def mark_uninvoiced
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Invoicing
2
+ class LatePayment < ActiveRecord::Base
3
+ belongs_to :invoice
4
+
5
+ before_create :set_penalty_date
6
+
7
+ def set_penalty_date
8
+ self.penalty_date = Date.today + 7.days
9
+ end
10
+ end
11
+ end