Skip to content

Commit

Permalink
4481 Print Individual Donation Receipts (#4484)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mdphillips375 committed Jul 19, 2024
1 parent c283168 commit 4acb394
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ group :test do
gem "webmock", "~> 3.23"
# Interface capybara to chrome headless
gem "cuprite"
# Read PDF files for tests
gem "pdf-reader"
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
Expand Down
11 changes: 11 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.1)
actioncable (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
Expand Down Expand Up @@ -77,6 +78,7 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
Expand Down Expand Up @@ -288,6 +290,7 @@ GEM
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
hashdiff (1.1.0)
hashery (2.1.2)
hashie (5.0.0)
httparty (0.22.0)
csv
Expand Down Expand Up @@ -436,6 +439,12 @@ GEM
ast (~> 2.4.1)
racc
pdf-core (0.9.0)
pdf-reader (2.12.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.6)
popper_js (2.11.8)
prawn (2.4.0)
Expand Down Expand Up @@ -586,6 +595,7 @@ GEM
ruby-graphviz (1.2.5)
rexml
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
Expand Down Expand Up @@ -750,6 +760,7 @@ DEPENDENCIES
omniauth-rails_csrf_protection
orderly (~> 0.1)
paper_trail
pdf-reader
pg (~> 1.5.6)
prawn-rails
pry-doc
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/donations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
class DonationsController < ApplicationController
before_action :authorize_admin, only: [:destroy]

def print
@donation = Donation.find(params[:id])
respond_to do |format|
format.any do
pdf = DonationPdf.new(current_organization, @donation)
send_data pdf.compute_and_render,
filename: format("%s %s.pdf", @donation.source, sortable_date(@donation.created_at)),
type: "application/pdf",
disposition: "inline"
end
end
end

def index
setup_date_range_picker

Expand Down
195 changes: 195 additions & 0 deletions app/pdfs/donation_pdf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Configures a Prawn PDF template for generating Donation receipts
class DonationPdf
include Prawn::View
include ItemsHelper

class DonorInfo
attr_reader :name, :address, :email

def initialize(donation)
if donation.nil?
raise "Must pass a Donation object"
end
case donation.source
when Donation::SOURCES[:donation_site]
@name = donation.donation_site.name
@address = donation.donation_site.address
@email = donation.donation_site.email
when Donation::SOURCES[:manufacturer]
@name = donation.manufacturer.name
@address = nil
@email = nil
when Donation::SOURCES[:product_drive]
@name = donation.product_drive_participant.business_name
@address = donation.product_drive_participant.address
@email = donation.product_drive_participant.email
when Donation::SOURCES[:misc]
@name = "Misc. Donation"
@address = nil
@email = nil
end
end
end

def initialize(organization, donation)
@donation = Donation.includes(line_items: [:item]).find_by(id: donation.id)
@organization = organization
@donor = DonorInfo.new(@donation)
end

def compute_and_render
font_families["OpenSans"] = PrawnRails.config["font_families"][:OpenSans]
font "OpenSans"
font_size 10

logo_image = if @organization.logo.attached?
StringIO.open(@organization.logo.download)
else
Organization::DIAPER_APP_LOGO
end

footer_height = 35

# Bounding box containing non-footer elements
bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do
image logo_image, fit: [250, 85]

bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do
text @organization.name, align: :right
text @organization.address, align: :right
text @organization.email, align: :right
end

font_size 12
text "Issued on:", style: :bold
text @donation.issued_at.to_fs(:distribution_date)
move_up 24

font_size 12
text "Donation from:", style: :bold, align: :right
font_size 10
text @donor.name, align: :right
text @donor.address, align: :right
text @donor.email, align: :right
move_down 10
# Get some additional vertical distance in left column if all donor info is nil
if @donor.name.nil? && @donor.address.nil? && @donor.email.nil?
move_down 10
end

font_size 12
money_raised = "$0.00"
if @donation.money_raised && @donation.money_raised > 0
money_raised = dollar_value(@donation.money_raised)
end
text "<strong>Money Raised In Dollars: </strong>#{money_raised}", inline_format: true

move_down 10
font_size 12
text "Comments:", style: :bold
text @donation.comment

move_down 20

data = donation_data

hide_columns(data)
hidden_columns_length = column_names_to_hide.length

font_size 11

# Line item table
table(data) do
self.header = true
self.cell_style = {
padding: [5, 20, 5, 20]
}
self.row_colors = %w[dddddd ffffff]

cells.borders = []

# Header row
row(0).borders = [:bottom]
row(0).border_width = 2
row(0).font_style = :bold
row(0).size = 9
row(0).column(1..-1).borders = %i[bottom left]

# Total Items footer row
row(-1).borders = [:top]
row(-1).font_style = :bold
row(-1).column(1..-1).borders = %i[top left]
row(-1).column(1..-1).border_left_color = "aaaaaa"

# Footer spacing row
row(-2).borders = [:top]
row(-2).padding = [2, 0, 2, 0]

column(0).width = 190 + (hidden_columns_length * 60)

# Quantity column
column(1..-1).row(1..-3).borders = [:left]
column(1..-1).row(1..-3).border_left_color = "aaaaaa"
column(1).style align: :right
end
end

number_pages "Page <page> of <total>",
start_count_at: 1,
at: [bounds.right - 130, 22],
align: :right

repeat :all do
# Page footer
bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do
stroke_bounds
font "OpenSans"
font_size 9
stroke_horizontal_rule
move_down(5)

logo_offset = (bounds.width - 190) / 2
bounding_box([logo_offset, 0], width: 190, height: 33) do
text "Lovingly created with", valign: :center
image Organization::DIAPER_APP_LOGO, width: 75, vposition: :center, position: :right
end
end
end

render
end

def donation_data
data = [["Items Received",
"Value/item",
"In-Kind Value",
"Quantity"]]
data += @donation.line_items.sorted.map do |c|
[c.item.name,
dollar_value(c.item.value_in_cents),
dollar_value(c.value_per_line_item),
c.quantity]
end
data + [["", "", "", ""],
["Total Items Received",
"",
dollar_value(@donation.value_per_itemizable),
@donation.line_items.total]]
end

def hide_columns(data)
column_names_to_hide.each do |col_name|
col_index = data.first.find_index(col_name)
data.each { |line| line.delete_at(col_index) } if col_index.present?
end
end

private

def column_names_to_hide
in_kind_column_name = "In-Kind Value"
columns_to_hide = []
columns_to_hide.push("Value/item", in_kind_column_name) if @organization.hide_value_columns_on_receipt
columns_to_hide
end
end
1 change: 1 addition & 0 deletions app/views/donations/_donation_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<td><%= truncate donation_row.comment, length: 140, separator: /\w+/ %>
<td class="text-right">
<%= view_button_to donation_path(donation_row) %>
<%= print_button_to print_donation_path(donation_row, format: :pdf) %>
</td>
</td>
</td>
Expand Down
1 change: 1 addition & 0 deletions app/views/donations/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
If you need to delete this donation or make a correction, please make the following items active: <%= @donation.inactive_items.map(&:name).join(", ") %>
</div>
<% end %>
<%= print_button_to print_donation_path(@donation, format: :pdf), { size: "md" } %>
</div>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def set_up_flipper
resources :distributions, only: [:index] do
get :print, on: :member
end
resources :donations, only: [:index] do
get :print, on: :member
end
end

# This is where a superadmin CRUDs all the things
Expand Down Expand Up @@ -220,6 +223,7 @@ def set_up_flipper
resources :product_drives

resources :donations do
get :print, on: :member
patch :add_item, on: :member
patch :remove_item, on: :member
end
Expand Down
68 changes: 68 additions & 0 deletions spec/pdfs/donation_pdf_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
describe DonationPdf do
let(:donation_site) { create(:donation_site, name: "Site X", address: "1500 Remount Road, Front Royal, VA 22630", email: "test@example.com") }
let(:organization) { create(:organization) }
let(:donation) do
create(:donation, organization: organization, donation_site: donation_site, source: Donation::SOURCES[:donation_site],
comment: "A donation comment")
end
let(:item1) { FactoryBot.create(:item, name: "Item 1", package_size: 50, value_in_cents: 100) }
let(:item2) { FactoryBot.create(:item, name: "Item 2", value_in_cents: 200) }
let(:item3) { FactoryBot.create(:item, name: "Item 3", value_in_cents: 300) }
let(:item4) { FactoryBot.create(:item, name: "Item 4", package_size: 25, value_in_cents: 400) }

let(:org_hiding_packages_and_values) do
FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME,
hide_value_columns_on_receipt: true, hide_package_column_on_receipt: true)
end
let(:org_hiding_packages) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_package_column_on_receipt: true) }
let(:org_hiding_values) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_value_columns_on_receipt: true) }

