invoices: rework of PDF
All checks were successful
Build and Deploy to Production / build (push) Successful in 50s
Build and Deploy to Production / deploy (push) Successful in 27s

This commit is contained in:
Sergei Poljanski 2026-06-09 03:52:12 +03:00
commit 16680be309
Signed by: asxpi
GPG key ID: 4F8851660FA4121B

View file

@ -13,8 +13,6 @@ class InvoicePdf
name_alt: 'SERGEI POLJANSKI',
name_georgian: 'ინდ. მეწარმე სერგეი პოლჯანსკი',
tax_id: '304813343',
reg_number: 'B26353511',
registered: '2026-05-04',
address: [
'Ilia and Nino Nakashidze St, N 1, Building N3, Apt N3',
'Krtsanisi, Tbilisi, Georgia'
@ -31,6 +29,13 @@ class InvoicePdf
COLOR_RULE = 'DADADA'.freeze
COLOR_ACCENT = '1A1A1A'.freeze
# Uniform line spacing for the FROM / BILL TO party blocks: every line gets the
# same leading, no extra gaps between groups, so both columns read as one even
# rhythm and line up. NAME_GAP is the one exception — a little extra air after
# the bold company name to set it off from the details below.
PARTY_LEADING = 2
NAME_GAP = 3
# `status` overrides the status shown in the PDF (e.g. to pre-render a "paid"
# copy before any payment date exists). Defaults to the invoice's live status.
def self.render(invoice, status: nil)
@ -109,17 +114,17 @@ class InvoicePdf
pdf.bounding_box([0, pdf.cursor], width: col_w) do
draw_party_label(pdf, 'FROM')
pdf.font_size(11) { pdf.text ISSUER[:name_latin], style: :bold, leading: 2 }
pdf.font_size(11) { pdf.text ISSUER[:name_latin], style: :bold, leading: PARTY_LEADING }
pdf.move_down NAME_GAP
pdf.font_size(9) do
pdf.text ISSUER[:name_georgian], leading: 4
pdf.move_down 8
pdf.text ISSUER[:address].join("\n"), leading: 3
pdf.move_down 8
pdf.text "Tax ID: #{ISSUER[:tax_id]}", leading: 3
pdf.text "Reg. №: #{ISSUER[:reg_number]} (#{ISSUER[:registered]})", leading: 3
pdf.move_down 8
pdf.text ISSUER[:email], leading: 3
pdf.text ISSUER[:phone]
lines = [
ISSUER[:name_georgian],
*ISSUER[:address],
"Tax ID: #{ISSUER[:tax_id]}",
ISSUER[:email],
ISSUER[:phone]
]
pdf.text lines.join("\n"), leading: PARTY_LEADING
end
end
from_y = pdf.y
@ -127,13 +132,12 @@ class InvoicePdf
pdf.y = top_y
pdf.bounding_box([right, pdf.cursor], width: pdf.bounds.right - right) do
draw_party_label(pdf, 'BILL TO')
pdf.font_size(11) { pdf.text @invoice.client_name, style: :bold, leading: 2 }
pdf.font_size(11) { pdf.text @invoice.client_name, style: :bold, leading: PARTY_LEADING }
pdf.move_down NAME_GAP
pdf.font_size(9) do
pdf.text @invoice.client_email, leading: 3
unless @invoice.client_address.to_s.strip.empty?
pdf.move_down 8
pdf.text @invoice.client_address, leading: 3
end
lines = [@invoice.client_email]
lines << @invoice.client_address unless @invoice.client_address.to_s.strip.empty?
pdf.text lines.join("\n"), leading: PARTY_LEADING
end
end
bill_y = pdf.y
@ -152,7 +156,7 @@ class InvoicePdf
def draw_meta(pdf)
pdf.stroke_color COLOR_RULE
pdf.stroke_horizontal_rule
pdf.move_down 10
pdf.move_down 8
cells = [
['ISSUED', @invoice.issued_on.strftime('%Y-%m-%d')],
@ -168,13 +172,16 @@ class InvoicePdf
pdf.fill_color COLOR_MUTED
pdf.font_size(7) { pdf.text label, character_spacing: 1.2 }
pdf.fill_color COLOR_TEXT
pdf.move_down 3
pdf.move_down 2
pdf.font_size(10) { pdf.text value, style: :bold }
end
end
# Reserve the height the row actually used.
pdf.move_down 30
# The cells are absolutely-positioned boxes that don't advance the outer
# cursor, so reserve the row height manually. Tuned (measured from the
# rendered PNG) so the gap below the values matches the ~10pt gap above the
# labels rather than ballooning to ~29pt.
pdf.move_down 9
pdf.stroke_horizontal_rule
pdf.move_down 16
end