payday 1.0.0beta1
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.
- 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
|