Skip to content

Commit 4acb394

Browse files
4481 Print Individual Donation Receipts (#4484)
* 4481 Print Individual Donation Receipts * 4481 Print Individual Donations add zero dollars and tests * 4481 Print Individual Donation Receipts Requested Changes * 4481 Print Individual Donation PDFs hardcode all test parameters
1 parent c283168 commit 4acb394

9 files changed

Lines changed: 317 additions & 0 deletions

File tree

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ group :test do
198198
gem "webmock", "~> 3.23"
199199
# Interface capybara to chrome headless
200200
gem "cuprite"
201+
# Read PDF files for tests
202+
gem "pdf-reader"
201203
end
202204

203205
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem

Gemfile.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
GEM
22
remote: https://rubygems.org/
33
specs:
4+
Ascii85 (1.1.1)
45
actioncable (7.1.3.4)
56
actionpack (= 7.1.3.4)
67
activesupport (= 7.1.3.4)
@@ -77,6 +78,7 @@ GEM
7778
tzinfo (~> 2.0)
7879
addressable (2.8.6)
7980
public_suffix (>= 2.0.2, < 6.0)
81+
afm (0.2.2)
8082
annotate (3.2.0)
8183
activerecord (>= 3.2, < 8.0)
8284
rake (>= 10.4, < 14.0)
@@ -288,6 +290,7 @@ GEM
288290
guard-compat (~> 1.1)
289291
rspec (>= 2.99.0, < 4.0)
290292
hashdiff (1.1.0)
293+
hashery (2.1.2)
291294
hashie (5.0.0)
292295
httparty (0.22.0)
293296
csv
@@ -436,6 +439,12 @@ GEM
436439
ast (~> 2.4.1)
437440
racc
438441
pdf-core (0.9.0)
442+
pdf-reader (2.12.0)
443+
Ascii85 (~> 1.0)
444+
afm (~> 0.2.1)
445+
hashery (~> 2.0)
446+
ruby-rc4
447+
ttfunk
439448
pg (1.5.6)
440449
popper_js (2.11.8)
441450
prawn (2.4.0)
@@ -586,6 +595,7 @@ GEM
586595
ruby-graphviz (1.2.5)
587596
rexml
588597
ruby-progressbar (1.13.0)
598+
ruby-rc4 (0.1.5)
589599
ruby-vips (2.1.4)
590600
ffi (~> 1.12)
591601
ruby2_keywords (0.0.5)
@@ -750,6 +760,7 @@ DEPENDENCIES
750760
omniauth-rails_csrf_protection
751761
orderly (~> 0.1)
752762
paper_trail
763+
pdf-reader
753764
pg (~> 1.5.6)
754765
prawn-rails
755766
pry-doc

app/controllers/donations_controller.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
class DonationsController < ApplicationController
33
before_action :authorize_admin, only: [:destroy]
44

5+
def print
6+
@donation = Donation.find(params[:id])
7+
respond_to do |format|
8+
format.any do
9+
pdf = DonationPdf.new(current_organization, @donation)
10+
send_data pdf.compute_and_render,
11+
filename: format("%s %s.pdf", @donation.source, sortable_date(@donation.created_at)),
12+
type: "application/pdf",
13+
disposition: "inline"
14+
end
15+
end
16+
end
17+
518
def index
619
setup_date_range_picker
720

app/pdfs/donation_pdf.rb

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Configures a Prawn PDF template for generating Donation receipts
2+
class DonationPdf
3+
include Prawn::View
4+
include ItemsHelper
5+
6+
class DonorInfo
7+
attr_reader :name, :address, :email
8+
9+
def initialize(donation)
10+
if donation.nil?
11+
raise "Must pass a Donation object"
12+
end
13+
case donation.source
14+
when Donation::SOURCES[:donation_site]
15+
@name = donation.donation_site.name
16+
@address = donation.donation_site.address
17+
@email = donation.donation_site.email
18+
when Donation::SOURCES[:manufacturer]
19+
@name = donation.manufacturer.name
20+
@address = nil
21+
@email = nil
22+
when Donation::SOURCES[:product_drive]
23+
@name = donation.product_drive_participant.business_name
24+
@address = donation.product_drive_participant.address
25+
@email = donation.product_drive_participant.email
26+
when Donation::SOURCES[:misc]
27+
@name = "Misc. Donation"
28+
@address = nil
29+
@email = nil
30+
end
31+
end
32+
end
33+
34+
def initialize(organization, donation)
35+
@donation = Donation.includes(line_items: [:item]).find_by(id: donation.id)
36+
@organization = organization
37+
@donor = DonorInfo.new(@donation)
38+
end
39+
40+
def compute_and_render
41+
font_families["OpenSans"] = PrawnRails.config["font_families"][:OpenSans]
42+
font "OpenSans"
43+
font_size 10
44+
45+
logo_image = if @organization.logo.attached?
46+
StringIO.open(@organization.logo.download)
47+
else
48+
Organization::DIAPER_APP_LOGO
49+
end
50+
51+
footer_height = 35
52+
53+
# Bounding box containing non-footer elements
54+
bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do
55+
image logo_image, fit: [250, 85]
56+
57+
bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do
58+
text @organization.name, align: :right
59+
text @organization.address, align: :right
60+
text @organization.email, align: :right
61+
end
62+
63+
font_size 12
64+
text "Issued on:", style: :bold
65+
text @donation.issued_at.to_fs(:distribution_date)
66+
move_up 24
67+
68+
font_size 12
69+
text "Donation from:", style: :bold, align: :right
70+
font_size 10
71+
text @donor.name, align: :right
72+
text @donor.address, align: :right
73+
text @donor.email, align: :right
74+
move_down 10
75+
# Get some additional vertical distance in left column if all donor info is nil
76+
if @donor.name.nil? && @donor.address.nil? && @donor.email.nil?
77+
move_down 10
78+
end
79+
80+
font_size 12
81+
money_raised = "$0.00"
82+
if @donation.money_raised && @donation.money_raised > 0
83+
money_raised = dollar_value(@donation.money_raised)
84+
end
85+
text "<strong>Money Raised In Dollars: </strong>#{money_raised}", inline_format: true
86+
87+
move_down 10
88+
font_size 12
89+
text "Comments:", style: :bold
90+
text @donation.comment
91+
92+
move_down 20
93+
94+
data = donation_data
95+
96+
hide_columns(data)
97+
hidden_columns_length = column_names_to_hide.length
98+
99+
font_size 11
100+
101+
# Line item table
102+
table(data) do
103+
self.header = true
104+
self.cell_style = {
105+
padding: [5, 20, 5, 20]
106+
}
107+
self.row_colors = %w[dddddd ffffff]
108+
109+
cells.borders = []
110+
111+
# Header row
112+
row(0).borders = [:bottom]
113+
row(0).border_width = 2
114+
row(0).font_style = :bold
115+
row(0).size = 9
116+
row(0).column(1..-1).borders = %i[bottom left]
117+
118+
# Total Items footer row
119+
row(-1).borders = [:top]
120+
row(-1).font_style = :bold
121+
row(-1).column(1..-1).borders = %i[top left]
122+
row(-1).column(1..-1).border_left_color = "aaaaaa"
123+
124+
# Footer spacing row
125+
row(-2).borders = [:top]
126+
row(-2).padding = [2, 0, 2, 0]
127+
128+
column(0).width = 190 + (hidden_columns_length * 60)
129+
130+
# Quantity column
131+
column(1..-1).row(1..-3).borders = [:left]
132+
column(1..-1).row(1..-3).border_left_color = "aaaaaa"
133+
column(1).style align: :right
134+
end
135+
end
136+
137+
number_pages "Page <page> of <total>",
138+
start_count_at: 1,
139+
at: [bounds.right - 130, 22],
140+
align: :right
141+
142+
repeat :all do
143+
# Page footer
144+
bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do
145+
stroke_bounds
146+
font "OpenSans"
147+
font_size 9
148+
stroke_horizontal_rule
149+
move_down(5)
150+
151+
logo_offset = (bounds.width - 190) / 2
152+
bounding_box([logo_offset, 0], width: 190, height: 33) do
153+
text "Lovingly created with", valign: :center
154+
image Organization::DIAPER_APP_LOGO, width: 75, vposition: :center, position: :right
155+
end
156+
end
157+
end
158+
159+
render
160+
end
161+
162+
def donation_data
163+
data = [["Items Received",
164+
"Value/item",
165+
"In-Kind Value",
166+
"Quantity"]]
167+
data += @donation.line_items.sorted.map do |c|
168+
[c.item.name,
169+
dollar_value(c.item.value_in_cents),
170+
dollar_value(c.value_per_line_item),
171+
c.quantity]
172+
end
173+
data + [["", "", "", ""],
174+
["Total Items Received",
175+
"",
176+
dollar_value(@donation.value_per_itemizable),
177+
@donation.line_items.total]]
178+
end
179+
180+
def hide_columns(data)
181+
column_names_to_hide.each do |col_name|
182+
col_index = data.first.find_index(col_name)
183+
data.each { |line| line.delete_at(col_index) } if col_index.present?
184+
end
185+
end
186+
187+
private
188+
189+
def column_names_to_hide
190+
in_kind_column_name = "In-Kind Value"
191+
columns_to_hide = []
192+
columns_to_hide.push("Value/item", in_kind_column_name) if @organization.hide_value_columns_on_receipt
193+
columns_to_hide
194+
end
195+
end

app/views/donations/_donation_row.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<td><%= truncate donation_row.comment, length: 140, separator: /\w+/ %>
1010
<td class="text-right">
1111
<%= view_button_to donation_path(donation_row) %>
12+
<%= print_button_to print_donation_path(donation_row, format: :pdf) %>
1213
</td>
1314
</td>
1415
</td>

app/views/donations/show.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
If you need to delete this donation or make a correction, please make the following items active: <%= @donation.inactive_items.map(&:name).join(", ") %>
8888
</div>
8989
<% end %>
90+
<%= print_button_to print_donation_path(@donation, format: :pdf), { size: "md" } %>
9091
</div>
9192
</div>
9293
</div>

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def set_up_flipper
5050
resources :distributions, only: [:index] do
5151
get :print, on: :member
5252
end
53+
resources :donations, only: [:index] do
54+
get :print, on: :member
55+
end
5356
end
5457

5558
# This is where a superadmin CRUDs all the things
@@ -220,6 +223,7 @@ def set_up_flipper
220223
resources :product_drives
221224

222225
resources :donations do
226+
get :print, on: :member
223227
patch :add_item, on: :member
224228
patch :remove_item, on: :member
225229
end

spec/pdfs/donation_pdf_spec.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
describe DonationPdf do
2+
let(:donation_site) { create(:donation_site, name: "Site X", address: "1500 Remount Road, Front Royal, VA 22630", email: "test@example.com") }
3+
let(:organization) { create(:organization) }
4+
let(:donation) do
5+
create(:donation, organization: organization, donation_site: donation_site, source: Donation::SOURCES[:donation_site],
6+
comment: "A donation comment")
7+
end
8+
let(:item1) { FactoryBot.create(:item, name: "Item 1", package_size: 50, value_in_cents: 100) }
9+
let(:item2) { FactoryBot.create(:item, name: "Item 2", value_in_cents: 200) }
10+
let(:item3) { FactoryBot.create(:item, name: "Item 3", value_in_cents: 300) }
11+
let(:item4) { FactoryBot.create(:item, name: "Item 4", package_size: 25, value_in_cents: 400) }
12+
13+
let(:org_hiding_packages_and_values) do
14+
FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME,
15+
hide_value_columns_on_receipt: true, hide_package_column_on_receipt: true)
16+
end
17+
let(:org_hiding_packages) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_package_column_on_receipt: true) }
18+
let(:org_hiding_values) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_value_columns_on_receipt: true) }
19+
20+
before(:each) do
21+
create(:line_item, itemizable: donation, item: item1, quantity: 50)
22+
create(:line_item, itemizable: donation, item: item2, quantity: 100)
23+
end
24+
25+
specify "#donation_data" do
26+
results = described_class.new(organization, donation).donation_data
27+
expect(results).to eq([
28+
["Items Received", "Value/item", "In-Kind Value", "Quantity"],
29+
["Item 1", "$1.00", "$50.00", 50],
30+
["Item 2", "$2.00", "$200.00", 100],
31+
["", "", "", ""],
32+
["Total Items Received", "", "$250.00", 150]
33+
])
34+
end
35+
36+
context "with donation data" do
37+
it "hides value and package columns when true on organization" do
38+
pdf = described_class.new(org_hiding_packages_and_values, donation)
39+
data = pdf.donation_data
40+
pdf.hide_columns(data)
41+
expect(data).to eq([
42+
["Items Received", "Quantity"],
43+
["Item 1", 50],
44+
["Item 2", 100],
45+
["", ""],
46+
["Total Items Received", 150]
47+
])
48+
end
49+
end
50+
51+
context "render pdf" do
52+
it "renders correctly" do
53+
pdf = described_class.new(organization, donation)
54+
pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render))
55+
expect(pdf_test.page(1).text).to include(donation_site.name)
56+
expect(pdf_test.page(1).text).to include(donation_site.address)
57+
expect(pdf_test.page(1).text).to include(donation_site.email)
58+
if donation.comment
59+
expect(pdf_test.page(1).text).to include(donation.comment)
60+
end
61+
expect(pdf_test.page(1).text).to include("Money Raised In Dollars: $0.00")
62+
expect(pdf_test.page(1).text).to include("Items Received")
63+
expect(pdf_test.page(1).text).to match(/Item 1\s+\$1\.00\s+\$50\.00\s+50/)
64+
expect(pdf_test.page(1).text).to match(/Item 2\s+\$2\.00\s+\$200\.00\s+100/)
65+
expect(pdf_test.page(1).text).to include("Total Items Received")
66+
end
67+
end
68+
end

0 commit comments

Comments
 (0)