payday 1.1.3 → 1.1.4
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +6 -0
- data/Rakefile +6 -6
- data/lib/generators/payday/setup/setup_generator.rb +15 -15
- data/lib/payday.rb +14 -14
- data/lib/payday/config.rb +7 -8
- data/lib/payday/invoice.rb +1 -2
- data/lib/payday/invoiceable.rb +12 -14
- data/lib/payday/line_item.rb +5 -5
- data/lib/payday/line_itemable.rb +1 -1
- data/lib/payday/locale/de.yml +1 -0
- data/lib/payday/pdf_renderer.rb +241 -217
- data/lib/payday/version.rb +1 -2
- data/payday.gemspec +14 -9
- data/spec/assets/testing.pdf +0 -0
- data/spec/invoice_spec.rb +74 -58
- data/spec/line_item_spec.rb +5 -6
- data/spec/pdf_renderer_spec.rb +7 -7
- data/spec/spec_helper.rb +2 -2
- data/spec/support/asset_matchers.rb +8 -7
- metadata +15 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c63a9378b3f1a7e552dacce10b9b6261bd0eaec9
|
4
|
+
data.tar.gz: 144ae033085831b2035a185f0e3c3db491be1e19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc86262b5750704fd33c9f4aefe8aa3a396bc8da0491e1f58865525688500b70b1233c1189b181ba7d58dc1c9d2970d7ad5e821bf008d9012637d6b58469920d
|
7
|
+
data.tar.gz: a7c8d4b0bf431d03eca023028268ba7faf96a9691b1372edbbd4325ea43fdf525f90807977b23839ae114cb01bd1bc744e3e9d366a7140e566246a5d550aff34
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
data/Rakefile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require
|
1
|
+
require "bundler"
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "rake"
|
4
|
+
require "rake/testtask"
|
5
5
|
|
6
|
-
task :
|
6
|
+
task default: :test
|
7
7
|
|
8
8
|
Bundler::GemHelper.install_tasks
|
9
9
|
|
10
10
|
Rake::TestTask.new do |t|
|
11
|
-
t.test_files = FileList[
|
12
|
-
end
|
11
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
12
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/migration"
|
3
3
|
|
4
4
|
# rails g payday:setup <invoice_name> <line_item_name>
|
5
5
|
# Generates models for invoicing with Payday includes prepped
|
@@ -10,29 +10,29 @@ require 'rails/generators/migration'
|
|
10
10
|
module Payday
|
11
11
|
class SetupGenerator < Rails::Generators::Base
|
12
12
|
include Rails::Generators::Migration
|
13
|
-
|
14
|
-
source_root File.expand_path(
|
15
|
-
|
16
|
-
class_option :invoice_name, :
|
17
|
-
class_option :line_item_name, :
|
18
|
-
class_option :skip_migration, :
|
19
|
-
|
13
|
+
|
14
|
+
source_root File.expand_path("../templates", __FILE__)
|
15
|
+
|
16
|
+
class_option :invoice_name, type: :string, default: "Invoice"
|
17
|
+
class_option :line_item_name, type: :string, default: "LineItem"
|
18
|
+
class_option :skip_migration, desc: "Does not create the migration file for tables", type: :boolean
|
19
|
+
|
20
20
|
def generate_invoice_model
|
21
21
|
template "invoice.rb", "app/models/#{options.invoice_name.underscore}.rb"
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def generate_line_item_model
|
25
25
|
template "line_item.rb", "app/models/#{options.line_item_name.underscore}.rb"
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def generate_migration
|
29
29
|
unless options.skip_migration?
|
30
|
-
migration_template
|
30
|
+
migration_template "migration.rb", "db/migrate/create_payday_tables.rb"
|
31
31
|
end
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
private
|
35
|
-
|
35
|
+
|
36
36
|
def self.next_migration_number(dirname)
|
37
37
|
if ActiveRecord::Base.timestamped_migrations
|
38
38
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
@@ -41,4 +41,4 @@ module Payday
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
44
|
-
end
|
44
|
+
end
|
data/lib/payday.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
# Not much to see here
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
2
|
+
require "date"
|
3
|
+
require "time"
|
4
|
+
require "bigdecimal"
|
5
|
+
require "prawn"
|
6
|
+
require "prawn-svg"
|
7
|
+
require "money"
|
8
8
|
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
14
|
-
require_relative
|
15
|
-
require_relative
|
16
|
-
require_relative
|
9
|
+
require_relative "payday/version"
|
10
|
+
require_relative "payday/config"
|
11
|
+
require_relative "payday/i18n"
|
12
|
+
require_relative "payday/line_itemable"
|
13
|
+
require_relative "payday/line_item"
|
14
|
+
require_relative "payday/pdf_renderer"
|
15
|
+
require_relative "payday/invoiceable"
|
16
|
+
require_relative "payday/invoice"
|
data/lib/payday/config.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Payday
|
2
|
-
|
3
2
|
# Configuration for Payday. This is a singleton, so to set the company_name you would call
|
4
3
|
# Payday::Config.default.company_name = "Awesome Corp".
|
5
4
|
class Config
|
@@ -19,13 +18,13 @@ module Payday
|
|
19
18
|
#
|
20
19
|
# Primarily intended for use in our tests.
|
21
20
|
def reset
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
# TODO: Move into specs and make minimal configuration required (company name / details)
|
22
|
+
self.invoice_logo = File.join(File.dirname(__FILE__), "..", "..", "spec", "assets", "default_logo.png")
|
23
|
+
self.company_name = "Awesome Corp"
|
24
|
+
self.company_details = "awesomecorp@commondream.net"
|
25
|
+
self.date_format = "%B %e, %Y"
|
26
|
+
self.currency = "USD"
|
27
|
+
self.page_size = "LETTER"
|
29
28
|
end
|
30
29
|
|
31
30
|
# Internal: Contruct a new config object.
|
data/lib/payday/invoice.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
module Payday
|
2
|
-
|
3
2
|
# Basically just an invoice. Stick a ton of line items in it, add some details, and then render it out!
|
4
3
|
class Invoice
|
5
4
|
include Payday::Invoiceable
|
6
5
|
|
7
6
|
attr_accessor :invoice_number, :bill_to, :ship_to, :notes, :line_items, :shipping_rate, :shipping_description,
|
8
|
-
|
7
|
+
:tax_rate, :tax_description, :due_at, :paid_at, :refunded_at, :currency, :invoice_details, :invoice_date
|
9
8
|
|
10
9
|
def initialize(options = {})
|
11
10
|
self.invoice_number = options[:invoice_number] || nil
|
data/lib/payday/invoiceable.rb
CHANGED
@@ -13,17 +13,16 @@
|
|
13
13
|
# If the +due_at+, +paid_at+, and +refunded_at+ methods are available, {Payday::Invoiceable} will use them to show due dates,
|
14
14
|
# paid dates, and refunded dates, as well as stamps showing if the invoice is paid or due.
|
15
15
|
module Payday::Invoiceable
|
16
|
-
|
17
16
|
# Who the invoice is being sent to.
|
18
17
|
def bill_to
|
19
18
|
"Goofy McGoofison\nYour Invoice Doesn't\nHave It's Own BillTo Method"
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
# Calculates the subtotal of this invoice by adding up all of the line items
|
23
22
|
def subtotal
|
24
|
-
line_items.
|
23
|
+
line_items.reduce(BigDecimal.new("0")) { |result, item| result += item.amount }
|
25
24
|
end
|
26
|
-
|
25
|
+
|
27
26
|
# The tax for this invoice, as a BigDecimal
|
28
27
|
def tax
|
29
28
|
if defined?(tax_rate)
|
@@ -34,7 +33,7 @@ module Payday::Invoiceable
|
|
34
33
|
0
|
35
34
|
end
|
36
35
|
end
|
37
|
-
|
36
|
+
|
38
37
|
# TODO Add a per weight unit shipping cost
|
39
38
|
# Calculates the shipping
|
40
39
|
def shipping
|
@@ -44,42 +43,41 @@ module Payday::Invoiceable
|
|
44
43
|
0
|
45
44
|
end
|
46
45
|
end
|
47
|
-
|
46
|
+
|
48
47
|
# Calculates the total for this invoice.
|
49
48
|
def total
|
50
49
|
subtotal + tax + shipping
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
def overdue?
|
54
53
|
defined?(:due_at) && ((due_at.is_a?(Date) && due_at < Date.today) || (due_at.is_a?(Time) && due_at < Time.now)) && !paid_at
|
55
54
|
end
|
56
|
-
|
55
|
+
|
57
56
|
def refunded?
|
58
57
|
defined?(:refunded_at) && !!refunded_at
|
59
58
|
end
|
60
|
-
|
59
|
+
|
61
60
|
def paid?
|
62
61
|
defined?(:paid_at) && !!paid_at
|
63
62
|
end
|
64
|
-
|
63
|
+
|
65
64
|
# Renders this invoice to pdf as a string
|
66
65
|
def render_pdf
|
67
66
|
Payday::PdfRenderer.render(self)
|
68
67
|
end
|
69
|
-
|
68
|
+
|
70
69
|
# Renders this invoice to pdf
|
71
70
|
def render_pdf_to_file(path)
|
72
71
|
Payday::PdfRenderer.render_to_file(self, path)
|
73
72
|
end
|
74
|
-
|
73
|
+
|
75
74
|
# Iterates through the details on this invoiceable. The block given should accept
|
76
75
|
# two parameters, the detail name and the actual detail value.
|
77
76
|
def each_detail(&block)
|
78
77
|
return if defined?(invoice_details).nil?
|
79
|
-
|
78
|
+
|
80
79
|
invoice_details.each do |detail|
|
81
80
|
block.call(detail[0], detail[1])
|
82
81
|
end
|
83
82
|
end
|
84
83
|
end
|
85
|
-
|
data/lib/payday/line_item.rb
CHANGED
@@ -6,9 +6,9 @@ module Payday
|
|
6
6
|
# Otherwise, we'll do our best to convert the set values to a +BigDecimal+.
|
7
7
|
class LineItem
|
8
8
|
include LineItemable
|
9
|
-
|
9
|
+
|
10
10
|
attr_accessor :description, :quantity, :display_quantity, :display_price, :price
|
11
|
-
|
11
|
+
|
12
12
|
# Initializes a new LineItem
|
13
13
|
def initialize(options = {})
|
14
14
|
self.quantity = options[:quantity] || "1"
|
@@ -17,15 +17,15 @@ module Payday
|
|
17
17
|
self.price = options[:price] || "0.00"
|
18
18
|
self.description = options[:description] || ""
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
# Sets the quantity of this {LineItem}
|
22
22
|
def quantity=(value)
|
23
23
|
@quantity = BigDecimal.new(value.to_s)
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# Sets the price for this {LineItem}
|
27
27
|
def price=(value)
|
28
28
|
@price = BigDecimal.new(value.to_s)
|
29
29
|
end
|
30
30
|
end
|
31
|
-
end
|
31
|
+
end
|
data/lib/payday/line_itemable.rb
CHANGED
data/lib/payday/locale/de.yml
CHANGED
data/lib/payday/pdf_renderer.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
module Payday
|
2
|
-
|
3
2
|
# The PDF renderer. We use this internally in Payday to render pdfs, but really you should just need to call
|
4
3
|
# {{Payday::Invoiceable#render_pdf}} to render pdfs yourself.
|
5
4
|
class PdfRenderer
|
6
|
-
|
7
5
|
# Renders the given invoice as a pdf on disk
|
8
6
|
def self.render_to_file(invoice, path)
|
9
7
|
pdf(invoice).render_file(path)
|
@@ -15,280 +13,306 @@ module Payday
|
|
15
13
|
end
|
16
14
|
|
17
15
|
private
|
18
|
-
def self.pdf(invoice)
|
19
|
-
pdf = Prawn::Document.new(:page_size => invoice_or_default(invoice, :page_size))
|
20
16
|
|
21
|
-
|
22
|
-
|
17
|
+
def self.pdf(invoice)
|
18
|
+
pdf = Prawn::Document.new(page_size: invoice_or_default(invoice, :page_size))
|
23
19
|
|
24
|
-
|
25
|
-
|
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)
|
20
|
+
# set up some default styling
|
21
|
+
pdf.font_size(8)
|
31
22
|
|
32
|
-
|
23
|
+
stamp(invoice, pdf)
|
24
|
+
company_banner(invoice, pdf)
|
25
|
+
bill_to_ship_to(invoice, pdf)
|
26
|
+
invoice_details(invoice, pdf)
|
27
|
+
line_items_table(invoice, pdf)
|
28
|
+
totals_lines(invoice, pdf)
|
29
|
+
notes(invoice, pdf)
|
33
30
|
|
34
|
-
|
35
|
-
end
|
31
|
+
page_numbers(pdf)
|
36
32
|
|
37
|
-
|
38
|
-
|
39
|
-
if invoice.refunded?
|
40
|
-
stamp = I18n.t 'payday.status.refunded', :default => "REFUNDED"
|
41
|
-
elsif invoice.paid?
|
42
|
-
stamp = I18n.t 'payday.status.paid', :default => "PAID"
|
43
|
-
elsif invoice.overdue?
|
44
|
-
stamp = I18n.t 'payday.status.overdue', :default => "OVERDUE"
|
45
|
-
end
|
33
|
+
pdf
|
34
|
+
end
|
46
35
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
36
|
+
def self.stamp(invoice, pdf)
|
37
|
+
stamp = nil
|
38
|
+
if invoice.refunded?
|
39
|
+
stamp = I18n.t "payday.status.refunded", default: "REFUNDED"
|
40
|
+
elsif invoice.paid?
|
41
|
+
stamp = I18n.t "payday.status.paid", default: "PAID"
|
42
|
+
elsif invoice.overdue?
|
43
|
+
stamp = I18n.t "payday.status.overdue", default: "OVERDUE"
|
44
|
+
end
|
45
|
+
|
46
|
+
if stamp
|
47
|
+
pdf.bounding_box([150, pdf.cursor - 50], width: pdf.bounds.width - 300) do
|
48
|
+
pdf.font("Helvetica-Bold") do
|
49
|
+
pdf.fill_color "cc0000"
|
50
|
+
pdf.text stamp, align: :center, size: 25, rotate: 15
|
53
51
|
end
|
54
52
|
end
|
55
|
-
|
56
|
-
pdf.fill_color "000000"
|
57
53
|
end
|
58
54
|
|
59
|
-
|
60
|
-
|
61
|
-
image = invoice_or_default(invoice, :invoice_logo)
|
62
|
-
height = nil
|
63
|
-
width = nil
|
64
|
-
|
65
|
-
# Handle images defined with a hash of options
|
66
|
-
if image.is_a?(Hash)
|
67
|
-
data = image
|
68
|
-
image = data[:filename]
|
69
|
-
width, height = data[:size].split("x").map(&:to_f)
|
70
|
-
end
|
55
|
+
pdf.fill_color "000000"
|
56
|
+
end
|
71
57
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
58
|
+
def self.company_banner(invoice, pdf)
|
59
|
+
# render the logo
|
60
|
+
image = invoice_or_default(invoice, :invoice_logo)
|
61
|
+
height = nil
|
62
|
+
width = nil
|
63
|
+
|
64
|
+
# Handle images defined with a hash of options
|
65
|
+
if image.is_a?(Hash)
|
66
|
+
data = image
|
67
|
+
image = data[:filename]
|
68
|
+
width, height = data[:size].split("x").map(&:to_f)
|
69
|
+
end
|
79
70
|
|
80
|
-
|
81
|
-
|
82
|
-
|
71
|
+
if File.extname(image) == ".svg"
|
72
|
+
logo_info = pdf.svg(File.read(image), at: pdf.bounds.top_left, width: width, height: height)
|
73
|
+
logo_height = logo_info[:height]
|
74
|
+
else
|
75
|
+
logo_info = pdf.image(image, at: pdf.bounds.top_left, width: width, height: height)
|
76
|
+
logo_height = logo_info.scaled_height
|
77
|
+
end
|
83
78
|
|
84
|
-
|
79
|
+
# render the company details
|
80
|
+
table_data = []
|
81
|
+
table_data << [bold_cell(pdf, invoice_or_default(invoice, :company_name).strip, size: 12)]
|
85
82
|
|
86
|
-
|
87
|
-
pdf.bounding_box([pdf.bounds.width - table.width, pdf.bounds.top], :width => table.width, :height => table.height + 5) do
|
88
|
-
table.draw
|
89
|
-
end
|
83
|
+
invoice_or_default(invoice, :company_details).lines.each { |line| table_data << [line] }
|
90
84
|
|
91
|
-
|
85
|
+
table = pdf.make_table(table_data, cell_style: { borders: [], padding: 0 })
|
86
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.bounds.top], width: table.width, height: table.height + 5) do
|
87
|
+
table.draw
|
92
88
|
end
|
93
89
|
|
94
|
-
|
95
|
-
|
96
|
-
bill_to_ship_to_bottom = 0
|
97
|
-
|
98
|
-
# render bill to
|
99
|
-
pdf.float do
|
100
|
-
table = pdf.table([[bold_cell(pdf, I18n.t('payday.invoice.bill_to', :default => "Bill To"))],
|
101
|
-
[invoice.bill_to]], :column_widths => [200], :cell_style => bill_to_cell_style)
|
102
|
-
bill_to_ship_to_bottom = pdf.cursor
|
103
|
-
end
|
104
|
-
|
105
|
-
# render ship to
|
106
|
-
if defined?(invoice.ship_to) && !invoice.ship_to.nil?
|
107
|
-
table = pdf.make_table([[bold_cell(pdf, I18n.t('payday.invoice.ship_to', :default => "Ship To"))],
|
108
|
-
[invoice.ship_to]], :column_widths => [200], :cell_style => bill_to_cell_style)
|
90
|
+
pdf.move_cursor_to(pdf.bounds.top - logo_height - 20)
|
91
|
+
end
|
109
92
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
end
|
93
|
+
def self.bill_to_ship_to(invoice, pdf)
|
94
|
+
bill_to_cell_style = { borders: [], padding: [2, 0] }
|
95
|
+
bill_to_ship_to_bottom = 0
|
114
96
|
|
115
|
-
|
116
|
-
|
117
|
-
pdf.
|
97
|
+
# render bill to
|
98
|
+
pdf.float do
|
99
|
+
table = pdf.table([[bold_cell(pdf, I18n.t("payday.invoice.bill_to", default: "Bill To"))],
|
100
|
+
[invoice.bill_to]], column_widths: [200], cell_style: bill_to_cell_style)
|
101
|
+
bill_to_ship_to_bottom = pdf.cursor
|
118
102
|
end
|
119
103
|
|
120
|
-
|
121
|
-
|
122
|
-
|
104
|
+
# render ship to
|
105
|
+
if defined?(invoice.ship_to) && !invoice.ship_to.nil?
|
106
|
+
table = pdf.make_table([[bold_cell(pdf, I18n.t("payday.invoice.ship_to", default: "Ship To"))],
|
107
|
+
[invoice.ship_to]], column_widths: [200], cell_style: bill_to_cell_style)
|
123
108
|
|
124
|
-
|
125
|
-
|
126
|
-
table_data << [bold_cell(pdf, I18n.t('payday.invoice.invoice_no', :default => "Invoice #:")),
|
127
|
-
bold_cell(pdf, invoice.invoice_number.to_s, :align => :right)]
|
128
|
-
end
|
129
|
-
|
130
|
-
# invoice date
|
131
|
-
if defined?(invoice.invoice_date) && invoice.invoice_date
|
132
|
-
if invoice.invoice_date.is_a?(Date) || invoice.invoice_date.is_a?(Time)
|
133
|
-
invoice_date = invoice.invoice_date.strftime(Payday::Config.default.date_format)
|
134
|
-
else
|
135
|
-
invoice_date = invoice.invoice_date.to_s
|
136
|
-
end
|
137
|
-
|
138
|
-
table_data << [bold_cell(pdf, I18n.t('payday.invoice.invoice_date', :default => "Invoice Date:")),
|
139
|
-
bold_cell(pdf, invoice_date, :align => :right)]
|
109
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor], width: table.width, height: table.height + 2) do
|
110
|
+
table.draw
|
140
111
|
end
|
112
|
+
end
|
141
113
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
else
|
147
|
-
due_date = invoice.due_at.to_s
|
148
|
-
end
|
114
|
+
# make sure we start at the lower of the bill_to or ship_to details
|
115
|
+
bill_to_ship_to_bottom = pdf.cursor if pdf.cursor < bill_to_ship_to_bottom
|
116
|
+
pdf.move_cursor_to(bill_to_ship_to_bottom - 20)
|
117
|
+
end
|
149
118
|
|
150
|
-
|
151
|
-
|
152
|
-
|
119
|
+
def self.invoice_details(invoice, pdf)
|
120
|
+
# invoice details
|
121
|
+
table_data = []
|
153
122
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
paid_date = invoice.paid_at.to_s
|
160
|
-
end
|
123
|
+
# invoice number
|
124
|
+
if defined?(invoice.invoice_number) && invoice.invoice_number
|
125
|
+
table_data << [bold_cell(pdf, I18n.t("payday.invoice.invoice_no", default: "Invoice #:")),
|
126
|
+
bold_cell(pdf, invoice.invoice_number.to_s, align: :right)]
|
127
|
+
end
|
161
128
|
|
162
|
-
|
163
|
-
|
129
|
+
# invoice date
|
130
|
+
if defined?(invoice.invoice_date) && invoice.invoice_date
|
131
|
+
if invoice.invoice_date.is_a?(Date) || invoice.invoice_date.is_a?(Time)
|
132
|
+
invoice_date = invoice.invoice_date.strftime(Payday::Config.default.date_format)
|
133
|
+
else
|
134
|
+
invoice_date = invoice.invoice_date.to_s
|
164
135
|
end
|
165
136
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
refunded_date = invoice.refunded_at.strftime(Payday::Config.default.date_format)
|
170
|
-
else
|
171
|
-
refunded_date = invoice.refunded_at.to_s
|
172
|
-
end
|
137
|
+
table_data << [bold_cell(pdf, I18n.t("payday.invoice.invoice_date", default: "Invoice Date:")),
|
138
|
+
bold_cell(pdf, invoice_date, align: :right)]
|
139
|
+
end
|
173
140
|
|
174
|
-
|
175
|
-
|
141
|
+
# Due on
|
142
|
+
if defined?(invoice.due_at) && invoice.due_at
|
143
|
+
if invoice.due_at.is_a?(Date) || invoice.due_at.is_a?(Time)
|
144
|
+
due_date = invoice.due_at.strftime(Payday::Config.default.date_format)
|
145
|
+
else
|
146
|
+
due_date = invoice.due_at.to_s
|
176
147
|
end
|
177
148
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
bold_cell(pdf, value, :align => :right)]
|
182
|
-
end
|
149
|
+
table_data << [bold_cell(pdf, I18n.t("payday.invoice.due_date", default: "Due Date:")),
|
150
|
+
bold_cell(pdf, due_date, align: :right)]
|
151
|
+
end
|
183
152
|
|
184
|
-
|
185
|
-
|
153
|
+
# Paid on
|
154
|
+
if defined?(invoice.paid_at) && invoice.paid_at
|
155
|
+
if invoice.paid_at.is_a?(Date) || invoice.paid_at.is_a?(Time)
|
156
|
+
paid_date = invoice.paid_at.strftime(Payday::Config.default.date_format)
|
157
|
+
else
|
158
|
+
paid_date = invoice.paid_at.to_s
|
186
159
|
end
|
160
|
+
|
161
|
+
table_data << [bold_cell(pdf, I18n.t("payday.invoice.paid_date", default: "Paid Date:")),
|
162
|
+
bold_cell(pdf, paid_date, align: :right)]
|
187
163
|
end
|
188
164
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
invoice.line_items.each do |line|
|
196
|
-
table_data << [line.description,
|
197
|
-
(line.display_price || number_to_currency(line.price, invoice)),
|
198
|
-
(line.display_quantity || BigDecimal.new(line.quantity.to_s).to_s("F")),
|
199
|
-
number_to_currency(line.amount, invoice)]
|
165
|
+
# Refunded on
|
166
|
+
if defined?(invoice.refunded_at) && invoice.refunded_at
|
167
|
+
if invoice.refunded_at.is_a?(Date) || invoice.refunded_at.is_a?(Time)
|
168
|
+
refunded_date = invoice.refunded_at.strftime(Payday::Config.default.date_format)
|
169
|
+
else
|
170
|
+
refunded_date = invoice.refunded_at.to_s
|
200
171
|
end
|
201
172
|
|
202
|
-
pdf.
|
203
|
-
|
204
|
-
|
205
|
-
:row_colors => ["dfdfdf", "ffffff"]) do
|
206
|
-
# left align the number columns
|
207
|
-
columns(1..3).rows(1..row_length - 1).style(:align => :right)
|
173
|
+
table_data << [bold_cell(pdf, I18n.t("payday.invoice.refunded_date", default: "Refunded Date:")),
|
174
|
+
bold_cell(pdf, refunded_date, align: :right)]
|
175
|
+
end
|
208
176
|
|
209
|
-
|
210
|
-
|
211
|
-
|
177
|
+
# loop through invoice_details and include them
|
178
|
+
invoice.each_detail do |key, value|
|
179
|
+
table_data << [bold_cell(pdf, key),
|
180
|
+
bold_cell(pdf, value, align: :right)]
|
181
|
+
end
|
212
182
|
|
213
|
-
|
214
|
-
|
183
|
+
if table_data.length > 0
|
184
|
+
pdf.table(table_data, cell_style: { borders: [], padding: [1, 10, 1, 1] })
|
215
185
|
end
|
186
|
+
end
|
216
187
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
invoice.shipping_description.nil? ? I18n.t('payday.invoice.shipping', :default => "Shipping:") : invoice.shipping_description),
|
229
|
-
cell(pdf, number_to_currency(invoice.shipping, invoice), :align => :right)]
|
230
|
-
end
|
231
|
-
table_data << [bold_cell(pdf, I18n.t('payday.invoice.total', :default => "Total:"), :size => 12),
|
232
|
-
cell(pdf, number_to_currency(invoice.total, invoice), :size => 12, :align => :right)]
|
233
|
-
table = pdf.make_table(table_data, :cell_style => { :borders => [] })
|
234
|
-
pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor], :width => table.width, :height => table.height + 2) do
|
235
|
-
table.draw
|
236
|
-
end
|
188
|
+
def self.line_items_table(invoice, pdf)
|
189
|
+
table_data = []
|
190
|
+
table_data << [bold_cell(pdf, I18n.t("payday.line_item.description", default: "Description"), borders: []),
|
191
|
+
bold_cell(pdf, I18n.t("payday.line_item.unit_price", default: "Unit Price"), align: :center, borders: []),
|
192
|
+
bold_cell(pdf, I18n.t("payday.line_item.quantity", default: "Quantity"), align: :center, borders: []),
|
193
|
+
bold_cell(pdf, I18n.t("payday.line_item.amount", default: "Amount"), align: :center, borders: [])]
|
194
|
+
invoice.line_items.each do |line|
|
195
|
+
table_data << [line.description,
|
196
|
+
(line.display_price || number_to_currency(line.price, invoice)),
|
197
|
+
(line.display_quantity || BigDecimal.new(line.quantity.to_s).to_s("F")),
|
198
|
+
number_to_currency(line.amount, invoice)]
|
237
199
|
end
|
238
200
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
201
|
+
pdf.move_cursor_to(pdf.cursor - 20)
|
202
|
+
pdf.table(table_data, width: pdf.bounds.width, header: true,
|
203
|
+
cell_style: { border_width: 0.5, border_color: "cccccc",
|
204
|
+
padding: [5, 10] },
|
205
|
+
row_colors: %w(dfdfdf ffffff)) do
|
206
|
+
|
207
|
+
# left align the number columns
|
208
|
+
columns(1..3).rows(1..row_length - 1).style(align: :right)
|
209
|
+
|
210
|
+
# set the column widths correctly
|
211
|
+
natural = natural_column_widths
|
212
|
+
natural[0] = width - natural[1] - natural[2] - natural[3]
|
213
|
+
|
214
|
+
column_widths = natural
|
251
215
|
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.totals_lines(invoice, pdf)
|
219
|
+
table_data = []
|
220
|
+
table_data << [
|
221
|
+
bold_cell(pdf, I18n.t("payday.invoice.subtotal", default: "Subtotal:")),
|
222
|
+
cell(pdf, number_to_currency(invoice.subtotal, invoice), align: :right)
|
223
|
+
]
|
252
224
|
|
253
|
-
|
254
|
-
if
|
255
|
-
|
225
|
+
if invoice.tax_rate > 0
|
226
|
+
if invoice.tax_description.nil?
|
227
|
+
tax_description = I18n.t("payday.invoice.tax", default: "Tax:")
|
228
|
+
else
|
229
|
+
tax_description = invoice.tax_description
|
256
230
|
end
|
257
|
-
end
|
258
231
|
|
259
|
-
|
260
|
-
|
261
|
-
invoice.
|
232
|
+
table_data << [
|
233
|
+
bold_cell(pdf, tax_description),
|
234
|
+
cell(pdf, number_to_currency(invoice.tax, invoice), align: :right)
|
235
|
+
]
|
236
|
+
end
|
237
|
+
if invoice.shipping_rate > 0
|
238
|
+
if invoice.shipping_description.nil?
|
239
|
+
shipping_description =
|
240
|
+
I18n.t("payday.invoice.shipping", default: "Shipping:")
|
262
241
|
else
|
263
|
-
|
242
|
+
shipping_description = invoice.shipping_description
|
264
243
|
end
|
244
|
+
|
245
|
+
table_data << [
|
246
|
+
bold_cell(pdf, shipping_description),
|
247
|
+
cell(pdf, number_to_currency(invoice.shipping, invoice),
|
248
|
+
align: :right)
|
249
|
+
]
|
250
|
+
end
|
251
|
+
table_data << [
|
252
|
+
bold_cell(pdf, I18n.t("payday.invoice.total", default: "Total:"),
|
253
|
+
size: 12),
|
254
|
+
cell(pdf, number_to_currency(invoice.total, invoice),
|
255
|
+
size: 12, align: :right)
|
256
|
+
]
|
257
|
+
table = pdf.make_table(table_data, cell_style: { borders: [] })
|
258
|
+
pdf.bounding_box([pdf.bounds.width - table.width, pdf.cursor],
|
259
|
+
width: table.width, height: table.height + 2) do
|
260
|
+
|
261
|
+
table.draw
|
265
262
|
end
|
263
|
+
end
|
266
264
|
|
267
|
-
|
268
|
-
|
265
|
+
def self.notes(invoice, pdf)
|
266
|
+
if defined?(invoice.notes) && invoice.notes
|
267
|
+
pdf.move_cursor_to(pdf.cursor - 30)
|
268
|
+
pdf.font("Helvetica-Bold") do
|
269
|
+
pdf.text(I18n.t("payday.invoice.notes", default: "Notes"))
|
270
|
+
end
|
271
|
+
pdf.line_width = 0.5
|
272
|
+
pdf.stroke_color = "cccccc"
|
273
|
+
pdf.stroke_line([0, pdf.cursor - 3, pdf.bounds.width, pdf.cursor - 3])
|
274
|
+
pdf.move_cursor_to(pdf.cursor - 10)
|
275
|
+
pdf.text(invoice.notes.to_s)
|
269
276
|
end
|
277
|
+
end
|
270
278
|
|
271
|
-
|
272
|
-
|
279
|
+
def self.page_numbers(pdf)
|
280
|
+
if pdf.page_count > 1
|
281
|
+
pdf.number_pages("<page> / <total>", at: [pdf.bounds.right - 18, -15])
|
273
282
|
end
|
283
|
+
end
|
274
284
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
Money.new(number, currency).format
|
285
|
+
def self.invoice_or_default(invoice, property)
|
286
|
+
if invoice.respond_to?(property) && invoice.send(property)
|
287
|
+
invoice.send(property)
|
288
|
+
else
|
289
|
+
Payday::Config.default.send(property)
|
281
290
|
end
|
291
|
+
end
|
282
292
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
293
|
+
def self.cell(pdf, text, options = {})
|
294
|
+
Prawn::Table::Cell::Text.make(pdf, text, options)
|
295
|
+
end
|
296
|
+
|
297
|
+
def self.bold_cell(pdf, text, options = {})
|
298
|
+
cell(pdf, "<b>#{text}</b>", options.merge(inline_format: true))
|
299
|
+
end
|
290
300
|
|
291
|
-
|
301
|
+
# Converts this number to a formatted currency string
|
302
|
+
def self.number_to_currency(number, invoice)
|
303
|
+
currency = Money::Currency.wrap(invoice_or_default(invoice, :currency))
|
304
|
+
number *= currency.subunit_to_unit
|
305
|
+
number = number.round unless Money.infinite_precision
|
306
|
+
Money.new(number, currency).format
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.max_cell_width(cell_proxy)
|
310
|
+
max = 0
|
311
|
+
cell_proxy.each do |cell|
|
312
|
+
max = cell.natural_content_width if cell.natural_content_width > max
|
292
313
|
end
|
314
|
+
|
315
|
+
max
|
316
|
+
end
|
293
317
|
end
|
294
318
|
end
|