| 1 |
<?php |
| 2 |
/** |
| 3 |
* Shared invoice/quote layout for server-side PDF rendering. |
| 4 |
* |
| 5 |
* Receives from PdfRenderer::buildHtml(), via invoice.php or quote.php: |
| 6 |
* $invoice — Invoice or Quote model (named for the invoice case, which came first) |
| 7 |
* $formatter — currency formatter with format() |
| 8 |
* $ei_pdf_type — 'invoice' or 'quote' |
| 9 |
* |
| 10 |
* One body for both documents. The plugin already carries ~6,500 lines of |
| 11 |
* near-identical parallel invoice and quote templates, and every fix to one of |
| 12 |
* them is a fix someone forgets to make to the other. The two differ only in |
| 13 |
* four labels and which date they show, so they differ here in four labels and |
| 14 |
* which date they show. |
| 15 |
* |
| 16 |
* Laid out with tables on purpose. dompdf implements CSS 2.1 and ignores |
| 17 |
* `display: flex`, so the flexbox designs in templates/invoice-templates/ |
| 18 |
* collapse into stacked rows when rendered here. Tables are the one layout |
| 19 |
* primitive every PDF engine agrees on. |
| 20 |
* |
| 21 |
* Override with the `easy_invoice_pdf_template_path` filter. |
| 22 |
* |
| 23 |
* @package Easy_Invoice |
| 24 |
*/ |
| 25 |
|
| 26 |
if ( ! defined( 'ABSPATH' ) ) { |
| 27 |
exit; |
| 28 |
} |
| 29 |
|
| 30 |
$ei_pdf_type = ( isset( $ei_pdf_type ) && 'quote' === $ei_pdf_type ) ? 'quote' : 'invoice'; |
| 31 |
$ei_is_quote = 'quote' === $ei_pdf_type; |
| 32 |
$ei_is_credit = ! $ei_is_quote && \EasyInvoice\Helpers\TemplateTextHelper::isCreditNote( $invoice ); |
| 33 |
|
| 34 |
$text_settings = $ei_is_quote |
| 35 |
? \EasyInvoice\Helpers\TemplateTextHelper::getQuoteTextSettings() |
| 36 |
: \EasyInvoice\Helpers\TemplateTextHelper::getInvoiceTextSettings(); |
| 37 |
$company_info = \EasyInvoice\Helpers\TemplateTextHelper::getCompanyInfo(); |
| 38 |
|
| 39 |
$t = static function ( $key, $fallback ) use ( $text_settings ) { |
| 40 |
return ! empty( $text_settings[ $key ] ) ? $text_settings[ $key ] : $fallback; |
| 41 |
}; |
| 42 |
|
| 43 |
$items = is_callable( [ $invoice, 'getItems' ] ) ? (array) $invoice->getItems() : []; |
| 44 |
$subtotal = is_callable( [ $invoice, 'getSubtotal' ] ) ? (float) $invoice->getSubtotal() : 0.0; |
| 45 |
$tax = is_callable( [ $invoice, 'getTaxAmount' ] ) ? (float) $invoice->getTaxAmount() : 0.0; |
| 46 |
$discount = is_callable( [ $invoice, 'getDiscountAmount' ] ) ? (float) $invoice->getDiscountAmount() : 0.0; |
| 47 |
$total = is_callable( [ $invoice, 'getTotal' ] ) ? (float) $invoice->getTotal() : 0.0; |
| 48 |
?> |
| 49 |
<!DOCTYPE html> |
| 50 |
<html> |
| 51 |
<head> |
| 52 |
<meta charset="utf-8"> |
| 53 |
<style> |
| 54 |
@page { margin: 18mm 15mm; } |
| 55 |
|
| 56 |
body { |
| 57 |
font-family: "DejaVu Sans", sans-serif; |
| 58 |
font-size: 10pt; |
| 59 |
line-height: 1.45; |
| 60 |
color: #222222; |
| 61 |
margin: 0; |
| 62 |
} |
| 63 |
|
| 64 |
table { width: 100%; border-collapse: collapse; } |
| 65 |
td, th { vertical-align: top; } |
| 66 |
|
| 67 |
/* Masthead: company on the left, document meta on the right. A table, |
| 68 |
because this is exactly the two-column arrangement flex would have |
| 69 |
given us on screen and cannot give us here. */ |
| 70 |
.masthead td { padding: 0 0 18pt 0; } |
| 71 |
.masthead .company-name { font-size: 15pt; font-weight: bold; color: #111111; } |
| 72 |
.masthead .company-detail { font-size: 8.5pt; color: #555555; } |
| 73 |
.masthead .doc-type { |
| 74 |
font-size: 20pt; font-weight: bold; color: #111111; |
| 75 |
text-align: right; letter-spacing: 1pt; |
| 76 |
} |
| 77 |
.masthead .doc-meta { text-align: right; font-size: 9pt; color: #555555; } |
| 78 |
.masthead .doc-meta strong { color: #222222; } |
| 79 |
|
| 80 |
.rule { border-bottom: 1px solid #d8d8d8; height: 1px; margin-bottom: 14pt; } |
| 81 |
|
| 82 |
.parties td { padding-bottom: 16pt; width: 50%; } |
| 83 |
.label { |
| 84 |
font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.8pt; |
| 85 |
color: #888888; padding-bottom: 3pt; |
| 86 |
} |
| 87 |
.party-name { font-weight: bold; font-size: 10.5pt; } |
| 88 |
.party-detail { font-size: 9pt; color: #555555; } |
| 89 |
|
| 90 |
/* Line items */ |
| 91 |
.items { margin-bottom: 14pt; } |
| 92 |
.items thead th { |
| 93 |
font-size: 7.5pt; text-transform: uppercase; letter-spacing: 0.6pt; |
| 94 |
color: #666666; text-align: left; font-weight: normal; |
| 95 |
border-bottom: 1px solid #cccccc; padding: 0 6pt 6pt 0; |
| 96 |
} |
| 97 |
.items tbody td { |
| 98 |
padding: 7pt 6pt 7pt 0; |
| 99 |
border-bottom: 1px solid #eeeeee; |
| 100 |
font-size: 9.5pt; |
| 101 |
} |
| 102 |
.items .num { text-align: right; padding-right: 0; } |
| 103 |
.items .item-name { font-weight: bold; } |
| 104 |
.items .item-desc { font-size: 8.5pt; color: #666666; } |
| 105 |
|
| 106 |
/* Totals sit in a right-aligned block; a nested table keeps the |
| 107 |
label/value pairs aligned without floats, which dompdf handles poorly. */ |
| 108 |
.totals { width: 46%; margin-left: 54%; } |
| 109 |
.totals td { padding: 4pt 0; font-size: 9.5pt; } |
| 110 |
.totals td.v { text-align: right; } |
| 111 |
.totals tr.grand td { |
| 112 |
border-top: 1.5px solid #222222; |
| 113 |
padding-top: 7pt; font-size: 12pt; font-weight: bold; color: #111111; |
| 114 |
} |
| 115 |
|
| 116 |
.blocks { margin-top: 20pt; } |
| 117 |
.blocks td { padding-bottom: 12pt; font-size: 8.8pt; color: #555555; } |
| 118 |
.blocks .label { color: #888888; } |
| 119 |
|
| 120 |
.footer { |
| 121 |
margin-top: 24pt; padding-top: 10pt; |
| 122 |
border-top: 1px solid #e4e4e4; |
| 123 |
font-size: 8pt; color: #888888; text-align: center; |
| 124 |
} |
| 125 |
</style> |
| 126 |
</head> |
| 127 |
<body> |
| 128 |
|
| 129 |
<table class="masthead"> |
| 130 |
<tr> |
| 131 |
<td style="width:55%"> |
| 132 |
<?php if ( ! empty( $company_info['logo'] ) ) : ?> |
| 133 |
<img src="<?php echo esc_url( $company_info['logo'] ); ?>" style="max-height:52pt; margin-bottom:6pt;" alt=""> |
| 134 |
<br> |
| 135 |
<?php endif; ?> |
| 136 |
<?php if ( ! empty( $company_info['name'] ) ) : ?> |
| 137 |
<div class="company-name"><?php echo esc_html( $company_info['name'] ); ?></div> |
| 138 |
<?php endif; ?> |
| 139 |
<?php foreach ( [ 'address', 'email', 'phone', 'website' ] as $field ) : ?> |
| 140 |
<?php if ( ! empty( $company_info[ $field ] ) ) : ?> |
| 141 |
<div class="company-detail"><?php echo nl2br( esc_html( $company_info[ $field ] ) ); ?></div> |
| 142 |
<?php endif; ?> |
| 143 |
<?php endforeach; ?> |
| 144 |
<?php if ( ! empty( $company_info['tax_number'] ) ) : ?> |
| 145 |
<div class="company-detail"><?php echo esc_html( $company_info['tax_number'] ); ?></div> |
| 146 |
<?php endif; ?> |
| 147 |
</td> |
| 148 |
<td style="width:45%"> |
| 149 |
<div class="doc-type"><?php |
| 150 |
echo esc_html( $ei_is_quote |
| 151 |
? $t( 'quote_title', __( 'QUOTE', 'easy-invoice' ) ) |
| 152 |
: ( $ei_is_credit ? __( 'CREDIT NOTE', 'easy-invoice' ) : $t( 'invoice_title', __( 'INVOICE', 'easy-invoice' ) ) ) ); |
| 153 |
?></div> |
| 154 |
<div class="doc-meta"> |
| 155 |
<?php if ( is_callable( [ $invoice, 'getNumber' ] ) && $invoice->getNumber() ) : ?> |
| 156 |
<div><?php echo esc_html( $ei_is_quote |
| 157 |
? $t( 'quote_number', __( 'Quote No', 'easy-invoice' ) ) |
| 158 |
: ( $ei_is_credit ? __( 'Credit Note No', 'easy-invoice' ) : $t( 'invoice_number', __( 'Invoice No', 'easy-invoice' ) ) ) ); ?>: |
| 159 |
<strong><?php echo esc_html( $invoice->getNumber() ); ?></strong></div> |
| 160 |
<?php endif; ?> |
| 161 |
<?php if ( is_callable( [ $invoice, 'getIssueDate' ] ) && $invoice->getIssueDate() ) : ?> |
| 162 |
<div><?php echo esc_html( $ei_is_credit ? __( 'Date', 'easy-invoice' ) : $t( 'invoice_date', __( 'Date', 'easy-invoice' ) ) ); ?>: |
| 163 |
<strong><?php echo esc_html( $invoice->getIssueDate() ); ?></strong></div> |
| 164 |
<?php endif; ?> |
| 165 |
<?php |
| 166 |
// A quote expires; an invoice falls due. Different field, different word. |
| 167 |
$ei_end_date = $ei_is_quote |
| 168 |
? ( is_callable( [ $invoice, 'getExpiryDate' ] ) ? $invoice->getExpiryDate() : '' ) |
| 169 |
: ( ( ! $ei_is_credit && is_callable( [ $invoice, 'getDueDate' ] ) ) ? $invoice->getDueDate() : '' ); |
| 170 |
?> |
| 171 |
<?php if ( $ei_end_date ) : ?> |
| 172 |
<div><?php echo esc_html( $ei_is_quote |
| 173 |
? $t( 'valid_until', __( 'Valid until', 'easy-invoice' ) ) |
| 174 |
: $t( 'due_date', __( 'Due', 'easy-invoice' ) ) ); ?>: |
| 175 |
<strong><?php echo esc_html( $ei_end_date ); ?></strong></div> |
| 176 |
<?php endif; ?> |
| 177 |
</div> |
| 178 |
</td> |
| 179 |
</tr> |
| 180 |
</table> |
| 181 |
|
| 182 |
<div class="rule"></div> |
| 183 |
|
| 184 |
<table class="parties"> |
| 185 |
<tr> |
| 186 |
<td> |
| 187 |
<div class="label"><?php echo esc_html( $t( 'to', $ei_is_quote |
| 188 |
? __( 'Quote for', 'easy-invoice' ) |
| 189 |
: __( 'Bill to', 'easy-invoice' ) ) ); ?></div> |
| 190 |
<?php if ( is_callable( [ $invoice, 'getCustomerName' ] ) && $invoice->getCustomerName() ) : ?> |
| 191 |
<div class="party-name"><?php echo esc_html( $invoice->getCustomerName() ); ?></div> |
| 192 |
<?php endif; ?> |
| 193 |
<?php if ( is_callable( [ $invoice, 'getCustomerAddress' ] ) && $invoice->getCustomerAddress() ) : ?> |
| 194 |
<div class="party-detail"><?php echo nl2br( esc_html( $invoice->getCustomerAddress() ) ); ?></div> |
| 195 |
<?php endif; ?> |
| 196 |
<?php if ( is_callable( [ $invoice, 'getCustomerEmail' ] ) && $invoice->getCustomerEmail() ) : ?> |
| 197 |
<div class="party-detail"><?php echo esc_html( $invoice->getCustomerEmail() ); ?></div> |
| 198 |
<?php endif; ?> |
| 199 |
</td> |
| 200 |
<td> |
| 201 |
<?php if ( is_callable( [ $invoice, 'getTitle' ] ) && $invoice->getTitle() ) : ?> |
| 202 |
<div class="label"><?php esc_html_e( 'Reference', 'easy-invoice' ); ?></div> |
| 203 |
<div class="party-detail"><?php echo esc_html( $invoice->getTitle() ); ?></div> |
| 204 |
<?php endif; ?> |
| 205 |
</td> |
| 206 |
</tr> |
| 207 |
</table> |
| 208 |
|
| 209 |
<table class="items"> |
| 210 |
<thead> |
| 211 |
<tr> |
| 212 |
<th style="width:52%"><?php echo esc_html( $t( 'service', __( 'Description', 'easy-invoice' ) ) ); ?></th> |
| 213 |
<th style="width:12%" class="num"><?php echo esc_html( $t( 'qty', __( 'Qty', 'easy-invoice' ) ) ); ?></th> |
| 214 |
<th style="width:18%" class="num"><?php echo esc_html( $t( 'rate_price', __( 'Rate', 'easy-invoice' ) ) ); ?></th> |
| 215 |
<th style="width:18%" class="num"><?php echo esc_html( $t( 'line_total', $t( 'total', __( 'Amount', 'easy-invoice' ) ) ) ); ?></th> |
| 216 |
</tr> |
| 217 |
</thead> |
| 218 |
<tbody> |
| 219 |
<?php if ( empty( $items ) ) : ?> |
| 220 |
<tr><td colspan="4" style="color:#888888"><?php esc_html_e( 'No line items.', 'easy-invoice' ); ?></td></tr> |
| 221 |
<?php else : ?> |
| 222 |
<?php foreach ( $items as $item ) : |
| 223 |
$name = is_callable( [ $item, 'getName' ] ) ? (string) $item->getName() : ''; |
| 224 |
$desc = is_callable( [ $item, 'getDescription' ] ) ? (string) $item->getDescription() : ''; |
| 225 |
$qty = is_callable( [ $item, 'getQuantity' ] ) ? (float) $item->getQuantity() : 0.0; |
| 226 |
$rate = is_callable( [ $item, 'getPrice' ] ) ? (float) $item->getPrice() : 0.0; |
| 227 |
$amt = is_callable( [ $item, 'getAmount' ] ) ? (float) $item->getAmount() : ( $qty * $rate ); |
| 228 |
?> |
| 229 |
<tr> |
| 230 |
<td> |
| 231 |
<?php if ( $name !== '' ) : ?><div class="item-name"><?php echo esc_html( $name ); ?></div><?php endif; ?> |
| 232 |
<?php if ( $desc !== '' ) : ?><div class="item-desc"><?php echo nl2br( esc_html( $desc ) ); ?></div><?php endif; ?> |
| 233 |
</td> |
| 234 |
<td class="num"><?php echo esc_html( rtrim( rtrim( number_format( $qty, 2 ), '0' ), '.' ) ); ?></td> |
| 235 |
<td class="num"><?php echo esc_html( $formatter->format( $rate ) ); ?></td> |
| 236 |
<td class="num"><?php echo esc_html( $formatter->format( $amt ) ); ?></td> |
| 237 |
</tr> |
| 238 |
<?php endforeach; ?> |
| 239 |
<?php endif; ?> |
| 240 |
</tbody> |
| 241 |
</table> |
| 242 |
|
| 243 |
<table class="totals"> |
| 244 |
<tr> |
| 245 |
<td><?php echo esc_html( $t( 'sub_total', __( 'Subtotal', 'easy-invoice' ) ) ); ?></td> |
| 246 |
<td class="v"><?php echo esc_html( $formatter->format( $subtotal ) ); ?></td> |
| 247 |
</tr> |
| 248 |
<?php if ( $discount > 0 ) : ?> |
| 249 |
<tr> |
| 250 |
<td><?php echo esc_html( $t( 'discount', __( 'Discount', 'easy-invoice' ) ) ); ?></td> |
| 251 |
<td class="v">-<?php echo esc_html( $formatter->format( $discount ) ); ?></td> |
| 252 |
</tr> |
| 253 |
<?php endif; ?> |
| 254 |
<?php if ( $tax > 0 ) : ?> |
| 255 |
<tr> |
| 256 |
<td><?php echo esc_html( $t( 'tax', __( 'Tax', 'easy-invoice' ) ) ); ?></td> |
| 257 |
<td class="v"><?php echo esc_html( $formatter->format( $tax ) ); ?></td> |
| 258 |
</tr> |
| 259 |
<?php endif; ?> |
| 260 |
<?php |
| 261 |
/** |
| 262 |
* Extra total lines — the hook the Additional Tax addon renders into on screen, |
| 263 |
* so a PDF shows the same figures a customer sees in the browser. |
| 264 |
*/ |
| 265 |
do_action( 'easy_invoice_pdf_totals_after_tax', $invoice ); |
| 266 |
?> |
| 267 |
<tr class="grand"> |
| 268 |
<td><?php echo esc_html( $t( 'total', __( 'Total', 'easy-invoice' ) ) ); ?></td> |
| 269 |
<td class="v"><?php echo esc_html( $formatter->format( $total ) ); ?></td> |
| 270 |
</tr> |
| 271 |
<?php |
| 272 |
// Money already received and credit notes issued: the total stays the |
| 273 |
// total, and what is still owed is spelled out underneath it. |
| 274 |
$ei_pdf_id = ( ! $ei_is_quote && is_callable( [ $invoice, 'getId' ] ) ) ? (int) $invoice->getId() : 0; |
| 275 |
$ei_pdf_paid = $ei_pdf_id ? \EasyInvoice\Services\InvoiceBalance::paid( $ei_pdf_id ) : 0.0; |
| 276 |
$ei_pdf_credited = $ei_pdf_id ? \EasyInvoice\Services\InvoiceBalance::credited( $ei_pdf_id ) : 0.0; |
| 277 |
if ( $ei_pdf_paid > 0 || $ei_pdf_credited > 0 ) : |
| 278 |
foreach ( \EasyInvoice\Services\CreditNote::forInvoice( $ei_pdf_id ) as $ei_credit_id ) : |
| 279 |
$ei_credit_amount = (float) get_post_meta( $ei_credit_id, '_easy_invoice_total', true ); |
| 280 |
if ( $ei_credit_amount <= 0 ) { |
| 281 |
continue; |
| 282 |
} |
| 283 |
?> |
| 284 |
<tr> |
| 285 |
<td><?php /* translators: %s: credit note number. */ echo esc_html( sprintf( __( 'Credit note %s', 'easy-invoice' ), (string) get_post_meta( $ei_credit_id, '_easy_invoice_number', true ) ) ); ?></td> |
| 286 |
<td class="v"><?php echo esc_html( '-' . $formatter->format( $ei_credit_amount ) ); ?></td> |
| 287 |
</tr> |
| 288 |
<?php endforeach; ?> |
| 289 |
<?php if ( $ei_pdf_paid > 0 ) : ?> |
| 290 |
<tr> |
| 291 |
<td><?php esc_html_e( 'Paid', 'easy-invoice' ); ?></td> |
| 292 |
<td class="v"><?php echo esc_html( '-' . $formatter->format( $ei_pdf_paid ) ); ?></td> |
| 293 |
</tr> |
| 294 |
<?php endif; ?> |
| 295 |
<tr class="grand"> |
| 296 |
<td><?php esc_html_e( 'Balance due', 'easy-invoice' ); ?></td> |
| 297 |
<td class="v"><?php echo esc_html( $formatter->format( max( 0, $total - $ei_pdf_paid - $ei_pdf_credited ) ) ); ?></td> |
| 298 |
</tr> |
| 299 |
<?php endif; ?> |
| 300 |
</table> |
| 301 |
|
| 302 |
<table class="blocks"> |
| 303 |
<?php if ( is_callable( [ $invoice, 'getNotes' ] ) && $invoice->getNotes() ) : ?> |
| 304 |
<tr><td> |
| 305 |
<div class="label"><?php esc_html_e( 'Notes', 'easy-invoice' ); ?></div> |
| 306 |
<?php echo nl2br( esc_html( $invoice->getNotes() ) ); ?> |
| 307 |
</td></tr> |
| 308 |
<?php endif; ?> |
| 309 |
<?php if ( ! $ei_is_credit && is_callable( [ $invoice, 'getTerms' ] ) && $invoice->getTerms() ) : ?> |
| 310 |
<tr><td> |
| 311 |
<div class="label"><?php esc_html_e( 'Terms', 'easy-invoice' ); ?></div> |
| 312 |
<?php echo nl2br( esc_html( $invoice->getTerms() ) ); ?> |
| 313 |
</td></tr> |
| 314 |
<?php endif; ?> |
| 315 |
</table> |
| 316 |
|
| 317 |
<?php |
| 318 |
/** |
| 319 |
* Fires after notes and terms on the PDF, before the footer. |
| 320 |
* |
| 321 |
* @param object $invoice The document (invoice or quote model). |
| 322 |
* @param string $ei_pdf_type 'invoice' or 'quote'. |
| 323 |
*/ |
| 324 |
do_action( 'easy_invoice_pdf_after_notes', $invoice, $ei_pdf_type ); |
| 325 |
?> |
| 326 |
<?php $ei_pdf_footer = \EasyInvoice\Helpers\TemplateTextHelper::footerText( $invoice, $ei_pdf_type ); ?> |
| 327 |
<?php if ( '' !== trim( wp_strip_all_tags( $ei_pdf_footer ) ) ) : ?> |
| 328 |
<div class="footer"><?php echo wp_kses_post( $ei_pdf_footer ); ?></div> |
| 329 |
<?php endif; ?> |
| 330 |
|
| 331 |
</body> |
| 332 |
</html> |
| 333 |
|