before(:each) do
create(:line_item, itemizable: donation, item: item1, quantity: 50)
create(:line_item, itemizable: donation, item: item2, quantity: 100)
end

specify "#donation_data" do
results = described_class.new(organization, donation).donation_data
expect(results).to eq([
["Items Received", "Value/item", "In-Kind Value", "Quantity"],
["Item 1", "$1.00", "$50.00", 50],
["Item 2", "$2.00", "$200.00", 100],
["", "", "", ""],
["Total Items Received", "", "$250.00", 150]
])
end

context "with donation data" do
it "hides value and package columns when true on organization" do
pdf = described_class.new(org_hiding_packages_and_values, donation)
data = pdf.donation_data
pdf.hide_columns(data)
expect(data).to eq([
["Items Received", "Quantity"],
["Item 1", 50],
["Item 2", 100],
["", ""],
["Total Items Received", 150]
])
end
end

context "render pdf" do
it "renders correctly" do
pdf = described_class.new(organization, donation)
pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render))
expect(pdf_test.page(1).text).to include(donation_site.name)
expect(pdf_test.page(1).text).to include(donation_site.address)
expect(pdf_test.page(1).text).to include(donation_site.email)
if donation.comment
expect(pdf_test.page(1).text).to include(donation.comment)
end
expect(pdf_test.page(1).text).to include("Money Raised In Dollars: $0.00")
expect(pdf_test.page(1).text).to include("Items Received")
expect(pdf_test.page(1).text).to match(/Item 1\s+\$1\.00\s+\$50\.00\s+50/)
expect(pdf_test.page(1).text).to match(/Item 2\s+\$2\.00\s+\$200\.00\s+100/)
expect(pdf_test.page(1).text).to include("Total Items Received")
end
end
end
Loading

0 comments on commit 4acb394

Please sign in to comment.