uomi 0.2.13
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +10 -0
- data/README.md +35 -0
- data/Rakefile +8 -0
- data/config.ru +7 -0
- data/invoicing.gemspec +27 -0
- data/lib/generators/active_record/invoicing_generator.rb +30 -0
- data/lib/generators/active_record/templates/migration.rb +84 -0
- data/lib/invoicing/buyer.rb +10 -0
- data/lib/invoicing/credit_note.rb +70 -0
- data/lib/invoicing/credit_note_credit_transaction.rb +6 -0
- data/lib/invoicing/credit_note_invoice.rb +6 -0
- data/lib/invoicing/credit_transaction.rb +9 -0
- data/lib/invoicing/debit_transaction.rb +9 -0
- data/lib/invoicing/exception.rb +4 -0
- data/lib/invoicing/invoice.rb +233 -0
- data/lib/invoicing/invoice_adjustment.rb +59 -0
- data/lib/invoicing/invoice_decorator.rb +6 -0
- data/lib/invoicing/invoiceable.rb +22 -0
- data/lib/invoicing/late_payment.rb +11 -0
- data/lib/invoicing/line_item.rb +17 -0
- data/lib/invoicing/line_item_type.rb +6 -0
- data/lib/invoicing/overdue_invoice.rb +16 -0
- data/lib/invoicing/payment_reference.rb +10 -0
- data/lib/invoicing/seller.rb +10 -0
- data/lib/invoicing/transaction.rb +13 -0
- data/lib/invoicing/version.rb +3 -0
- data/lib/invoicing.rb +46 -0
- data/spec/README +0 -0
- data/spec/internal/config/database.yml.sample +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +81 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/invoicing/credit_note_spec.rb +140 -0
- data/spec/lib/invoicing/invoice_adjustment_spec.rb +136 -0
- data/spec/lib/invoicing/invoice_late_payment_spec.rb +10 -0
- data/spec/lib/invoicing/invoice_spec.rb +419 -0
- data/spec/lib/invoicing/line_item_spec.rb +14 -0
- data/spec/lib/invoicing/overdue_invoice_spec.rb +67 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/helpers.rb +20 -0
- metadata +185 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Invoicing
|
2
|
+
class LineItem < ActiveRecord::Base
|
3
|
+
belongs_to :invoice
|
4
|
+
belongs_to :invoiceable, polymorphic: true
|
5
|
+
belongs_to :line_item_type
|
6
|
+
|
7
|
+
validates :amount, numericality: {greater_than_or_equal_to: 0}, presence: true
|
8
|
+
|
9
|
+
def net_amount
|
10
|
+
amount - tax
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for(item)
|
14
|
+
where(id: item.id).first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Invoicing
|
2
|
+
class OverdueInvoice
|
3
|
+
|
4
|
+
def self.all
|
5
|
+
Invoice.owing.select{|invoice| invoice.overdue?}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.record_late_payments!
|
9
|
+
OverdueInvoice.all.each do |invoice|
|
10
|
+
invoice.late_payment = LatePayment.new amount: invoice.balance.abs
|
11
|
+
invoice.save!
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Invoicing
|
2
|
+
class Seller < ActiveRecord::Base
|
3
|
+
has_many :invoices
|
4
|
+
belongs_to :sellerable, polymorphic: true
|
5
|
+
|
6
|
+
def self.for(sellerable)
|
7
|
+
Seller.where(sellerable_type: sellerable.class.name, sellerable_id: sellerable.id).first || Seller.create!(sellerable_type: sellerable.class.name, sellerable_id: sellerable.id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/invoicing.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "workflow"
|
3
|
+
require "invoicing/version"
|
4
|
+
|
5
|
+
module Invoicing
|
6
|
+
|
7
|
+
def self.table_name_prefix
|
8
|
+
'invoicing_'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.generate(&block)
|
12
|
+
invoice = Invoice.new
|
13
|
+
invoice.instance_eval(&block)
|
14
|
+
invoice.save!
|
15
|
+
invoice
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.generate_credit_note(&block)
|
19
|
+
credit_note = CreditNote.new
|
20
|
+
credit_note.instance_eval(&block)
|
21
|
+
credit_note.save!
|
22
|
+
credit_note.issue!
|
23
|
+
credit_note
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
require "invoicing/exception"
|
29
|
+
require "invoicing/invoiceable"
|
30
|
+
require "invoicing/transaction"
|
31
|
+
require "invoicing/credit_transaction"
|
32
|
+
require "invoicing/debit_transaction"
|
33
|
+
require "invoicing/late_payment"
|
34
|
+
require "invoicing/line_item"
|
35
|
+
require "invoicing/payment_reference"
|
36
|
+
require "invoicing/seller"
|
37
|
+
require "invoicing/buyer"
|
38
|
+
require "invoicing/invoice_decorator"
|
39
|
+
require "invoicing/credit_note_invoice"
|
40
|
+
require "invoicing/credit_note_credit_transaction"
|
41
|
+
|
42
|
+
require "invoicing/invoice"
|
43
|
+
require "invoicing/credit_note"
|
44
|
+
require "invoicing/overdue_invoice"
|
45
|
+
require "invoicing/invoice_adjustment"
|
46
|
+
require "invoicing/line_item_type"
|
data/spec/README
ADDED
File without changes
|
@@ -0,0 +1,81 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
create_table "invoicing_late_payments", :force => true do |t|
|
4
|
+
t.integer "invoice_id"
|
5
|
+
t.integer "amount"
|
6
|
+
t.datetime "penalty_date"
|
7
|
+
t.boolean "processed"
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table "invoicing_line_items", :force => true do |t|
|
12
|
+
t.integer "invoice_id"
|
13
|
+
t.string "description"
|
14
|
+
t.integer "amount"
|
15
|
+
t.integer "tax"
|
16
|
+
t.integer "invoiceable_id"
|
17
|
+
t.string "invoiceable_type"
|
18
|
+
t.integer "line_item_type_id"
|
19
|
+
t.timestamps
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "invoicing_transactions", :force => true do |t|
|
23
|
+
t.integer "invoice_id"
|
24
|
+
t.string "type"
|
25
|
+
t.integer "amount"
|
26
|
+
t.timestamps
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table "invoicing_invoices", :force => true do |t|
|
30
|
+
t.integer "seller_id"
|
31
|
+
t.integer "buyer_id"
|
32
|
+
t.string "invoice_number"
|
33
|
+
t.datetime "due_date"
|
34
|
+
t.datetime "issued_at"
|
35
|
+
t.integer "total"
|
36
|
+
t.integer "tax"
|
37
|
+
t.integer "balance"
|
38
|
+
t.string "type"
|
39
|
+
t.string "workflow_state"
|
40
|
+
t.timestamps
|
41
|
+
end
|
42
|
+
|
43
|
+
create_table "invoicing_payment_references", :force => true do |t|
|
44
|
+
t.integer "invoice_id"
|
45
|
+
t.string "reference"
|
46
|
+
t.timestamps
|
47
|
+
end
|
48
|
+
|
49
|
+
create_table "invoicing_sellers", :force => true do |t|
|
50
|
+
t.integer "sellerable_id"
|
51
|
+
t.string "sellerable_type"
|
52
|
+
t.timestamps
|
53
|
+
end
|
54
|
+
|
55
|
+
create_table "invoicing_buyers", :force => true do |t|
|
56
|
+
t.integer "buyerable_id"
|
57
|
+
t.string "buyerable_type"
|
58
|
+
t.timestamps
|
59
|
+
end
|
60
|
+
|
61
|
+
create_table "invoicing_invoice_decorators", :force => true do |t|
|
62
|
+
t.integer "invoice_id"
|
63
|
+
t.text "data"
|
64
|
+
t.timestamps
|
65
|
+
end
|
66
|
+
|
67
|
+
create_table "invoicing_credit_note_invoices", :force => true do |t|
|
68
|
+
t.integer "invoice_id"
|
69
|
+
t.integer "credit_note_id"
|
70
|
+
end
|
71
|
+
|
72
|
+
create_table "invoicing_credit_note_credit_transactions", :force => true do |t|
|
73
|
+
t.integer "credit_note_id"
|
74
|
+
t.integer "transaction_id"
|
75
|
+
end
|
76
|
+
|
77
|
+
create_table "invoicing_line_item_types", :force => true do |t|
|
78
|
+
t.string "name"
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
File without changes
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Invoicing::CreditNote do
|
4
|
+
include Helpers
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
tear_it_down
|
8
|
+
|
9
|
+
invoice_buyer = Invoicing::DebitTransaction.create!
|
10
|
+
invoice_seller = Invoicing::DebitTransaction.create!
|
11
|
+
|
12
|
+
@invoice = Invoicing::generate do
|
13
|
+
to invoice_buyer
|
14
|
+
from invoice_seller
|
15
|
+
|
16
|
+
line_item description: "Line Item 1", amount: 1101
|
17
|
+
line_item description: "Line Item 2", amount: 5097
|
18
|
+
line_item description: "Line Item 3", amount: 1714
|
19
|
+
line_item description: "Line Item 4", amount: 20300
|
20
|
+
|
21
|
+
payment_reference "REF123"
|
22
|
+
decorate_with tenant_name: "Peter"
|
23
|
+
end
|
24
|
+
|
25
|
+
@invoice_buyer = invoice_buyer
|
26
|
+
@invoice_seller = invoice_seller
|
27
|
+
|
28
|
+
@invoice.issue!
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when recording a credit note" do
|
32
|
+
before(:each) do
|
33
|
+
@invoice.line_items.each do |li|
|
34
|
+
li.invoiceable = li
|
35
|
+
li.invoiceable.extend(Invoicing::Invoiceable)
|
36
|
+
li.amount = 0
|
37
|
+
li.tax = 0
|
38
|
+
li.save!
|
39
|
+
end
|
40
|
+
|
41
|
+
@invoice.reload
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should raise an exception if the invoice is not in an issued state" do
|
45
|
+
lambda {
|
46
|
+
@credit_note = Invoicing::generate_credit_note do
|
47
|
+
against_invoice @invoice
|
48
|
+
end
|
49
|
+
}.should raise_error(RuntimeError, "You must allocate a credit note against an invoice")
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
context "against an invoice" do
|
54
|
+
before(:each) do
|
55
|
+
invoice = @invoice
|
56
|
+
line_items = invoice.line_items
|
57
|
+
|
58
|
+
@credit_note = Invoicing::generate_credit_note do
|
59
|
+
line_items.each do |line_item|
|
60
|
+
credit amount: line_item.amount, line_item: line_item, description: "Credit note for Line Item #{line_item.id}", tax: 0
|
61
|
+
end
|
62
|
+
|
63
|
+
against_invoice invoice
|
64
|
+
end
|
65
|
+
|
66
|
+
@invoice.reload
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should be issued" do
|
70
|
+
@credit_note.should be_issued
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should have a receipt number" do
|
74
|
+
@credit_note.receipt_number.should == "CN#{@credit_note.id}"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should be able to record a line item for the credit note" do
|
78
|
+
@credit_note.line_items.count.should == @invoice.line_items.count
|
79
|
+
@credit_note.line_items.first.amount.should == @invoice.line_items.first.amount
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should create a credit transaction against the invoice" do
|
83
|
+
@invoice.settled?.should be_true
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should know if it is linked to an invoice" do
|
87
|
+
@credit_note.invoice.should == @invoice
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should be linked to the credit transaction paid against the invoice" do
|
91
|
+
@credit_note.credit_note_credit_transactions.count.should == 1
|
92
|
+
@credit_note.credit_note_credit_transactions.first.transaction.amount.should == @invoice.total
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should be recorded against the invoice" do
|
96
|
+
@invoice.reload
|
97
|
+
@invoice.credit_notes.count.should == 1
|
98
|
+
@invoice.credit_notes.first.receipt_number.should == "CN#{@credit_note.id}"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should set the buyer to the invoice's buyer" do
|
102
|
+
@credit_note.buyer.buyerable.should == @invoice_buyer
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should set the seller to the invoice's seller" do
|
106
|
+
@credit_note.seller.sellerable.should == @invoice_seller
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
context "When applying credits to the invoiceables" do
|
112
|
+
|
113
|
+
it "should receive handle credit for each line item being credited" do
|
114
|
+
credit_note = Invoicing::CreditNote.new
|
115
|
+
credit_note.line_items = @invoice.line_items
|
116
|
+
|
117
|
+
credit_note.line_items.map(&:invoiceable).compact.each do |item|
|
118
|
+
item.should_receive(:handle_credit)
|
119
|
+
end
|
120
|
+
|
121
|
+
credit_note.record_credit_notes!
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
context "trying to create a standalone credit note" do
|
127
|
+
|
128
|
+
it "should raise an error if no invoice is specified" do
|
129
|
+
expect {Invoicing::generate_credit_note do
|
130
|
+
line_items.each do |line_item|
|
131
|
+
credit amount: 500, description: "Credit note for Line Item #{line_item.id}", tax: 0
|
132
|
+
end
|
133
|
+
decorate_with tenant_name: "Peter"
|
134
|
+
end}.to raise_error(RuntimeError, "You must allocate a credit note against an invoice")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Invoicing::InvoiceAdjustment do
|
4
|
+
include Helpers
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
tear_it_down
|
8
|
+
|
9
|
+
buyer = Invoicing::DebitTransaction.create!
|
10
|
+
|
11
|
+
@invoice = Invoicing::generate do
|
12
|
+
to buyer
|
13
|
+
numbered "INV123"
|
14
|
+
|
15
|
+
line_item description: "Line Item 1", amount: 1101, line_item_type_id: 1
|
16
|
+
line_item description: "Line Item 2", amount: 5097, line_item_type_id: 1
|
17
|
+
line_item description: "Line Item 3", amount: 1714, line_item_type_id: 1
|
18
|
+
|
19
|
+
payment_reference "REF123"
|
20
|
+
decorate_with tenant_name: "Peter"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "adjusting a draft invoice" do
|
25
|
+
|
26
|
+
it "should allow the due date to be updated" do
|
27
|
+
@invoice.adjust do
|
28
|
+
due Date.tomorrow
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should allow the buyer to be changed" do
|
33
|
+
new_buyer = Invoicing::DebitTransaction.create!
|
34
|
+
|
35
|
+
@invoice.adjust do
|
36
|
+
to new_buyer
|
37
|
+
end
|
38
|
+
|
39
|
+
@invoice.buyer.buyerable.should == new_buyer
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should allow a payment references to be added" do
|
43
|
+
@invoice.adjust do
|
44
|
+
add_payment_reference "NEWREF123"
|
45
|
+
end
|
46
|
+
|
47
|
+
@invoice.payment_references.count.should == 2
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should allow a payment references to be removed" do
|
51
|
+
@invoice.adjust do
|
52
|
+
remove_payment_reference @invoice.payment_references.last
|
53
|
+
end
|
54
|
+
|
55
|
+
@invoice.payment_references.count.should == 0
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should allow the invoice decorator to be updated" do
|
59
|
+
decorations = {tenant_name: "Bob"}
|
60
|
+
|
61
|
+
@invoice.adjust do
|
62
|
+
decorate_with decorations
|
63
|
+
end
|
64
|
+
|
65
|
+
# @invoice.reload
|
66
|
+
|
67
|
+
@invoice.decorator.data.should == decorations
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should allow line items to be added" do
|
71
|
+
item_to_invoice = @invoice.extend(Invoicing::Invoiceable)
|
72
|
+
@invoice.line_items.count.should == 3
|
73
|
+
|
74
|
+
@invoice.adjust do
|
75
|
+
add_line_item(item_to_invoice)
|
76
|
+
end
|
77
|
+
|
78
|
+
@invoice.line_items.count.should == 4
|
79
|
+
@invoice.total.should == 7912
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should allow line items to be edited" do
|
83
|
+
line_item = @invoice.line_items.first
|
84
|
+
item_to_invoice = @invoice.extend(Invoicing::Invoiceable)
|
85
|
+
|
86
|
+
@invoice.adjust do
|
87
|
+
edit_line_item(line_item, {description: "Modified Line Item", amount: 500})
|
88
|
+
end
|
89
|
+
|
90
|
+
@invoice.line_items.count.should == 3
|
91
|
+
@invoice.total.should == 7311
|
92
|
+
|
93
|
+
line_item.reload
|
94
|
+
|
95
|
+
line_item.description.should == "Modified Line Item"
|
96
|
+
line_item.amount.should == 500
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should allow line items to be removed" do
|
100
|
+
@invoice.line_items.count.should == 3
|
101
|
+
|
102
|
+
@invoice.adjust do
|
103
|
+
remove_line_item(@invoice.line_items.first)
|
104
|
+
end
|
105
|
+
|
106
|
+
@invoice.line_items.count.should == 2
|
107
|
+
@invoice.total.should == 6811
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
context "Adjusting an Invoice Number" do
|
113
|
+
it "should allow the invoice number to be updated" do
|
114
|
+
@invoice.adjust do
|
115
|
+
numbered "DRAFT-INV123"
|
116
|
+
end
|
117
|
+
|
118
|
+
@invoice.invoice_number.should == "DRAFT-INV123"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "adjusting an issued invoice" do
|
123
|
+
|
124
|
+
it "should raise an exception" do
|
125
|
+
@invoice.issue!
|
126
|
+
|
127
|
+
expect {
|
128
|
+
@invoice.adjust do
|
129
|
+
due Date.tomorrow
|
130
|
+
end
|
131
|
+
}.to raise_error(Invoicing::CannotAdjustIssuedDocument)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|