Clay-UI
view release on metacpan or search on metacpan
examples/16-invoice-pdf.pl view on Meta::CPAN
my $panel = $self->_box(
layout => {
sizing => { width => sizing_grow() },
padding => padding_all(12),
child_gap => 24,
},
background_color => $COLOR{zebra},
corner_radius => 4,
);
my $terms = $self->_box(layout => { sizing => { width => sizing_grow() }, layout_direction => CLAY_TOP_TO_BOTTOM, child_gap => 4 });
$terms->add_child(
$self->_caption('Payment terms'),
map { $self->_wrapped_text($_) } $invoice->{payment_terms}->@*,
);
my $bank = $self->_column(4,
$self->_caption('Bank details'),
map { $self->_text($_) } $invoice->{bank}->@*,
);
$panel->add_child($terms, $bank);
return $panel;
}
# ---- Footer ----
method _footer ($page_number, $page_count) {
my $footer = $self->_box(
layout => {
sizing => { width => sizing_grow() },
padding => { top => 8 },
child_alignment => { y => CLAY_ALIGN_Y_CENTER },
},
border_color => $COLOR{rule},
border_width => { top => 1, left => 0, right => 0, bottom => 0, between_children => 0 },
);
$footer->add_child(
$self->_text($invoice->{seller}{legal}, font_size => 7, color => 'muted'),
$self->_spacer,
$self->_text("Page $page_number / $page_count", font_size => 8, color => 'muted'),
);
return $footer;
}
}
# ---- Invoice data ----
sub format_money ($cents) {
my $text = sprintf '%d.%02d', int($cents / 100), $cents % 100;
1 while $text =~ s/^(\d+)(\d{3})/$1,$2/;
return $text;
}
# Deterministic line items for the service period, September 2026
# (calendar weeks 36 to 40). Monthly services come first, billed once
# for the month; the other services are billed per week. Within a week
# the services rotate, a week has at most one item of each daily service,
# and every hourly item names its own ticket, so no week lists the same
# item twice. Some items carry a long detail text
# that wraps over several lines.
sub generate_items ($count) {
my @monthly_services = (
[ 'Managed database', 24_900, 'PostgreSQL cluster with daily backups, point-in-time recovery and around-the-clock monitoring.' ],
[ 'Support retainer', 45_000, undef ],
);
my @weekly_services = (
[ 'Backend development', 'hours', 9_500, 'API endpoints for the order import, including validation of partner payloads and retry handling for failed deliveries.' ],
[ 'Frontend development', 'hours', 8_800, undef ],
[ 'Code review', 'hours', 7_500, 'Review of pull requests for the reporting module.' ],
[ 'Project management', 'hours', 7_000, undef ],
[ 'UX workshop', 'days', 120_000, 'Remote workshop with stakeholders, including preparation, moderation and a written summary of all decisions taken.' ],
[ 'Security audit', 'days', 135_000, 'Dependency audit and penetration test of the customer portal; findings delivered as a prioritised report.' ],
);
my ($first_week, $week_count) = (36, 5);
my @hourly_services = grep { $_->[1] eq 'hours' } @weekly_services;
my $monthly_count = $count < @monthly_services ? $count : @monthly_services;
my @items;
for my $service (@monthly_services[ 0 .. $monthly_count - 1 ]) {
my ($name, $unit_price, $detail) = @$service;
push @items, {
title => "$name, September 2026",
detail => $detail,
quantity => 1,
unit => 'month',
unit_price => $unit_price,
amount => $unit_price,
};
}
my $weekly_count = $count - @items;
my %items_in_week;
my $next_ticket = 1040;
for my $index (0 .. $weekly_count - 1) {
my $week = $first_week + int($index * $week_count / $weekly_count);
my $position = $items_in_week{$week}++;
# Once every service had its turn this week, only hourly ones repeat.
my $services = $position < @weekly_services ? \@weekly_services : \@hourly_services;
my ($name, $unit, $unit_price, $detail) = $services->[ ($week + $position) % @$services ]->@*;
my $quantity = $unit eq 'hours' ? 1 + ($index * 7) % 16 : 1 + $index % 3;
my $ticket = $unit eq 'hours' ? sprintf(', ticket NL-%d', $next_ticket++) : '';
push @items, {
title => "$name, calendar week $week$ticket",
detail => $detail,
quantity => $quantity,
unit => $quantity == 1 ? substr($unit, 0, -1) : $unit,
unit_price => $unit_price,
amount => $quantity * $unit_price,
};
}
return \@items;
}
sub build_invoice ($item_count) {
my $items = generate_items($item_count);
my $vat_percent = 19;
my $subtotal = 0;
$subtotal += $_->{amount} for @$items;
my $vat = int(($subtotal * $vat_percent + 50) / 100); # cents, rounded half up
return {
number => 'INV-2026-0042',
( run in 1.112 second using v1.01-cache-2.11-cpan-3f962061493 )