uomi 0.2.13

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 (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