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) . '
' . $dates_html . '
'; $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 = ''; foreach ($items as $item) { $item_serials = $serials[intval($item['item_id'])] ?? []; $html .= ''; } $html .= '
ITEMQTYPACKED
' . $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'])) . ' ☐
'; } else { $html = ''; $line_groups = $groups ?: ['' => ['frequency' => '', 'label' => '', 'items' => $items]]; foreach ($line_groups as $line_group) { if ($groups) { $html .= ''; } 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 .= ''; } } if (!$items) { $html .= ''; } $html .= '
ITEMQTY PRICETAX AMOUNT

' . $label($line_group['label']) . '
' . $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']) . '
Nothing on it yet.
'; } $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)) . '' : '') . '
' . $rows . '
'; $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']) . '
' . $dates_html . '
'; $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('' . $cells . '
', 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 = ''; foreach ($statement['invoices'] as $invoice) { $late = $invoice['balance'] > 0 && $invoice['days'] > 0; $html .= ''; } if (!$statement['invoices']) { $html .= ''; } $html .= '
DATEINVOICEDUE TOTALPAIDBALANCE
' . $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') . '
Nothing owed in this period.
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')]; }