Hi I'm trying to implement downloading a PDF receipt. I'm not sure how to integrate Prawn with Rails 4 app and I couldn't find any tutorials on how to do this. Please see below for things that I have done. Could someone please suggest some articles to me or some tips.
1 Added Prawn gem and did bundle install
2 Added controller code to render PDF
respond_to do |format|
format.html
format.pdf do
pdf = OrderPdf.new(#order, view_context)
send_data pdf.render, filename: "order_#{#order.order_number}.pdf",
type: "application/pdf",
disposition: "inline"
end
end
3 Have a link_to code in the view. The view is located App > PDF
<%= link_to "Download PDF", order_path(#order, format: pdf) %>
I'm not sure which part you need help with, but here is how I've done it. In the code below, I am creating a pdf of a receipt and storing it in the database. The output looks like this - Sample Receipt
Perhaps it may be of some help.
class Omni::ReceiptWorksheet < Omni::Receipt
def print(receipt)
pdf = header receipt
data = []
data[0] = ["PO Nbr","Carton Nbr","Sku Nbr","Sku Description","S/U Open","S/U per Pack","Packs Open", "Packs Received"]
receipt.receipt_details.each_with_index do |detail,i|
selling_units = detail.purchase_detail.selling_units_approved - detail.purchase_detail.selling_units_received - detail.purchase_detail.selling_units_cancelled
data[i+1] = [detail.purchase.purchase_nbr,' ', detail.sku.sku_nbr, detail.sku.display, selling_units, detail.receipt_pack_size, selling_units / detail.receipt_pack_size, ' ']
end
pdf.move_down 110
pdf.table(data) do |t|
t.style(t.row(0), :background_color => '0075C9')
t.header = true
end
pdf.number_pages "page <page> of <total>", { :at => [pdf.bounds.right - 150, 0], width: 150, align: :right, page_filter: (1..50), start_count_at: 1, color: "002B82" }
attach StringIO.new(pdf.render), "receiving_worksheet#{Date.today}.pdf", receipt
end
def header(receipt)
pdf = Prawn::Document.new
pdf.font_size = 12
pdf.draw_text "Printed on: #{Date.today}", at: [0, 670]
pdf.draw_text "Receiving Worksheet", at: [220, 670]
pdf.draw_text "Page 1", at: [480, 670]
pdf.draw_text "Receipt #: #{receipt.receipt_nbr}", at: [0, 650]
pdf.draw_text "Receipt Date: #{Date.today}", at: [400, 650]
pdf.draw_text "Receiving Location: #{receipt.location_display}", at: [0, 640]
pdf.draw_text "Carrier Name: #{receipt.carrier_supplier.display}", at: [0, 620]
pdf.draw_text "Bill of Lading: #{receipt.bill_of_lading_number}", at: [450, 620]
pdf
end
def attach(file, file_name, receipt)
attachment = Buildit::Attachment.create(
attachable_type: "Omni::Receipt",
attachable_id: receipt.receipt_id,
file_name: file_name,
mime_type: 'application/pdf',
byte_size: file.size,
locale: 'en',
is_enabled: true
)
Buildit::Content.create(
contentable_type: "Buildit::Attachment",
contentable_id: attachment.attachment_id,
data: file.read
)
end
end
Below is my controller for uploading and downloading the attachment.
class ContentController < ActionController::Base
def download
content = Buildit::Content.find_by_content_id(params[:file_id])
contentable = content.contentable
file_name = (contentable ? contentable.file_name : 'file')
send_data content.data, :disposition => 'attachment', :filename => file_name
end # def download
def upload
begin
content = Buildit::Content.create(
data: params[:file].read
)
result = {
success: true,
content_id: content.content_id,
file_name: params[:file].original_filename,
mime_type: params[:file].content_type,
byte_size: params[:file].size
}
rescue
result = {success: false}
end
render text: result.to_json, status: 200
end # def upload
end # class ContentController
Good luck! Let me know if you need something more specific.
I found the prawn_rails gem useful. Note in your gemfile you need gem prawn_rails with the underscore, not a dash
Related
I'm using gem 'receipts' by Chris Oliver, and I want to insert QRcode with payment details (to the footer section for example).
Models: Partner and Charge.
I want QR code to contain model attributes, like these: charge.partner.iban, charge.amount and something like that.
Charge.rb has method receipt:
def receipt
Receipts::Invoice.new(
details: [
["Receipt Number", "123"],
["Date paid", Date.today],
["Payment method", "ACH super long super long super long super long super long"]
],
company: {
name: "Example, LLC",
address: "123 Fake Street\nNew York City, NY 10012",
email: "support#example.com",
logo: File.expand_path("./app/assets/images/logo.png")
},
recipient: [
self.partner.name,
self.partner.address,
"City, State Zipcode",
nil,
"customer#example.org"
],
line_items: [
["<b>Item</b>", "<b>Unit Cost</b>", "<b>Quantity</b>", "<b>Amount</b>"],
["Subscription", "$19.00", "1", "$19.00"],
[nil, nil, "Subtotal", "$19.00"],
[nil, nil, "Tax", "$1.12"],
[nil, nil, "Total", "$20.12"],
[nil, nil, "<b>Amount paid</b>", "$20.12"],
[nil, nil, "Refunded on #{Date.today}", "$5.00"]
],
footer: "Thanks for your business. Please contact us if you have any questions."
)
end
charges_controller has:
def show
respond_to do |format|
format.html
format.json
format.pdf { send_pdf }
end
end
private
def send_pdf
send_data #charge.receipt.render,
filename: "#{#charge.created_at.strftime("%Y-%m-%d")}-gorails-receipt.pdf",
type: "application/pdf",
disposition: :inline # or :attachment to download
end
charges/show.html.erb:
<%= link_to "View invoice", charge_path(#charge, format: :pdf) %>
I tried using prawn-qrcode, but couldn't make it.
Maximum what I get is something as line of text in the footer.
When I put this in receipts method:
qrcode = RQRCode::QRCode.new(self.partner.ico.to_s)
png = qrcode.as_png(
resize_gte_to: false,
resize_exactly_to: false,
fill: 'white',
color: 'black',
size: 120,
border_modules: 4,
module_px_size: 6,
file: nil # path to write
)
and this in the footer:
footer: ActionController::Base.helpers.image_tag(png.to_data_url)
What should I do to insert QRcode with desired data? Is there any examples of similar task?
Thanks
Note: This is untested as-is but should work based on the documentation of the libraries themselves.
receipts uses prawn to generate pdfs.
Someone has created a QR Code renderer for prawn called prawn-qrcode which uses rqrcode so you may be able to use this as a simple bridge between the 2 libraries.
Theoretical Example:
def send_pdf
receipt = #charge.receipt
qr_code = RQRCode::QRCode.new(self.partner.ico.to_s)
receipt.render_qr_code(qr_code, extent: 72)
send_data receipt.render,
filename: "#{#charge.created_at.strftime("%Y-%m-%d")}-gorails-receipt.pdf",
type: "application/pdf",
disposition: :inline # or :attachment to download
end
Additional Notes:
You may have to play around with positioning e.g. render_qr_code(qr_code, pos: [x,y])
I am not sure what self.partner.ico.to_s generates
In my ParametersController, I defined the export method to download a list of parameters as a xlsx formated file:
# Get /parameters_list/1/export.xlsx
def export
### Exports the parameters attached to #parameters_lists in a Workbook
workbook = WriteXLSX.new("./public/#{#parameters_list.code}_parameters.xlsx")
title = workbook.add_format(:font => 'Calibri', :size => 16, :color => 'blue', :bold => 1)
header = workbook.add_format(:font => 'Calibri', :size => 12, :color => 'black', :bold => 1)
data = workbook.add_format(:font => 'Calibri', :size => 10, :color => 'black', :bold => 0)
workbook.set_properties(
:title => "Parameters lists export: #{#parameters_list.code}",
:subject => 'This file contains exported parameters',
:author => 'Open Data Quality',
:comments => translation_for(#parameters_list.name_translations)
#,
#:hyperlink_base => url_for #parameters_list
)
### write Description sheet
cover = workbook.add_worksheet('Parameters list')
cover.write(0, 0, #parameters_list.code, title)
cover.write(1, 0, translation_for(#parameters_list.name_translations), header)
index = 3
#parameters_list.attributes.map do |name, value|
cover.write(index, 0, name, header)
cover.write(index, 1, value, data)
index += 1
end
### write Data sheet
data_sheet = workbook.add_worksheet('Parameters')
index = 0
## Headers
# Write attributes headers
#parameters_list.parameters.first.attributes.each do |attribute, value|
data_sheet.write(0, index, attribute, header)
index += 1
end
# Add translations headers
list_of_languages.order(:property).each do |locution|
data_sheet.write(0, index, "Name_#{locution.code}", header)
index += 1
data_sheet.write(0, index, "Description_#{locution.code}", header)
index += 1
end
## Data
#parameters_list.parameters.each_with_index do |parameter, line_index|
index = 0
# Write attributes
parameter.attributes.each do |attribute, value|
data_sheet.write(line_index + 1, index, value, data)
index += 1
end
# Add translations
list_of_languages.order(:property).each do |locution|
data_sheet.write(line_index + 1, index, parameter.name_translations.where(language: locution.property).first&.translation, data)
index += 1
data_sheet.write(line_index + 1, index, parameter.description_translations.where(language: locution.property).first&.translation, data)
index += 1
end
end
# close workbook
workbook.close
send_file "./public/#{#parameters_list.code}_parameters.xlsx", type: "application/xlsx"
end
With respect to the MVC architecture, I think this would rather be defined as a view, accessible through a URL with specified format, such as my_server/parameters_lists/15.xlsx relying on the show method:
# GET /parameters_list/1
# GET /parameters_list/1.json
# GET /parameters_list/1.csv
# GET /parameters_list/1.xlsx
def show
### #parameters_list retrieved by callback function
respond_to do |format|
format.html # show.html.erb
format.json { render json: #parameters_list.parameters }
format.csv { send_data #parameters_list.parameters.to_csv }
format.xlsx # uses specific template to generate the a file
end
end
I tried several ways to move and encapsulate the code into a show.xlsx.erb file, but it does not generate
more than an text file without data.
Can you help me find out how to achieve this?
You can use caxlsx_rails gem
For eg: Create the template with the .xlsx.axlsx extension
sample.xlsx.axlsx
wb = xlsx_package.workbook
wb.add_worksheet(name: "Samples") do |sheet|
#samples.each do |sample|
sheet.add_row [sample]
end
end
format.xlsx {
response.headers['Content-Disposition'] = 'attachment; filename="my_new_filename.xlsx"'
}
For more info you can go here
In my Rails app I have a Job to create PDF files with WickedPDF. This worked fine until I migrated to Rails 5.2, now I get this error:
ActionView::Template::Error (Can't resolve image into URL: undefined method `polymorphic_url' for #<ActionView::Base:0x0000000004854590>):
The code where this happens is this line in the view:
image_tag(image)
This view is rendered from the Job, which I added in full below. The error happens when executing the last method in the class: private def render_image_pdf(view, pdf_name, image), where it is rendering a PDF.
After rolling back to Rails 5.1, everything works fine again, so I'm pretty sure it has something to do with an added/changed feature in Rails 5.2. So what should I change to make this working again?
The PDF Job:
class SendInvoiceAsAttachmentJob < ApplicationJob
require 'open-uri'
queue_as :default
def perform(target, invoice, send_invoice, to, cc, subject, body1, body2, body3)
view = ActionView::Base.new(ActionController::Base.view_paths, {})
view.extend(ApplicationHelper)
view.extend(Rails.application.routes.url_helpers)
# Create invoice as PDF
pdf = Rails.root.join('tmp', "tmp_invoice.pdf")
File.open(pdf, 'wb') do |file|
file << render_invoice_pdf(invoice, view, "tmp_invoice.pdf")
end
# Add (signed) timesheet as PDF
projectuser = Projectuser.where(project_id: invoice.project_id, user_id: invoice.user_id).order(:updated_at).last
if projectuser.blank? or invoice.fixed_price? or invoice.project.service?
pdf_name = "#{invoice.set_pdf_filename}.pdf"
pdf_to_email = pdf
else
if invoice.timesheet and invoice.timesheet.signed_copy?(projectuser)
# Download signed copy
timesheet_copy = TimesheetCopy.where(projectuser_id: projectuser.id, timesheet_id: invoice.timesheet_id).first
timesheet_file = Rails.root.join('tmp', timesheet_copy.attachment_file_name)
timesheet_name = timesheet_copy.attachment_file_name
if timesheet_copy.attachment.url.index("http").blank?
download = open("https:#{timesheet_copy.attachment.url}")
else
download = open(timesheet_copy.attachment.url)
end
IO.copy_stream(download, timesheet_file)
# Push into a PDF when image
if timesheet_copy.attachment_file_name.index(".pdf") == nil
timesheet_tmp = Rails.root.join('tmp', "tmp_timesheet.pdf")
File.open(timesheet_tmp, 'wb') do |file|
file << render_image_pdf(view, "tmp_timesheet.pdf", timesheet_file)
end
timesheet_file = timesheet_tmp
File.delete Rails.root.join('tmp', timesheet_copy.attachment_file_name)
end
else
# Create timesheet PDF
timesheet = Timesheet.find(invoice.timesheet_id)
timesheet_builder = TimesheetBuilder.new(timesheet.month, timesheet.year)
timesheet_name = "Timesheet #{timesheet.user.full_name} #{I18n.t("date.month_names")[timesheet.month]} #{timesheet.year}"
timesheet_file = Rails.root.join('tmp', timesheet_name)
File.open(timesheet_file, 'wb') do |file|
file << render_timesheet_pdf(timesheet, view, timesheet_name, projectuser, timesheet_builder)
end
end
# Combine the 2 PDF's
combined_pdf = CombinePDF.new
combined_pdf << CombinePDF.load(pdf, allow_optional_content: true)
combined_pdf << CombinePDF.load(timesheet_file, allow_optional_content: true)
pdf_name = "#{invoice.set_pdf_filename}.pdf"
combined_pdf.save Rails.root.join('tmp', pdf_name)
pdf_to_email = Rails.root.join('tmp', pdf_name)
File.delete(timesheet_file)
end
# Send email
if target == "basecone"
company = Company.find(invoice.company_id)
UserMailer.send_pdf_to_basecone(company.basecone_email, pdf_to_email, pdf_name).deliver
else
UserMailer.invoice_email(invoice, send_invoice, to, cc, subject, body1, body2, body3, pdf_to_email, pdf_name).deliver
end
File.delete(pdf_to_email)
end
private def render_invoice_pdf(invoice, view, pdf_name)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'admin/invoices/show_as_attachment.pdf.haml',
locals: { invoice: invoice, copy_invoice: nil },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
private def render_timesheet_pdf(timesheet, view, pdf_name, projectuser, timesheet_builder)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'timesheets/show_as_attachment.pdf.haml',
locals: { timesheet: timesheet, timesheet_builder: timesheet_builder, projectuser: projectuser },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
private def render_image_pdf(view, pdf_name, image)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'admin/invoices/image_timesheet_as_attachment.pdf.haml',
locals: { image: image },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
end
Use wicked_pdf_image_tag Instead of image_tag
<%=wicked_pdf_image_tag image%>
Instead of manually constructing the ActionView setup (and thus needing to add all the right helpers, which is where you're going wrong -- I think some moved around), you can use the newish ActionController::Renderer API to perform a view render within the context of an arbitrary controller.
# Very untested; please treat this is syntactically-valid pseudocode...
WickedPdf.new.pdf_from_string(
ApplicationController.render(
template: 'admin/invoices/show_as_attachment.pdf.haml',
assigns: {
pdf: pdf_name,
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4',
},
locals: { invoice: invoice, copy_invoice: nil },
)
)
My code is
class pdfController < ApplicationController
def index
#posts = Post.all
respond_to do |format|
format.html
format.pdf do
pdf = Prawn::Document.new
send_data pdf.render, :filename => "report.pdf", :type => "application/pdf", :disposition => "inline"
end
end
end
end
Now the problem is, pdf file downloaded. But I am getting an empty file, there is no report in that downloaded file.
And the code#posts = Post.all Post is a table, I can get the data from Post to downloaded pdf file.
As suggested in this post: http://adamalbrecht.com/2014/01/14/generate-clean-testable-pdf-reports-in-rails-with-prawn/
You can inherit Prawn::Document class for an easy configuration of your table border color and font size.
class PdfReport < Prawn::Document
# Often-Used Constants
TABLE_ROW_COLORS = ["FFFFFF","DDDDDD"]
TABLE_FONT_SIZE = 9
TABLE_BORDER_STYLE = :grid
def initialize(default_prawn_options={})
super(default_prawn_options)
font_size 10
end
def header(title=nil)
text "My Awesome Invoice!!", size: 18, style: :bold, align: :center
if title
text title, size: 14, style: :bold_italic, align: :center
end
end
def footer
# ...
end
# ... More helpers
end
Now, the above class is handful to use at other places by inheriting to some other classes, for example: posts reports, comments reports, etc. For your posts reports, We could do something like this:
class PostSummaryReportPdf < PdfReport
TABLE_WIDTHS = [20, 100, 30, 60]
TABLE_HEADERS = ["ID", "Name", "Date", "User"]
def initialize(posts=[])
super()
#posts = posts
header "Posts' Invoice Summary Report"
display_event_table
footer
end
private
def display_event_table
if table_data.empty?
text "No Events Found"
else
table table_data,
headers: TABLE_HEADERS,
column_widths: TABLE_WIDTHS,
row_colors: TABLE_ROW_COLORS,
font_size: TABLE_FONT_SIZE
end
end
def table_data
#table_data ||= #posts.map { |p| [p.id, p.name, p.created_at.strftime("%m/%d/%y"), p.created_by.try(:full_name)] }
end
end
Everything is now up and ready to be used in DRY-way within your controllers.
class pdfController < ApplicationController
def index
#posts = Post.all
respond_to do |format|
format.html
format.pdf do
pdf = PostSummaryReportPdf.new(#posts)
send_data pdf.render, :filename => "report.pdf", :type => "application/pdf", :disposition => "inline"
end
end
end
end
I hope that helps.
What's the best way to test the following code using RSpec? And what should I be testing for? The show action opens a file and streams it. Also, if the action relies on a file existing somewhere, can I test that?
def show
image_option = params[:image_option]
respond_to do |format|
format.js
format.pdf {open_bmap_file("#{#bmap.bmap_pdf_file}", 'application/pdf', "#{#bmap.bmap_name}.pdf", "pdf", "pdf")}
format.png {open_bmap_file("#{#bmap.bmap_png_file}", 'image/png', "#{#bmap.bmap_name}.png", "png", image_option)}
end
end
private
def open_bmap_file(filename, application_type, send_filename, format, image_option = nil)
filename = "app/assets/images/image_not_available_small.png" unless File.exist? filename
path = Bmap.bmaps_pngs_path
case image_option
when "image"
filename = "#{#bmap.bmap_name}.png"
when "large_thumbnail"
filename = "#{#bmap.bmap_name}_large_thumb.png"
when "thumbnail"
filename = "#{#bmap.bmap_name}_thumb.png"
when "pdf"
filename = "#{#bmap.bmap_name}.pdf"
path = Bmap.bmaps_pdfs_path
else
filename = "#{#bmap.bmap_name}.pdf"
path = Bmap.bmaps_pdfs_path
end
begin
File.open(path + filename, 'rb') do |f|
send_data f.read, :disposition => image_option == "pdf" ? 'attachment' : 'inline', :type => application_type, :filename => send_filename
end
rescue
flash[:error] = 'File not found.'
redirect_to root_url
end
I needed to test send_data in a controller action that downloads a csv file, and I did it in the following way.
controller:
def index
respond_to do |format|
format.csv do
send_data(Model.generate_csv,
type: 'text/csv; charset=utf-8; header=present',
filename: "report.csv",
disposition: 'attachment')
end
end
end
(rspec 2 solution) controller_spec:
context "when format is csv" do
let(:csv_string) { Model.generate_csv }
let(:csv_options) { {filename: "report.csv", disposition: 'attachment', type: 'text/csv; charset=utf-8; header=present'} }
it "should return a csv attachment" do
#controller.should_receive(:send_data).with(csv_string, csv_options).
and_return { #controller.render nothing: true } # to prevent a 'missing template' error
get :index, format: :csv
end
end
(rspec 3 solution) controller_spec:
context "when format is csv" do
let(:csv_string) { Model.generate_csv }
let(:csv_options) { {filename: "report.csv", disposition: 'attachment', type: 'text/csv; charset=utf-8; header=present'} }
it "should return a csv attachment" do
expect(#controller).to receive(:send_data).with(csv_string, csv_options) {
#controller.render nothing: true # to prevent a 'missing template' error
}
get :index, format: :csv
end
end
It's helped me
stub(controller).send_data { controller.render nothing: true }