invoices: cancel with stamped PDF, superuser-only hiding
An invoice issued by mistake could only be left pending or marked paid. Add cancel (with an optional reason) and reinstate. Cancelling renders a third PDF variant with a diagonal CANCELLED stamp and the date/reason, and it becomes the current one, so the client's link and download show the invoice as void. Cancelled wins over paid in the status, and marking a cancelled invoice paid is refused. Superusers can also hide an invoice: it disappears from the admin list, dashboard and invoice pages for every other user, while the client's public link keeps working. Assisted-by: Claude:opus-5
This commit is contained in:
parent
97919f3f71
commit
eeac07ed34
10 changed files with 231 additions and 13 deletions
|
|
@ -24,7 +24,13 @@ class Invoice < Sequel::Model(:invoices)
|
|||
!paid_at.nil?
|
||||
end
|
||||
|
||||
def cancelled?
|
||||
!cancelled_at.nil?
|
||||
end
|
||||
|
||||
# Cancellation wins over payment: a cancelled invoice is no longer owed.
|
||||
def status
|
||||
return 'cancelled' if cancelled?
|
||||
paid? ? 'paid' : 'pending'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class InvoicePdf
|
|||
COLOR_MUTED = '6F6F6F'.freeze
|
||||
COLOR_RULE = 'DADADA'.freeze
|
||||
COLOR_ACCENT = '1A1A1A'.freeze
|
||||
COLOR_VOID = 'C0392B'.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
|
||||
|
|
@ -71,12 +72,38 @@ class InvoicePdf
|
|||
draw_totals(pdf)
|
||||
draw_payment(pdf)
|
||||
draw_footer(pdf)
|
||||
draw_cancelled_stamp(pdf) if @status == 'cancelled'
|
||||
|
||||
pdf.render
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Diagonal stamp on every page plus the date/reason under it, so a
|
||||
# cancelled invoice can't be mistaken for a payable one.
|
||||
def draw_cancelled_stamp(pdf)
|
||||
when_text = (@invoice.cancelled_at || Time.now.utc).strftime('%Y-%m-%d')
|
||||
reason = @invoice.cancel_reason.to_s.strip
|
||||
pdf.repeat(:all) do
|
||||
pdf.fill_color COLOR_VOID
|
||||
pdf.transparent(0.18) do
|
||||
pdf.rotate(35, origin: [pdf.bounds.width / 2, pdf.bounds.height / 2]) do
|
||||
pdf.font_size(96) do
|
||||
pdf.draw_text 'CANCELLED', style: :bold,
|
||||
at: [pdf.bounds.width / 2 - pdf.width_of('CANCELLED', style: :bold) / 2, pdf.bounds.height / 2 - 30]
|
||||
end
|
||||
end
|
||||
end
|
||||
pdf.transparent(0.9) do
|
||||
pdf.font_size(9) do
|
||||
line = "Cancelled #{when_text}#{reason.empty? ? '' : ": #{reason}"}"
|
||||
pdf.draw_text line, at: [0, pdf.bounds.height - 4]
|
||||
end
|
||||
end
|
||||
pdf.fill_color COLOR_TEXT
|
||||
end
|
||||
end
|
||||
|
||||
def register_fonts(pdf)
|
||||
pdf.font_families.update(
|
||||
'NotoSans' => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue