Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4481 Print Individual Donation Receipts #4484

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,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
13 changes: 12 additions & 1 deletion 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 @@ -441,6 +444,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 @@ -591,6 +600,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 @@ -756,6 +766,7 @@ DEPENDENCIES
omniauth-rails_csrf_protection
orderly (~> 0.1)
paper_trail
pdf-reader
pg (~> 1.5.6)
prawn-rails
pry-doc
Expand Down Expand Up @@ -787,4 +798,4 @@ DEPENDENCIES
webmock (~> 3.23)

BUNDLED WITH
2.5.11
2.5.14
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
mdphillips375 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -221,6 +224,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
mdphillips375 marked this conversation as resolved.
Show resolved Hide resolved
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)
mdphillips375 marked this conversation as resolved.
Show resolved Hide resolved
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
mdphillips375 marked this conversation as resolved.
Show resolved Hide resolved
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