Last active
July 5, 2024 07:28
-
-
Save ajesler/5b37945fbfd4bdc63888b985e29e527a to your computer and use it in GitHub Desktop.
PDF Invoice generation script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Usage: | |
# Save this to a file invoice.rb, and set the values from L#185 on to your needs. | |
# Then run | |
# $ ruby invoice.rb | |
require 'bundler/inline' | |
require "date" | |
gemfile true do | |
source "https://rubygems.org" | |
gem 'prawn-table', "~> 0.2.2" | |
end | |
module TwoColumnHelper | |
def two_columns(ratio: 0.5, left_width: nil) | |
left_column_width = left_width || ratio * bounds.width | |
right_column_width = bounds.width - left_column_width | |
start_y = cursor | |
bounding_box([0, start_y], :width => left_column_width) do | |
yield :left | |
end | |
bounding_box([left_column_width, start_y], :width => right_column_width) do | |
yield :right | |
end | |
end | |
end | |
class LineItem | |
attr_reader :description, :rate, :quantity | |
def initialize(description:, rate:, quantity:) | |
@description = description | |
@rate = rate | |
@quantity = quantity | |
end | |
def amount | |
quantity * rate | |
end | |
end | |
BankDetails = Struct.new(:name, :account_number) | |
Supplier = Struct.new(:name, :address, :phone_number, :gst_number, :bank_account) | |
Recipient = Struct.new(:name, :address) | |
class Invoice | |
attr_reader :supplier, :recipient, :date, :id, :line_items, :extra_information | |
def initialize(supplier:, recipient:, date:, id:, line_items:, **extra_information) | |
@supplier = supplier | |
@recipient = recipient | |
@date = date | |
@id = id | |
@line_items = line_items | |
@extra_information = extra_information | |
end | |
def name | |
"#{recipient.name}-#{id}" | |
end | |
end | |
class InvoiceDocument | |
include Prawn::View | |
include TwoColumnHelper | |
attr_reader :invoice | |
def initialize(invoice) | |
@invoice = invoice | |
end | |
def build | |
text "TAX INVOICE", size: 20, align: :center | |
hr | |
supplier_details | |
hr | |
recipient_details | |
move_down 30 | |
items | |
move_down 30 | |
payment_info | |
end | |
private | |
def hr | |
move_down 10 | |
horizontal_rule | |
move_down 10 | |
end | |
def supplier_details | |
two_columns do |side| | |
if side == :left | |
text "GST Number: #{supplier.gst_number}" | |
text "Date: #{invoice.date.strftime('%e %b %Y')}" | |
text "Invoice Number: #{invoice.id}" | |
else | |
text supplier.name, align: :right | |
text supplier.address, align: :right | |
end | |
end | |
end | |
def recipient_details | |
two_columns(left_width: 40) do |side| | |
if side == :left | |
text "To:" | |
else | |
text recipient.name | |
text recipient.address | |
end | |
end | |
if invoice.extra_information.size > 0 | |
move_down 20 | |
invoice.extra_information.each { |name, value| text "#{name}: #{value}" } | |
end | |
end | |
def items | |
subtotal = invoice.line_items.sum(&:amount).round(2) | |
gst = (subtotal * 0.15).round(2) | |
total = (subtotal + gst).round(2) | |
data = [ | |
["Description", "Hours", "Rate", "Amount"], | |
*invoice.line_items.map { |li| [li.description, right_aligned(li.quantity), right_aligned(li.rate), right_aligned(li.amount)] }, | |
[nil]*4, | |
[nil, {content: "Subtotal", colspan: 2, align: :right, font_style: :bold}, right_aligned("%.2f" % subtotal)], | |
[nil, {content: "GST", colspan: 2, align: :right, font_style: :bold}, right_aligned("%.2f" % gst)], | |
[nil, {content: "Total Due (NZD)", colspan: 2, align: :right, font_style: :bold}, right_aligned("%.2f" % total)], | |
] | |
column_widths = [0.55, 0.15, 0.15, 0.15].map { |r| r * bounds.width } | |
items_offset = invoice.line_items.count + 1 | |
table(data, column_widths: column_widths) do | |
cells.padding = 5 | |
row(0).font_style = :bold | |
row(items_offset).borders = [] | |
rows(items_offset..(items_offset + 4)).borders = [] | |
row(items_offset+3).columns(1..3).borders = [:top] | |
end | |
end | |
def right_aligned(content) | |
{ content: content.to_s, align: :right } | |
end | |
def payment_info | |
if (bank_account = invoice.supplier.bank_account) | |
text "Please pay by direct credit to" | |
move_down 5 | |
text "Name: #{bank_account.name}" | |
text "Account Number: #{bank_account.account_number}" | |
end | |
end | |
def supplier | |
invoice.supplier | |
end | |
def recipient | |
invoice.recipient | |
end | |
end | |
if __FILE__ == $0 | |
# Generate and save the invoice PDF" | |
invoice_date = Date.new(2024, 1, 31) | |
bank_details = BankDetails.new("J SMITH", "01-1234-0000567-000") | |
supplier = Supplier.new("Jean Smith", "1 Sample Ave\nSumner\nChristchurch", "021 111 1111", "000-000-000", bank_details) | |
recipient = Recipient.new("Company Being Billed Co", "PO Box 20 123\nAuckland") | |
id = invoice_date.strftime("%Y%m%d") | |
line_items = [ | |
LineItem.new( | |
description: "Technical services (January 2024)", | |
quantity: 75, | |
rate: 50 | |
) | |
] | |
invoice = Invoice.new( | |
supplier: supplier, | |
recipient: recipient, | |
date: invoice_date, | |
id: id, | |
line_items: line_items, | |
"Purchase Order Number": "1234-5678", | |
"Custom Attribute": "ABC123" | |
) | |
invoice_file_name = "invoice-#{invoice.id}.pdf" | |
invoice_document = InvoiceDocument.new(invoice) | |
invoice_document.build | |
invoice_document.save_as(invoice_file_name) | |
puts "\nGenerated #{invoice_file_name}" | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment