payday 1.0.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +9 -0
- data/README.md +75 -0
- data/Rakefile +12 -0
- data/assets/default_logo.png +0 -0
- data/lib/payday.rb +12 -0
- data/lib/payday/config.rb +21 -0
- data/lib/payday/invoice.rb +26 -0
- data/lib/payday/invoiceable.rb +60 -0
- data/lib/payday/line_item.rb +29 -0
- data/lib/payday/line_itemable.rb +8 -0
- data/lib/payday/pdf_renderer.rb +220 -0
- data/lib/payday/version.rb +3 -0
- data/payday.gemspec +21 -0
- data/test/invoice_test.rb +116 -0
- data/test/line_item_test.rb +25 -0
- data/test/test_helper.rb +30 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
Payday!
|
2
|
+
===
|
3
|
+
Payday is a library for rendering invoices. At present it supports rendering invoices to pdfs, but we're planning on adding support for other formats in the near future.
|
4
|
+
|
5
|
+
Using Payday
|
6
|
+
===
|
7
|
+
It's pretty easy to use Payday with the built in objects. We include the Invoice and LineItem classes, and with them you can get started pretty quickly.
|
8
|
+
|
9
|
+
Example:
|
10
|
+
|
11
|
+
invoice = Payday::Invoice.new(:invoice_number => 12)
|
12
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
13
|
+
i.line_items << LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
|
14
|
+
i.line_items << LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
|
15
|
+
i.render_pdf_to_file("/path/to_file.pdf")
|
16
|
+
|
17
|
+
Customizing Your Logo and Company Name
|
18
|
+
===
|
19
|
+
Check out Payday::Config to customize your company's name, details, and logo.
|
20
|
+
|
21
|
+
Example:
|
22
|
+
|
23
|
+
Payday::Config.default.invoice_log = "/path/to/company/logo.png"
|
24
|
+
Payday::Config.default.company_name = "Awesome Corp"
|
25
|
+
Payday::Config.default.company_details = "10 This Way\nManhattan, NY 10001\n800-111-2222\nawesome@awesomecorp.com"
|
26
|
+
|
27
|
+
Using Payday with ActiveRecord Objects (or any other objects, for that matter)
|
28
|
+
===
|
29
|
+
|
30
|
+
TODO
|
31
|
+
|
32
|
+
Rendering Payday PDFs To The Web
|
33
|
+
===
|
34
|
+
Payday's Invoiceable module includes methods for rendering pdfs to disk and for rendering them to a string. In a Rails controller, you can use the
|
35
|
+
render to string method to render a pdf directly to the browser like this:
|
36
|
+
|
37
|
+
In config/initializers/mime_types.rb:
|
38
|
+
|
39
|
+
Mime::Type.register 'application/pdf', :pdf
|
40
|
+
|
41
|
+
In your controller:
|
42
|
+
|
43
|
+
respond_to do |format|
|
44
|
+
format.html
|
45
|
+
format.pdf do
|
46
|
+
send_data invoice.render_pdf, :filename => "Invoice #12.pdf", :type => "application/pdf", :disposition => :inline
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Be sure to restart your server after you edit the mime_types initializer. The updating setting won't take effect until you do.
|
51
|
+
|
52
|
+
Contributing
|
53
|
+
===
|
54
|
+
Payday is pretty young, so there's still a good bit of work to be done. Feel free to fork the project, make some changes, and send a pull request. If you're unsure about what to work on, send me a message on GitHub. I'd love the help!
|
55
|
+
|
56
|
+
To Do
|
57
|
+
===
|
58
|
+
Here's what we're planning on working on with Payday in the near future:
|
59
|
+
|
60
|
+
* Package as gem
|
61
|
+
* Document how to use with ActiveRecord
|
62
|
+
* Release 1.0!
|
63
|
+
|
64
|
+
* Add support for currencies other than USD
|
65
|
+
* Add support for Money gem or BigDecimal or general numerics (right now we support BigDecimal and general numerics)
|
66
|
+
* Add support for blank line items
|
67
|
+
* Add support for indented line items
|
68
|
+
* Apply different tax rates to different line items
|
69
|
+
* Add support for shipping either pre or post tax
|
70
|
+
* Add ability to set pdf document size to something other than 8.5 x 11
|
71
|
+
* Add invoice_details has for stuff under the invoice number
|
72
|
+
* Add ability to show skus or product ids on each line item
|
73
|
+
|
74
|
+
* Add page numbers
|
75
|
+
* Ability to render invoice to html for web viewing
|
data/Rakefile
ADDED
Binary file
|
data/lib/payday.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Not much to see here
|
2
|
+
require 'date'
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'prawn'
|
5
|
+
|
6
|
+
require File.join(File.dirname(__FILE__), "payday", "version")
|
7
|
+
require File.join(File.dirname(__FILE__), "payday", "config")
|
8
|
+
require File.join(File.dirname(__FILE__), "payday", "line_itemable")
|
9
|
+
require File.join(File.dirname(__FILE__), "payday", "line_item")
|
10
|
+
require File.join(File.dirname(__FILE__), "payday", "pdf_renderer")
|
11
|
+
require File.join(File.dirname(__FILE__), "payday", "invoiceable")
|
12
|
+
require File.join(File.dirname(__FILE__), "payday", "invoice")
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Payday
|
2
|
+
|
3
|
+
# Configuration for Payday. This is a singleton, so to set the company_name you would call
|
4
|
+
# +Payday::Config.default.company_name = "Awesome Corp"+.
|
5
|
+
class Config
|
6
|
+
attr_accessor :invoice_logo, :company_name, :company_details, :date_format, :currency
|
7
|
+
|
8
|
+
# Returns the default configuration instance
|
9
|
+
def self.default
|
10
|
+
@@default ||= new
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def initialize
|
15
|
+
self.invoice_logo = File.join(File.dirname(__FILE__), "..", "..", "assets", "default_logo.png")
|
16
|
+
self.company_name = "Awesome Corp"
|
17
|
+
self.company_details = "awesomecorp@commondream.net"
|
18
|
+
self.date_format = "%B %e, %Y"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Payday
|
2
|
+
|
3
|
+
# Basically just an invoice. Stick a ton of line items in it, add some details, and then render it out!
|
4
|
+
class Invoice
|
5
|
+
include Payday::Invoiceable
|
6
|
+
|
7
|
+
attr_accessor :invoice_number, :bill_to, :ship_to, :notes, :line_items, :tax_rate, :tax_description, :due_on, :paid_at
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
self.invoice_number = options[:invoice_number] || nil
|
11
|
+
self.bill_to = options[:bill_to] || nil
|
12
|
+
self.ship_to = options[:ship_to] || nil
|
13
|
+
self.notes = options[:notes] || nil
|
14
|
+
self.line_items = options[:line_items] || []
|
15
|
+
self.tax_rate = options[:tax_rate] || nil
|
16
|
+
self.tax_description = options[:tax_description] || nil
|
17
|
+
self.due_on = options[:due_on] || nil
|
18
|
+
self.paid_at = options[:paid_at] || nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# The tax rate that we're applying, as a BigDecimal
|
22
|
+
def tax_rate=(value)
|
23
|
+
@tax_rate = BigDecimal.new(value.to_s)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Include {Payday::Invoiceable} in your Invoice class to make it Payday compatible. Payday
|
2
|
+
# expects that a +line_items+ method containing an Enumerable of {Payday::LineItem} compatible
|
3
|
+
# elements exists. Those LineItem objects primarily need to include quantity, price, and description methods.
|
4
|
+
#
|
5
|
+
# The +bill_to+ method should always be overwritten by your class. Otherwise, it'll say that your invoice should
|
6
|
+
# be billed to Goofy McGoofison. +ship_to+ is also available, but will not be used in rendered invoices if it
|
7
|
+
# doesn't exist.
|
8
|
+
#
|
9
|
+
# Although not required, if a +tax_rate+ method exists, {Payday::Invoiceable} will use it to calculate tax
|
10
|
+
# when generating an invoice. We include a simple tax method that calculates tax, but it's probably wiser
|
11
|
+
# to override this in your class (our calculated tax won't be stored to a database by default, for example).
|
12
|
+
#
|
13
|
+
# If the +due_on+ and +paid_at+ methods are available, {Payday::Invoiceable} will use them to show due dates and
|
14
|
+
# paid dates, as well as stamps showing if the invoice is paid or due.
|
15
|
+
module Payday::Invoiceable
|
16
|
+
|
17
|
+
# Who the invoice is being sent to.
|
18
|
+
def bill_to
|
19
|
+
"Goofy McGoofison\nYour Invoice Doesn't\nHave It's Own BillTo Method"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Calculates the subtotal of this invoice by adding up all of the line items
|
23
|
+
def subtotal
|
24
|
+
line_items.inject(BigDecimal.new("0")) { |result, item| result += item.amount }
|
25
|
+
end
|
26
|
+
|
27
|
+
# The tax for this invoice, as a BigDecimal
|
28
|
+
def tax
|
29
|
+
if defined?(tax_rate)
|
30
|
+
calculated = subtotal * tax_rate
|
31
|
+
return 0 if calculated < 0
|
32
|
+
calculated
|
33
|
+
else
|
34
|
+
0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Calculates the total for this invoice.
|
39
|
+
def total
|
40
|
+
subtotal + tax
|
41
|
+
end
|
42
|
+
|
43
|
+
def overdue?
|
44
|
+
defined?(:due_on) && due_on.is_a?(Date) && due_on < Date.today && !paid_at
|
45
|
+
end
|
46
|
+
|
47
|
+
def paid?
|
48
|
+
defined?(:paid_at) && !!paid_at
|
49
|
+
end
|
50
|
+
|
51
|
+
# Renders this invoice to pdf as a string
|
52
|
+
def render_pdf
|
53
|
+
Payday::PdfRenderer.render(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Renders this invoice to pdf
|
57
|
+
def render_pdf_to_file(path)
|
58
|
+
Payday::PdfRenderer.render_to_file(self, path)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Payday
|
2
|
+
# Represents a line item in an invoice.
|
3
|
+
#
|
4
|
+
# +quantity+ and +price+ are written to be pretty picky, primarily because if we're not picky about what values are set to
|
5
|
+
# them your invoice math could get pretty messed up. It's recommended that both values be set to +BigDecimal+ values.
|
6
|
+
# Otherwise, we'll do our best to convert the set values to a +BigDecimal+.
|
7
|
+
class LineItem
|
8
|
+
include LineItemable
|
9
|
+
|
10
|
+
attr_accessor :description, :quantity, :price
|
11
|
+
|
12
|
+
# Initializes a new LineItem
|
13
|
+
def initialize(options = {})
|
14
|
+
self.quantity = options[:quantity] || "1"
|
15
|
+
self.price = options[:price] || "0.00"
|
16
|
+
self.description = options[:description] || ""
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets the quantity of this {LineItem}
|
20
|
+
def quantity=(value)
|
21
|
+
@quantity = BigDecimal.new(value.to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sets the price for this {LineItem}
|
25
|
+
def price=(value)
|
26
|
+
@price = BigDecimal.new(value.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# Include this module into your line item implementation to make sure that Payday stays happy
|
2
|
+
# with it, or just make sure that your line item implements the amount method.
|
3
|
+
module Payday::LineItemable
|
4
|
+
# Returns the total amount for this {LineItemable}, or {#price} * {#quantity}
|
5
|
+
def amount
|
6
|
+
price * quantity
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Payday
|
2
|
+
|
3
|
+
# The PDF renderer. We use this internally in Payday to render pdfs, but really you should just need to call
|
4
|
+
# {{Payday::Invoiceable#render_pdf}} to render pdfs yourself.
|
5
|
+
class PdfRenderer
|
6
|
+
|
7
|
+
# Renders the given invoice as a pdf on disk
|
8
|
+
def self.render_to_file(invoice, path)
|
9
|
+
pdf(invoice).render_file(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Renders the given invoice as a pdf, returning a string
|
13
|
+
def self.render(invoice)
|
14
|
+
pdf(invoice).render
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def self.pdf(invoice)
|
19
|
+
pdf = Prawn::Document.new
|
20
|
+
|
21
|
+
# set up some default styling
|
22
|
+
pdf.font_size(8)
|
23
|
+
|
24
|
+
stamp(invoice, pdf)
|
25
|
+
company_banner(invoice, pdf)
|
26
|
+
bill_to_ship_to(invoice, pdf)
|
27
|
+
invoice_details(invoice, pdf)
|
28
|
+
line_items_table(invoice, pdf)
|
29
|
+
totals_lines(invoice, pdf)
|
30
|
+
notes(invoice, pdf)
|
31
|
+
|
32
|
+
pdf
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.stamp(invoice, pdf)
|
36
|
+
stamp = nil
|
37
|
+
if invoice.paid?
|
38
|
+
stamp = "PAID"
|
39
|
+
elsif invoice.overdue?
|
40
|
+
stamp = "OVERDUE"
|
41
|
+
end
|
42
|
+
|
43
|
+
if stamp
|
44
|
+
pdf.bounding_box([200, pdf.cursor - 50], :width => pdf.bounds.width - 400) do
|
45
|
+
pdf.font("Helvetica-Bold") do
|
46
|
+
pdf.fill_color "cc0000"
|
47
|
+
pdf.text stamp, :align=> :center, :size => 25, :rotate => 15
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
pdf.fill_color "000000"
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.company_banner(invoice, pdf)
|
56
|
+
# render the logo
|
57
|
+
logo_info = pdf.image(invoice_or_default(invoice, :invoice_logo), :at => pdf.bounds.top_left, :fit => [200, 100])
|
58
|
+
|
59
|
+
# render the company details
|
60
|
+
table_data = []
|
61
|
+
table_data << [bold_cell(pdf, invoice_or_default(invoice, :company_name), :size => 12)]
|
62
|
+
table_data << [invoice_or_default(invoice, :company_details)]
|
63
|
+
table = pdf.make_table(table_data, :cell_style => { :borders => [], :padding => [2, 0] })
|
64
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.bounds.top], :width => table.width, :height => table.height) do
|
65
|
+
table.draw
|
66
|
+
end
|
67
|
+
|
68
|
+
pdf.move_cursor_to(pdf.bounds.top - logo_info.scaled_height - 20)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.bill_to_ship_to(invoice, pdf)
|
72
|
+
bill_to_cell_style = { :borders => [], :padding => [2, 0]}
|
73
|
+
bill_to_ship_to_bottom = 0
|
74
|
+
|
75
|
+
# render bill to
|
76
|
+
pdf.float do
|
77
|
+
table = pdf.table([[bold_cell(pdf, "Bill To")], [invoice.bill_to]], :column_widths => [200], :cell_style => bill_to_cell_style)
|
78
|
+
bill_to_ship_to_bottom = pdf.cursor
|
79
|
+
end
|
80
|
+
|
81
|
+
# render ship to
|
82
|
+
if defined?(invoice.ship_to)
|
83
|
+
table = pdf.make_table([[bold_cell(pdf, "Ship To")], [invoice.ship_to]], :column_widths => [200],
|
84
|
+
:cell_style => bill_to_cell_style)
|
85
|
+
|
86
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor], :width => table.width, :height => table.height + 2) do
|
87
|
+
table.draw
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# make sure we start at the lower of the bill_to or ship_to details
|
92
|
+
bill_to_ship_to_bottom = pdf.cursor if pdf.cursor < bill_to_ship_to_bottom
|
93
|
+
pdf.move_cursor_to(bill_to_ship_to_bottom - 20)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.invoice_details(invoice, pdf)
|
97
|
+
# invoice details
|
98
|
+
table_data = []
|
99
|
+
|
100
|
+
# invoice number
|
101
|
+
if defined?(invoice.invoice_number) && invoice.invoice_number
|
102
|
+
table_data << [bold_cell(pdf, "Invoice #:"), bold_cell(pdf, invoice.invoice_number.to_s, :align => :right)]
|
103
|
+
end
|
104
|
+
|
105
|
+
# Due on
|
106
|
+
if defined?(invoice.due_on) && invoice.due_on
|
107
|
+
if invoice.due_on.is_a?(Date)
|
108
|
+
due_date = invoice.due_on.strftime(Payday::Config.default.date_format)
|
109
|
+
else
|
110
|
+
due_date = invoice.due_on.to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
table_data << [bold_cell(pdf, "Due Date:"), bold_cell(pdf, due_date, :align => :right)]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Paid on
|
117
|
+
if defined?(invoice.paid_at) && invoice.paid_at
|
118
|
+
if invoice.paid_at.is_a?(Date)
|
119
|
+
paid_date = invoice.paid_at.strftime(Payday::Config.default.date_format)
|
120
|
+
else
|
121
|
+
paid_date = invoice.paid_at.to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
table_data << [bold_cell(pdf, "Paid Date:"), bold_cell(pdf, paid_date, :align => :right)]
|
125
|
+
end
|
126
|
+
|
127
|
+
if table_data.length > 0
|
128
|
+
pdf.table(table_data, :cell_style => { :borders => [], :padding => 1 })
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.line_items_table(invoice, pdf)
|
133
|
+
table_data = []
|
134
|
+
table_data << [bold_cell(pdf, "Description", :borders => []),
|
135
|
+
bold_cell(pdf, "Unit Price", :align => :center, :borders => []),
|
136
|
+
bold_cell(pdf, "Quantity", :align => :center, :borders => []),
|
137
|
+
bold_cell(pdf, "Amount", :align => :center, :borders => [])]
|
138
|
+
invoice.line_items.each do |line|
|
139
|
+
table_data << [line.description, number_to_currency(line.price), line.quantity.to_s,
|
140
|
+
number_to_currency(line.amount)]
|
141
|
+
end
|
142
|
+
|
143
|
+
pdf.move_cursor_to(pdf.cursor - 20)
|
144
|
+
pdf.table(table_data, :width => pdf.bounds.width, :header => true, :cell_style => {:border_width => 0.5, :border_color => "cccccc", :padding => [5, 10]},
|
145
|
+
:row_colors => ["dfdfdf", "ffffff"]) do
|
146
|
+
# left align the number columns
|
147
|
+
columns(1..3).rows(1..row_length - 1).style(:align => :right)
|
148
|
+
|
149
|
+
# set the column widths correctly
|
150
|
+
natural = natural_column_widths
|
151
|
+
natural[0] = width - natural[1] - natural[2] - natural[3]
|
152
|
+
|
153
|
+
column_widths = natural
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.totals_lines(invoice, pdf)
|
158
|
+
table_data = []
|
159
|
+
table_data << [bold_cell(pdf, "Subtotal:"), cell(pdf, number_to_currency(invoice.subtotal), :align => :right)]
|
160
|
+
table_data << [bold_cell(pdf, "Tax:"), cell(pdf, number_to_currency(invoice.tax), :align => :right)]
|
161
|
+
table_data << [bold_cell(pdf, "Total:", :size => 12),
|
162
|
+
cell(pdf, number_to_currency(invoice.total), :size => 12, :align => :right)]
|
163
|
+
table = pdf.make_table(table_data, :cell_style => { :borders => [] })
|
164
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor], :width => table.width, :height => table.height + 2) do
|
165
|
+
table.draw
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.notes(invoice, pdf)
|
170
|
+
if defined?(invoice.notes) && invoice.notes
|
171
|
+
pdf.move_cursor_to(pdf.cursor - 30)
|
172
|
+
pdf.font("Helvetica-Bold") do
|
173
|
+
pdf.text("Notes")
|
174
|
+
end
|
175
|
+
pdf.line_width = 0.5
|
176
|
+
pdf.stroke_color = "cccccc"
|
177
|
+
pdf.stroke_line([0, pdf.cursor - 3, pdf.bounds.width, pdf.cursor - 3])
|
178
|
+
pdf.move_cursor_to(pdf.cursor - 10)
|
179
|
+
pdf.text(invoice.notes.to_s)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.invoice_or_default(invoice, property)
|
184
|
+
if invoice.respond_to?(property) && invoice.send(property)
|
185
|
+
invoice.send(property)
|
186
|
+
else
|
187
|
+
Payday::Config.default.send(property)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.cell(pdf, text, options = {})
|
192
|
+
Prawn::Table::Cell::Text.make(pdf, text, options)
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.bold_cell(pdf, text, options = {})
|
196
|
+
options[:font] = "Helvetica-Bold"
|
197
|
+
cell(pdf, text, options)
|
198
|
+
end
|
199
|
+
|
200
|
+
# from Rails, I think
|
201
|
+
def self.number_with_delimiter(number, delimiter=",")
|
202
|
+
number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.number_to_currency(number)
|
206
|
+
number_with_delimiter(sprintf("$%.02f", number))
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.max_cell_width(cell_proxy)
|
210
|
+
max = 0
|
211
|
+
cell_proxy.each do |cell|
|
212
|
+
if cell.natural_content_width > max
|
213
|
+
max = cell.natural_content_width
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
max
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/payday.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "payday/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "payday"
|
7
|
+
s.version = Payday::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Alan Johnson"]
|
10
|
+
s.email = ["alan@commondream.net"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{git remote add origin git@github.com:commondream/payday.git}
|
13
|
+
s.description = %q{Payday is a library for rendering invoices. At present it supports rendering invoices to pdfs, but we're planning on adding support for other formats in the near future.}
|
14
|
+
|
15
|
+
s.add_dependency("prawn", "~> 0.11.1.pre")
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "test/test_helper"
|
2
|
+
|
3
|
+
module Payday
|
4
|
+
class InvoiceTest < Test::Unit::TestCase
|
5
|
+
test "that setting values through the options hash on initialization works" do
|
6
|
+
i = Invoice.new(:invoice_number => 20, :bill_to => "Here", :ship_to => "There",
|
7
|
+
:notes => "These are some notes.",
|
8
|
+
:line_items => [LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")],
|
9
|
+
:tax_rate => 12.5, :tax_description => "Local Sales Tax, 12.5%")
|
10
|
+
|
11
|
+
assert_equal 20, i.invoice_number
|
12
|
+
assert_equal "Here", i.bill_to
|
13
|
+
assert_equal "There", i.ship_to
|
14
|
+
assert_equal "These are some notes.", i.notes
|
15
|
+
assert_equal "Shirts", i.line_items[0].description
|
16
|
+
assert_equal BigDecimal.new("12.5"), i.tax_rate
|
17
|
+
assert_equal "Local Sales Tax, 12.5%", i.tax_description
|
18
|
+
end
|
19
|
+
|
20
|
+
test "that subtotal totals up all of the line items in an invoice correctly" do
|
21
|
+
i = Invoice.new
|
22
|
+
|
23
|
+
# $100 in Pants
|
24
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
25
|
+
|
26
|
+
# $30 in Shirts
|
27
|
+
i.line_items << LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
|
28
|
+
|
29
|
+
# $1000 in Hats
|
30
|
+
i.line_items << LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
|
31
|
+
|
32
|
+
assert_equal BigDecimal.new("1130"), i.subtotal
|
33
|
+
end
|
34
|
+
|
35
|
+
test "that tax returns the correct tax amount, rounded to two decimal places" do
|
36
|
+
i = Invoice.new(:tax_rate => 0.1)
|
37
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
38
|
+
|
39
|
+
assert_equal(BigDecimal.new("10"), i.tax)
|
40
|
+
end
|
41
|
+
|
42
|
+
test "that taxes aren't applied to invoices with a subtotal of 0 or a negative amount" do
|
43
|
+
i = Invoice.new(:tax_rate => 0.1)
|
44
|
+
i.line_items << LineItem.new(:price => -1, :quantity => 100, :description => "Negative Priced Pants")
|
45
|
+
|
46
|
+
assert_equal(BigDecimal.new("0"), i.tax)
|
47
|
+
end
|
48
|
+
|
49
|
+
test "that the total for this invoice calculates correctly" do
|
50
|
+
i = Invoice.new(:tax_rate => 0.1)
|
51
|
+
|
52
|
+
# $100 in Pants
|
53
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
54
|
+
|
55
|
+
# $30 in Shirts
|
56
|
+
i.line_items << LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
|
57
|
+
|
58
|
+
# $1000 in Hats
|
59
|
+
i.line_items << LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
|
60
|
+
|
61
|
+
assert_equal BigDecimal.new("1243"), i.total
|
62
|
+
end
|
63
|
+
|
64
|
+
test "overdue? is false when past date and unpaid" do
|
65
|
+
i = Invoice.new(:due_on => Date.today - 1)
|
66
|
+
assert i.overdue?
|
67
|
+
end
|
68
|
+
|
69
|
+
test "overdue? is true when past date but paid" do
|
70
|
+
i = Invoice.new(:due_on => Date.today - 1, :paid_at => Date.today)
|
71
|
+
assert !i.overdue?
|
72
|
+
end
|
73
|
+
|
74
|
+
test "paid is false when not paid" do
|
75
|
+
i = Invoice.new
|
76
|
+
assert !i.paid?
|
77
|
+
end
|
78
|
+
|
79
|
+
test "paid is true when paid" do
|
80
|
+
i = Invoice.new(:paid_at => Date.today)
|
81
|
+
assert i.paid?
|
82
|
+
end
|
83
|
+
|
84
|
+
test "rendering to file" do
|
85
|
+
File.unlink("tmp/testing.pdf") if File.exists?("tmp/testing.pdf")
|
86
|
+
assert !File.exists?("tmp/testing.pdf")
|
87
|
+
|
88
|
+
i = Invoice.new(:tax_rate => 0.1, :notes => "These are some crazy awesome notes!", :invoice_number => 12,
|
89
|
+
:due_on => Date.civil(2011, 1, 22), :paid_at => Date.civil(2012, 2, 22),
|
90
|
+
:bill_to => "Alan Johnson\n101 This Way\nSomewhere, SC 22222", :ship_to => "Frank Johnson\n101 That Way\nOther, SC 22229")
|
91
|
+
|
92
|
+
3.times do
|
93
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
94
|
+
i.line_items << LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
|
95
|
+
i.line_items << LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
|
96
|
+
end
|
97
|
+
|
98
|
+
i.render_pdf_to_file("tmp/testing.pdf")
|
99
|
+
assert File.exists?("tmp/testing.pdf")
|
100
|
+
end
|
101
|
+
|
102
|
+
test "rendering to string" do
|
103
|
+
i = Invoice.new(:tax_rate => 0.1, :notes => "These are some crazy awesome notes!", :invoice_number => 12,
|
104
|
+
:due_on => Date.civil(2011, 1, 22), :paid_at => Date.civil(2012, 2, 22),
|
105
|
+
:bill_to => "Alan Johnson\n101 This Way\nSomewhere, SC 22222", :ship_to => "Frank Johnson\n101 That Way\nOther, SC 22229")
|
106
|
+
|
107
|
+
3.times do
|
108
|
+
i.line_items << LineItem.new(:price => 20, :quantity => 5, :description => "Pants")
|
109
|
+
i.line_items << LineItem.new(:price => 10, :quantity => 3, :description => "Shirts")
|
110
|
+
i.line_items << LineItem.new(:price => 5, :quantity => 200, :description => "Hats")
|
111
|
+
end
|
112
|
+
|
113
|
+
assert_not_nil i.render_pdf
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "test/test_helper"
|
2
|
+
|
3
|
+
module Payday
|
4
|
+
class LineItemTest < Test::Unit::TestCase
|
5
|
+
test "initializing with a price" do
|
6
|
+
li = LineItem.new(:price => BigDecimal.new("20"))
|
7
|
+
assert_equal BigDecimal.new("20"), li.price
|
8
|
+
end
|
9
|
+
|
10
|
+
test "initializing with a quantity" do
|
11
|
+
li = LineItem.new(:quantity => 30)
|
12
|
+
assert_equal BigDecimal.new("30"), li.quantity
|
13
|
+
end
|
14
|
+
|
15
|
+
test "initializing with a description" do
|
16
|
+
li = LineItem.new(:description => "12 Pairs of Pants")
|
17
|
+
assert_equal "12 Pairs of Pants", li.description
|
18
|
+
end
|
19
|
+
|
20
|
+
test "that amount returns the correct amount" do
|
21
|
+
li = LineItem.new(:price => 10, :quantity => 12)
|
22
|
+
assert_equal BigDecimal.new("120"), li.amount
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "payday")
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
# Shamelessly ripped from jm's context library: https://github.com/jm/context/blob/master/lib/context/test.rb
|
6
|
+
class Test::Unit::TestCase
|
7
|
+
class << self
|
8
|
+
# Create a test method. +name+ is a native-language string to describe the test
|
9
|
+
# (e.g., no more +test_this_crazy_thing_with_underscores+).
|
10
|
+
#
|
11
|
+
# test "A user should not be able to delete another user" do
|
12
|
+
# assert_false @user.can?(:delete, @other_user)
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
def test(name, opts={}, &block)
|
16
|
+
test_name = ["test:", name].reject { |n| n == "" }.join(' ')
|
17
|
+
# puts "running test #{test_name}"
|
18
|
+
defined = instance_method(test_name) rescue false
|
19
|
+
raise "#{test_name} is already defined in #{self}" if defined
|
20
|
+
|
21
|
+
if block_given?
|
22
|
+
define_method(test_name, &block)
|
23
|
+
else
|
24
|
+
define_method(test_name) do
|
25
|
+
flunk "No implementation provided for #{name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: payday
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 62196353
|
5
|
+
prerelease: 5
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
- beta
|
11
|
+
- 1
|
12
|
+
version: 1.0.0beta1
|
13
|
+
platform: ruby
|
14
|
+
authors:
|
15
|
+
- Alan Johnson
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2011-03-07 00:00:00 -05:00
|
21
|
+
default_executable:
|
22
|
+
dependencies:
|
23
|
+
- !ruby/object:Gem::Dependency
|
24
|
+
name: prawn
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
hash: 961915928
|
32
|
+
segments:
|
33
|
+
- 0
|
34
|
+
- 11
|
35
|
+
- 1
|
36
|
+
- pre
|
37
|
+
version: 0.11.1.pre
|
38
|
+
type: :runtime
|
39
|
+
version_requirements: *id001
|
40
|
+
description: Payday is a library for rendering invoices. At present it supports rendering invoices to pdfs, but we're planning on adding support for other formats in the near future.
|
41
|
+
email:
|
42
|
+
- alan@commondream.net
|
43
|
+
executables: []
|
44
|
+
|
45
|
+
extensions: []
|
46
|
+
|
47
|
+
extra_rdoc_files: []
|
48
|
+
|
49
|
+
files:
|
50
|
+
- .gitignore
|
51
|
+
- Gemfile
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- assets/default_logo.png
|
55
|
+
- lib/payday.rb
|
56
|
+
- lib/payday/config.rb
|
57
|
+
- lib/payday/invoice.rb
|
58
|
+
- lib/payday/invoiceable.rb
|
59
|
+
- lib/payday/line_item.rb
|
60
|
+
- lib/payday/line_itemable.rb
|
61
|
+
- lib/payday/pdf_renderer.rb
|
62
|
+
- lib/payday/version.rb
|
63
|
+
- payday.gemspec
|
64
|
+
- test/invoice_test.rb
|
65
|
+
- test/line_item_test.rb
|
66
|
+
- test/test_helper.rb
|
67
|
+
has_rdoc: true
|
68
|
+
homepage: ""
|
69
|
+
licenses: []
|
70
|
+
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 25
|
91
|
+
segments:
|
92
|
+
- 1
|
93
|
+
- 3
|
94
|
+
- 1
|
95
|
+
version: 1.3.1
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.4.2
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: git remote add origin git@github.com:commondream/payday.git
|
103
|
+
test_files:
|
104
|
+
- test/invoice_test.rb
|
105
|
+
- test/line_item_test.rb
|
106
|
+
- test/test_helper.rb
|