SetFillColor(13, 110, 253);
$this->Rect(0, 0, $this->getPageWidth(), 2.5, 'F');
}
public function Footer() {
$margins = $this->getMargins();
$width = $this->getPageWidth() - $margins['left'] - $margins['right'];
$this->SetY(-14);
$this->SetDrawColor(222, 226, 230);
$this->SetLineWidth(0.2);
$this->Line($margins['left'], $this->GetY() - 1.5, $this->getPageWidth() - $margins['right'], $this->GetY() - 1.5);
$this->SetFont('dejavusans', '', 7);
$this->SetTextColor(108, 117, 125);
$this->Cell($width * 0.75, 4, $this->footer_label, 0, 0, 'L');
$this->Cell($width * 0.25, 4, 'Page ' . $this->getAliasNumPage() . ' of ' . $this->getAliasNbPages(), 0, 0, 'R');
}
}
/** The footers and the tax ID setting - a page has them loaded, the nightly run reads them here. */
function billingPdfConfig() {
global $mysqli, $config_invoice_footer, $config_quote_footer, $config_invoice_show_tax_id;
if (isset($config_invoice_footer, $config_quote_footer, $config_invoice_show_tax_id)) {
return;
}
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_invoice_footer, config_quote_footer, config_invoice_show_tax_id FROM settings WHERE company_id = 1")) ?: [];
$config_invoice_footer = $config_invoice_footer ?? (string) ($row['config_invoice_footer'] ?? '');
$config_quote_footer = $config_quote_footer ?? (string) ($row['config_quote_footer'] ?? '');
$config_invoice_show_tax_id = $config_invoice_show_tax_id ?? intval($row['config_invoice_show_tax_id'] ?? 0);
}
/**
* Draw an invoice, quote or recurring invoice - or an invoice's packing slip
* ($variant 'packing') - and return ['name' => file name, 'data' => the PDF].
*/
function billingPdfRender($type, array $row, $variant = 'document') {
global $mysqli, $config_invoice_footer, $config_quote_footer, $config_invoice_show_tax_id;
billingPdfConfig();
$t = billingType($type);
$c = $t['col'];
$id = intval($row["{$c}_id"]);
$company = billingCompany();
$bill_to = billingBillTo($row['client_id']);
$items = billingItems($type, $id);
$totals = billingTotals($type, $row, $items);
$currency = (string) $row["{$c}_currency_code"];
$number = billingNumber($type, $row);
$packing = $variant === 'packing';
$e = function ($value) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
};
$money = function ($amount) use ($currency, $e) {
return $e(billingMoney($amount, $currency));
};
$status = $type === 'recurring' ? '' : (string) $row["{$c}_status"];
// A quote with lines billed each month, quarter or year shows them apart from what is billed once
$groups = $type === 'quote' && !empty($totals['recurring']) ? $totals['groups'] : null;
$shown_groups = $groups ? array_filter($groups, function ($group) {
return $group['frequency'] !== '' || $group['items'] || $group['discount'] != 0;
}) : null;
$paid = $type === 'invoice' ? invoicePaid($id) : 0;
$state = $type === 'invoice' ? invoiceState($row, $paid) : null;
// Letter for North America, A4 everywhere else
$format = in_array($company['company_country'] ?? '', ['United States', 'USA', 'US', 'Canada'], true) ? 'LETTER' : 'A4';
$pdf = new BillingPdf('P', 'mm', $format, true, 'UTF-8', false);
$title = $packing ? 'Packing Slip' : ['invoice' => 'Invoice', 'quote' => 'Quote', 'recurring' => 'Recurring Invoice'][$type];
$pdf->SetCreator('ITFlow');
$pdf->SetAuthor((string) ($company['company_name'] ?? ''));
$pdf->SetTitle("$title $number");
$pdf->SetMargins(16, 14, 16);
$pdf->SetHeaderMargin(0);
$pdf->SetFooterMargin(12);
$pdf->SetAutoPageBreak(true, 20);
$pdf->setFontSubsetting(true);
$pdf->setImageScale(1.5);
$pdf->footer_label = trim(($company['company_name'] ?? '') . ' · ' . "$title $number", ' ·');
$pdf->AddPage();
$margins = $pdf->getMargins();
$width = $pdf->getPageWidth() - $margins['left'] - $margins['right'];
$grey = '#6c757d';
$line = '#dee2e6';
$label = function ($text) use ($grey, $e) {
return '' . $e(strtoupper($text)) . '';
};
// Who it is from, and what it is
$from_lines = array_merge(array_filter([
formatAddress($company['company_address'] ?? '', $company['company_city'] ?? '', $company['company_state'] ?? '', $company['company_zip'] ?? '', $company['company_country'] ?? '', "\n"),
implode(' · ', array_filter([$company['company_email'] ?? '', $company['phone_display'] ?? ''])),
$company['company_website'] ?? '',
!empty($config_invoice_show_tax_id) && !empty($company['company_tax_id']) ? 'Tax ID: ' . $company['company_tax_id'] : '',
]), $packing ? [] : billingTaxRegistrationLines($totals));
$logo = !empty($company['logo_path']) ? ' . ')
' : '';
if ($packing) {
$dates = ['Invoice' => $number, 'Date' => $row['invoice_date']];
} elseif ($type === 'invoice') {
$dates = ['Invoice date' => $row['invoice_date'], 'Due' => $row['invoice_due']];
} elseif ($type === 'quote') {
$dates = ['Quote date' => $row['quote_date'], 'Valid until' => $row['quote_expire']];
} else {
$dates = ['Billed' => recurringInvoiceFrequencies()[$row['recurring_invoice_frequency']] ?? $row['recurring_invoice_frequency'], 'Next invoice' => $row['recurring_invoice_next_date']];
}
$dates_html = '';
foreach ($dates as $date_label => $value) {
$dates_html .= '
| ' . $e($date_label) . ' | ' . $e($value) . ' |
';
}
$html = '
' . $logo . '' . $e($company['company_name'] ?? '') . ' ' . nl2br($e(implode("\n", $from_lines))) . ' |
' . $e(strtoupper($title)) . ' ' . $e($number) . '
|
';
$pdf->SetFont('dejavusans', '', 8.5);
$pdf->SetTextColor(33, 37, 41);
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->SetDrawColor(222, 226, 230);
$pdf->SetLineWidth(0.2);
$pdf->Ln(1);
$pdf->Line($margins['left'], $pdf->GetY(), $margins['left'] + $width, $pdf->GetY());
$pdf->Ln(4);
// Who it is for, and what it comes to
$to_lines = array_filter([
formatAddress($bill_to['location_address'] ?? '', $bill_to['location_city'] ?? '', $bill_to['location_state'] ?? '', $bill_to['location_zip'] ?? '', $bill_to['location_country'] ?? '', "\n"),
$bill_to['contact_name'] ?? '',
implode(' · ', array_filter([$bill_to['contact_email'] ?? '', $bill_to['contact_phone'] ?? ''])),
!empty($bill_to['client_tax_id_number']) ? 'Tax ID: ' . $bill_to['client_tax_id_number'] : '',
!$packing && !empty($bill_to['tax_exempt_text']) && $totals['tax'] == 0 ? $bill_to['tax_exempt_text'] : '',
]);
$to_label = $packing ? 'Ship to' : ($type === 'quote' ? 'Prepared for' : 'Bill to');
if ($packing) {
$amount_html = '';
} elseif ($type === 'invoice') {
$done = in_array($status, ['Paid', 'Cancelled', 'Non-Billable'], true);
$amount_html = $label($done ? 'Balance' : 'Amount due') . '
' . $money($state['balance']) . ''
. (!$done && $status !== 'Draft' && $state['balance'] > 0 ? '
by ' . $e($row['invoice_due']) . '' : '');
} elseif ($type === 'quote' && $shown_groups) {
$amount_html = '';
foreach (array_values($shown_groups) as $i => $group) {
$amount_html .= ($i ? '
' : '') . $label($group['frequency'] === '' ? 'Up front' : $group['label']) . '
' . $money($group['total']) . '';
}
} elseif ($type === 'quote') {
$amount_html = $label('Quote total') . '
' . $money($totals['total']) . '';
} else {
$amount_html = $label('Each ' . recurringInvoicePeriodWord($row['recurring_invoice_frequency'])) . '
' . $money($totals['total']) . '';
}
$html = '
' . $label($to_label) . ' ' . $e($bill_to['client_name'] ?? $row['client_name']) . ' ' . nl2br($e(implode("\n", $to_lines))) . ' |
' . $amount_html . ' |
';
$pdf->writeHTML($html, true, false, true, false, '');
// A stamp across a document that is done with
$stamp = null;
if (!$packing) {
if ($type === 'invoice' && $status === 'Paid') {
$stamp = ['PAID', [25, 135, 84]];
} elseif ($type === 'invoice' && $status === 'Cancelled') {
$stamp = ['CANCELLED', [220, 53, 69]];
} elseif ($type === 'quote' && in_array($status, ['Accepted', 'Invoiced'], true)) {
$stamp = ['ACCEPTED', [25, 135, 84]];
} elseif ($type === 'quote' && $status === 'Declined') {
$stamp = ['DECLINED', [220, 53, 69]];
}
}
if ($stamp) {
$y = $pdf->GetY();
$pdf->SetAlpha(0.28);
$pdf->StartTransform();
$pdf->Rotate(12, $margins['left'] + $width / 2, 21);
$pdf->SetFont('dejavusans', 'B', 20);
$pdf->SetTextColor(...$stamp[1]);
$pdf->SetDrawColor(...$stamp[1]);
$pdf->SetLineWidth(0.8);
$stamp_width = $pdf->GetStringWidth($stamp[0]) + 8;
$pdf->SetXY($margins['left'] + $width / 2 - $stamp_width / 2, 15.5);
$pdf->Cell($stamp_width, 11, $stamp[0], 1, 0, 'C');
$pdf->StopTransform();
$pdf->SetAlpha(1);
$pdf->SetDrawColor(222, 226, 230);
$pdf->SetLineWidth(0.2);
$pdf->SetTextColor(33, 37, 41);
$pdf->SetXY($margins['left'], $y);
}
$pdf->SetFont('dejavusans', '', 8.5);
if (trim((string) $row["{$c}_scope"]) !== '') {
$pdf->Ln(1);
$pdf->writeHTML('| '
. $label('For') . ' ' . $e($row["{$c}_scope"]) . ' |
', true, false, true, false, '');
}
$pdf->Ln(2);
// The lines
$th = 'font-size:6.5pt; color:' . $grey . '; font-weight:bold; border-bottom:0.6px solid #adb5bd;';
$td = 'border-bottom:0.3px solid ' . $line . ';';
if ($packing) {
// The serials of hardware already on the client's assets, under its line
$serials = [];
$sql = mysqli_query($mysqli, "SELECT asset_invoice_item_id, asset_serial FROM assets
WHERE asset_invoice_id = $id AND asset_invoice_item_id > 0 AND COALESCE(asset_serial, '') <> '' ORDER BY asset_name ASC, asset_id ASC");
while ($serial = mysqli_fetch_assoc($sql)) {
$serials[intval($serial['asset_invoice_item_id'])][] = $serial['asset_serial'];
}
$html = '
| ITEM | QTY | PACKED |
';
foreach ($items as $item) {
$item_serials = $serials[intval($item['item_id'])] ?? [];
$html .= '' . $e($item['item_name']) . ''
. (trim((string) $item['item_description']) !== '' ? ' ' . nl2br($e($item['item_description'])) . '' : '')
. ($item_serials ? ' ' . (count($item_serials) === 1 ? 'Serial' : 'Serials') . ': ' . $e(implode(', ', $item_serials)) . '' : '') . ' |
' . $e(billingQuantity($item['item_quantity'])) . ' |
☐ |
';
}
$html .= '
';
} else {
$html = '
| ITEM | QTY |
PRICE | TAX |
AMOUNT |
';
$line_groups = $groups ?: ['' => ['frequency' => '', 'label' => '', 'items' => $items]];
foreach ($line_groups as $line_group) {
if ($groups) {
$html .= ' ' . $label($line_group['label']) . ' |
';
}
foreach ($line_group['items'] as $item) {
$tax_label = floatval($item['item_tax']) != 0 || intval($item['item_tax_id'])
? '
' . $e(trim(($item['tax_name'] ?? '') . ' ' . ($item['tax_percent'] !== null ? taxPercentText($item['tax_percent']) . '%' : ''))) . ''
: '';
$html .= '' . $e($item['item_name']) . ''
. (trim((string) $item['item_description']) !== '' ? ' ' . nl2br($e($item['item_description'])) . '' : '') . ' |
' . $e(billingQuantity($item['item_quantity'])) . ' |
' . $money($item['item_price']) . ' |
' . ($tax_label !== '' ? $money($item['item_tax']) . $tax_label : '-') . ' |
' . $money($item['item_total']) . ' |
';
}
}
if (!$items) {
$html .= '| Nothing on it yet. |
';
}
$html .= '
';
}
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->Ln(3);
if ($packing) {
// Who packed it, and who took it
$sign = '
' . $label('Packed by') . '
Name, signature and date |
|
' . $label('Received by') . '
Name, signature and date |
';
$pdf->writeHTML($sign, true, false, true, false, '');
} else {
// Notes, and what it all comes to
$rows = '| Subtotal | ' . $money($totals['subtotal']) . ' |
';
if ($shown_groups) {
// A block for each - what is billed once, and each month, quarter or year
// (widths set on each row - "Total each quarter" needs more than half)
$rows = '';
$w = ' width="60%"';
$wr = ' width="40%"';
foreach (array_values($shown_groups) as $i => $group) {
if ($i) {
$rows .= '| |
';
}
$rows .= '| ' . $label($group['label']) . ' |
';
$rows .= '| Subtotal | ' . $money($group['subtotal']) . ' |
';
if ($group['discount'] != 0) {
$rows .= '| Discount | -' . $money($group['discount']) . ' |
';
}
foreach ($group['taxes'] as $tax_label => $tax_amount) {
$rows .= '| ' . $e($tax_label) . ' | ' . $money($tax_amount) . ' |
';
}
$rows .= '| ' . ($group['frequency'] === '' ? 'Total up front' : 'Total ' . $e(strtolower($group['label']))) . ' | ' . $money($group['total']) . ' |
';
}
}
if (!$shown_groups) {
if ($totals['discount'] != 0) {
$rows .= '| Discount | -' . $money($totals['discount']) . ' |
';
}
foreach ($totals['taxes'] as $tax_label => $tax_amount) {
$rows .= '| ' . $e($tax_label) . ' | ' . $money($tax_amount) . ' |
';
}
if ($totals['credit'] > 0) {
$rows .= '| Credit | -' . $money($totals['credit']) . ' |
';
}
$rows .= '| Total | ' . $money($totals['total']) . ' |
';
}
if ($type === 'invoice' && $state['paid'] > 0) {
$rows .= '| Paid | -' . $money($state['paid']) . ' |
';
$rows .= '| Balance due | ' . $money($state['balance']) . ' |
';
}
$note = trim((string) $row["{$c}_note"]);
$html = '
' . ($note !== '' ? $label('Notes') . ' ' . nl2br($e($note)) . '' : '') . ' |
|
|
';
$pdf->writeHTML($html, true, false, true, false, '');
// Open it online - pay an invoice still owed, answer a quote still waiting
$online = null;
if ($type === 'invoice' && in_array($status, invoiceOpenStatuses(), true) && $state['balance'] > 0) {
$online = [billingOnlinePayments() ? 'Pay online' : 'View online', billingOnlinePayments() ? 'Scan to open this invoice and pay it by card.' : 'Scan to open this invoice online.'];
} elseif ($type === 'quote' && in_array($status, ['Sent', 'Viewed'], true) && !quoteExpired($row)) {
$online = ['Answer online', 'Scan to open this quote and accept or decline it.'];
}
if ($online) {
$url = billingGuestUrl($type, $row);
if ($pdf->GetY() > $pdf->getPageHeight() - 60) {
$pdf->AddPage();
}
$pdf->Ln(4);
$y = $pdf->GetY();
$pdf->SetFillColor(241, 243, 245);
$pdf->RoundedRect($margins['left'], $y, $width, 28, 2, '1111', 'F');
$pdf->write2DBarcode($url, 'QRCODE,M', $margins['left'] + 3, $y + 3, 22, 22, ['border' => false, 'padding' => 0, 'fgcolor' => [33, 37, 41], 'bgcolor' => false], 'N');
$pdf->SetXY($margins['left'] + 30, $y + 6);
$pdf->SetFont('dejavusans', 'B', 10);
$pdf->Cell($width - 34, 5, $online[0], 0, 2, 'L');
$pdf->SetFont('dejavusans', '', 7.5);
$pdf->SetTextColor(108, 117, 125);
$pdf->MultiCell($width - 34, 4, $online[1] . "\n" . $url, 0, 'L', false, 1, $margins['left'] + 30);
$pdf->SetTextColor(33, 37, 41);
$pdf->SetY($y + 30);
}
$footer = trim((string) ($type === 'quote' ? $config_quote_footer : $config_invoice_footer));
if ($footer !== '') {
$pdf->Ln(4);
$pdf->SetFont('dejavusans', '', 7.5);
$pdf->SetTextColor(108, 117, 125);
$pdf->MultiCell($width, 4, $footer, 0, 'C', false, 1);
}
}
$date = $type === 'recurring' ? date('Y-m-d') : (string) $row["{$c}_date"];
$name = preg_replace('/[^A-Za-z0-9_\-]+/', '_', trim("{$date}_{$row['client_name']}_" . str_replace(' ', '_', $title) . "_$number", '_')) . '.pdf';
return ['name' => $name, 'data' => $pdf->Output($name, 'S')];
}
/**
* A client's statement as a PDF - billingStatement() - in the same look:
* who it is from and for, the date and period, what is owed and how late,
* then each invoice with what was billed, paid and is left. ['name', 'data'].
*/
function billingStatementPdfRender(array $statement) {
global $config_invoice_footer;
billingPdfConfig();
$company = billingCompany();
$client = $statement['client'];
$bill_to = billingBillTo($client['client_id']);
$currency = $statement['currency'];
$e = function ($value) {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
};
$money = function ($amount) use ($currency, $e) {
return $e(billingMoney($amount, $currency));
};
$grey = '#6c757d';
$red = '#c42b37';
$label = function ($text) use ($grey, $e) {
return '' . $e(strtoupper($text)) . '';
};
$format = in_array($company['company_country'] ?? '', ['United States', 'USA', 'US', 'Canada'], true) ? 'LETTER' : 'A4';
$pdf = new BillingPdf('P', 'mm', $format, true, 'UTF-8', false);
$pdf->SetCreator('ITFlow');
$pdf->SetAuthor((string) ($company['company_name'] ?? ''));
$pdf->SetTitle('Statement ' . $client['client_name']);
$pdf->SetMargins(16, 14, 16);
$pdf->SetHeaderMargin(0);
$pdf->SetFooterMargin(12);
$pdf->SetAutoPageBreak(true, 20);
$pdf->setFontSubsetting(true);
$pdf->setImageScale(1.5);
$pdf->footer_label = trim(($company['company_name'] ?? '') . ' · Statement · ' . $client['client_name'], ' ·');
$pdf->AddPage();
$margins = $pdf->getMargins();
$width = $pdf->getPageWidth() - $margins['left'] - $margins['right'];
// Who it is from, and what it is
$from_lines = array_filter([
formatAddress($company['company_address'] ?? '', $company['company_city'] ?? '', $company['company_state'] ?? '', $company['company_zip'] ?? '', $company['company_country'] ?? '', "\n"),
implode(' · ', array_filter([$company['company_email'] ?? '', $company['phone_display'] ?? ''])),
$company['company_website'] ?? '',
]);
$logo = !empty($company['logo_path']) ? ' . ')
' : '';
$dates = ['Statement date' => date('Y-m-d'), 'Period' => $statement['from'] !== '' ? $statement['from'] . ' to ' . $statement['to'] : 'Up to ' . $statement['to']];
$dates_html = '';
foreach ($dates as $date_label => $value) {
$dates_html .= '| ' . $e($date_label) . ' | ' . $e($value) . ' |
';
}
$html = '
' . $logo . '' . $e($company['company_name'] ?? '') . ' ' . nl2br($e(implode("\n", $from_lines))) . ' |
STATEMENT ' . $e($client['client_name']) . '
|
';
$pdf->SetFont('dejavusans', '', 8.5);
$pdf->SetTextColor(33, 37, 41);
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->SetDrawColor(222, 226, 230);
$pdf->SetLineWidth(0.2);
$pdf->Ln(1);
$pdf->Line($margins['left'], $pdf->GetY(), $margins['left'] + $width, $pdf->GetY());
$pdf->Ln(4);
// Who it is for, and what is owed
$to_lines = array_filter([
formatAddress($bill_to['location_address'] ?? '', $bill_to['location_city'] ?? '', $bill_to['location_state'] ?? '', $bill_to['location_zip'] ?? '', $bill_to['location_country'] ?? '', "\n"),
$bill_to['contact_name'] ?? '',
implode(' · ', array_filter([$bill_to['contact_email'] ?? '', $bill_to['contact_phone'] ?? ''])),
]);
$html = '
' . $label('Statement for') . ' ' . $e($bill_to['client_name'] ?? $client['client_name']) . ' ' . nl2br($e(implode("\n", $to_lines))) . ' |
' . $label('Total due') . ' ' . $money($statement['total']) . ''
. ($statement['overdue'] > 0 ? ' ' . $money($statement['overdue']) . ' of it overdue' : '') . ' |
';
$pdf->writeHTML($html, true, false, true, false, '');
$pdf->Ln(2);
// How late it is
$cells = '';
foreach ($statement['aging'] as $bucket => $amount) {
$late = $bucket !== 'Current' && $amount > 0;
$cells .= '' . $label($bucket) . ' ' . $money($amount) . ' | ';
}
$pdf->writeHTML('', true, false, true, false, '');
$pdf->Ln(3);
// Each invoice
$th = 'font-size:6.5pt; color:' . $grey . '; font-weight:bold; border-bottom:0.6px solid #adb5bd;';
$td = 'border-bottom:0.3px solid #dee2e6;';
$html = '
| DATE | INVOICE | DUE |
TOTAL | PAID | BALANCE |
';
foreach ($statement['invoices'] as $invoice) {
$late = $invoice['balance'] > 0 && $invoice['days'] > 0;
$html .= '| ' . $e($invoice['invoice_date']) . ' |
' . $e(billingNumber('invoice', $invoice)) . '' . (trim((string) $invoice['invoice_scope']) !== '' ? ' ' . $e($invoice['invoice_scope']) . '' : '') . ' |
' . $e($invoice['invoice_due']) . ($late ? ' ' . intval($invoice['days']) . ' days late' : '') . ' |
' . $money($invoice['invoice_amount']) . ' |
' . (floatval($invoice['paid']) > 0 ? $money($invoice['paid']) : '-') . ' |
' . ($invoice['balance'] > 0 ? $money($invoice['balance']) : 'Paid') . ' |
';
}
if (!$statement['invoices']) {
$html .= '| Nothing owed in this period. |
';
}
$html .= '| Total due | ' . $money($statement['total']) . ' |
';
$pdf->writeHTML($html, true, false, true, false, '');
$footer = trim((string) $config_invoice_footer);
if ($footer !== '') {
$pdf->Ln(4);
$pdf->SetFont('dejavusans', '', 7.5);
$pdf->SetTextColor(108, 117, 125);
$pdf->MultiCell($width, 4, $footer, 0, 'C', false, 1);
}
$name = preg_replace('/[^A-Za-z0-9_\-]+/', '_', trim($statement['to'] . '_' . $client['client_name'] . '_Statement', '_')) . '.pdf';
return ['name' => $name, 'data' => $pdf->Output($name, 'S')];
}