PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
← All changes | includes/Helpers/TemplateTextHelper.php +259 -21 2.2.02.4.1 View file →
@@ -18,8 +18,77 @@
18 18
19 19 class TemplateTextHelper {
20 20
21 21 /**
22 + * The footer printed under a document: the document's own footer text,
23 + * else the default from Settings, then the `easy_invoice_pdf_footer_html`
24 + * filter (Pro's White-Label addon replaces it there).
25 + *
26 + * @param object $document Invoice or Quote model.
27 + * @param string $type 'invoice' or 'quote'.
28 + */
29 + public static function footerText($document, string $type = 'invoice'): string {
30 + $text = is_callable([$document, 'getFooterText']) ? (string) $document->getFooterText() : '';
31 + if ('' === trim($text)) {
32 + $text = 'quote' === $type
33 + ? \EasyInvoice\Controllers\SettingsController::getQuoteFooterText()
34 + : \EasyInvoice\Controllers\SettingsController::getInvoiceFooterText();
35 + }
36 + /**
37 + * Filter the footer of a rendered document (page, PDF, email attachment).
38 + *
39 + * @param string $text Footer text or HTML.
40 + * @param object $document The document.
41 + * @param string $type 'invoice' or 'quote'.
42 + */
43 + return (string) apply_filters('easy_invoice_pdf_footer_html', $text, $document, $type);
44 + }
45 +
46 + /**
47 + * Is this document a credit note? Credit notes are stored as their own
48 + * post type but render through the invoice designs.
49 + *
50 + * @param object $document Invoice model.
51 + */
52 + public static function isCreditNote($document): bool {
53 + return is_object($document) && is_callable([$document, 'getId'])
54 + && get_post_type((int) $document->getId()) === \EasyInvoice\Constants\PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE;
55 + }
56 +
57 + /**
58 + * The labels a design should use for this particular document: the
59 + * invoice labels as saved, or the credit-note wording when the document
60 + * is a credit note (a credit note is not a bill — no due date, no amount
61 + * to pay, no payment terms).
62 + *
63 + * @param array $text_settings From getInvoiceTextSettings().
64 + * @param object $document Invoice model.
65 + * @return array
66 + */
67 + public static function forDocument(array $text_settings, $document): array {
68 + if (!self::isCreditNote($document)) {
69 + return $text_settings;
70 + }
71 + $text_settings['invoice'] = __('Credit Note', 'easy-invoice');
72 + $text_settings['invoice_number'] = __('Credit Note Number', 'easy-invoice');
73 + $text_settings['invoice_date'] = __('Credit Note Date', 'easy-invoice');
74 + $text_settings['total'] = __('Total Credited', 'easy-invoice');
75 + $text_settings['total_due'] = __('Total Credited', 'easy-invoice');
76 + return $text_settings;
77 + }
78 +
79 + /**
80 + * The document kind printed above the title.
81 + *
82 + * @param object $document Invoice model.
83 + */
84 + public static function documentKind($document): string {
85 + return self::isCreditNote($document)
86 + ? __('Credit Note', 'easy-invoice')
87 + : \EasyInvoice\Controllers\SettingsController::getTextInvoice();
88 + }
89 +
90 + /**
22 91 * Get all text settings for templates
23 92 *
24 93 * @return array Array of text settings
25 94 */
@@ -43,8 +112,9 @@
43 112 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
44 113 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
45 114 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
46 115 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
116 + 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
47 117 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
48 118 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
49 119
50 120 // Button labels
@@ -63,9 +133,9 @@
63 133 *
64 134 * @return array Company information
65 135 */
66 136 public static function getCompanyInfo(): array {
67 - return [
137 + $info = [
68 138 'name' => get_option('easy_invoice_company_name', 'Your Company Name'),
69 139 'address' => get_option('easy_invoice_company_address', ''),
70 140 'email' => get_option('easy_invoice_company_email', ''),
71 141 'phone' => get_option('easy_invoice_company_phone', ''),
@@ -70,10 +140,19 @@
70 140 'email' => get_option('easy_invoice_company_email', ''),
71 141 'phone' => get_option('easy_invoice_company_phone', ''),
72 142 'website' => get_option('easy_invoice_company_website', ''),
73 143 'logo' => get_option('easy_invoice_company_logo', ''),
74 - 'tax_number' => get_option('easy_invoice_tax_number', ''),
144 + 'tax_number' => get_option('easy_invoice_tax_number', '') ?: get_option('easy_invoice_company_vat_number', ''),
75 145 ];
146 + /**
147 + * Filter the resolved company info used by invoice/quote templates
148 + * and PDF generation. The White-Label addon hooks this to replace
149 + * the logo with its branded header image without forcing every
150 + * admin to also see the white-label logo on their settings page.
151 + *
152 + * @param array<string,string> $info Resolved company info.
153 + */
154 + return apply_filters('easy_invoice_template_company_info', $info);
76 155 }
77 156
78 157 /**
79 158 * Get invoice specific text settings
@@ -92,8 +171,9 @@
92 171 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
93 172 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
94 173 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
95 174 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
175 + 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
96 176 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
97 177 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
98 178 'print' => \EasyInvoice\Controllers\SettingsController::getTextPrint(),
99 179 'download_pdf' => \EasyInvoice\Controllers\SettingsController::getTextDownloadPdf(),
@@ -118,8 +198,9 @@
118 198 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
119 199 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
120 200 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
121 201 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
202 + 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
122 203 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
123 204 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
124 205 'print' => \EasyInvoice\Controllers\SettingsController::getTextPrint(),
125 206 'download_pdf' => \EasyInvoice\Controllers\SettingsController::getTextDownloadPdf(),
@@ -175,8 +256,13 @@
175 256 }
176 257 if ($company_info['website']) {
177 258 $output .= '<div class="company-website">' . esc_html($company_info['website']) . '</div>';
178 259 }
260 + if (!empty($company_info['tax_number'])) {
261 + // A tax identifier belongs on the document itself; EU B2B
262 + // invoices are not valid without the supplier's VAT ID.
263 + $output .= '<div class="company-tax-number">' . esc_html(self::taxIdLabel()) . ' ' . esc_html($company_info['tax_number']) . '</div>';
264 + }
179 265 $output .= '</div>';
180 266 break;
181 267
182 268 case 'elegant':
@@ -303,8 +389,107 @@
303 389 return $output;
304 390 }
305 391
306 392 /**
393 + * What is still owed on an invoice: the total less completed payments.
394 + *
395 + * @param mixed $invoice Invoice model.
396 + * @return float
397 + */
398 + public static function amountDue($invoice): float {
399 + // Total less completed payments and credit notes; see InvoiceBalance
400 + // for the `easy_invoice_amount_due` filter.
401 + return \EasyInvoice\Services\InvoiceBalance::due($invoice);
402 + }
403 +
404 + /**
405 + * Label in front of a tax identifier, on both sides of the document.
406 + *
407 + * @return string
408 + */
409 + public static function taxIdLabel(): string {
410 + /**
411 + * Filter the label shown before tax identifiers on documents.
412 + *
413 + * @param string $label Label, "VAT" by default.
414 + */
415 + return (string) apply_filters('easy_invoice_tax_id_label', __('VAT', 'easy-invoice'));
416 + }
417 +
418 + /**
419 + * The recipient block of a document: who it is for and how to reach them.
420 + *
421 + * Business name first, the contact person underneath when the client
422 + * record has both, then address, email, phone and the client's VAT
423 + * number. Returns '' when there is nobody to address, so a design can
424 + * leave the block out instead of printing an empty "To".
425 + *
426 + * @param mixed $document Invoice or Quote model.
427 + * @return string HTML, or '' when the document has no recipient.
428 + */
429 + public static function generateClientBlock($document): string {
430 + if (!is_object($document)) {
431 + return '';
432 + }
433 + $name = trim((string) (is_callable([$document, 'getCustomerName']) ? $document->getCustomerName() : ''));
434 + $address = trim((string) (is_callable([$document, 'getCustomerAddress']) ? $document->getCustomerAddress() : ''));
435 + $email = trim((string) (is_callable([$document, 'getCustomerEmail']) ? $document->getCustomerEmail() : ''));
436 + $vat = trim((string) (is_callable([$document, 'getCustomerVatNumber']) ? $document->getCustomerVatNumber() : ''));
437 + $contact = '';
438 + $phone = '';
439 +
440 + $client_id = is_callable([$document, 'getClientId']) ? (int) $document->getClientId() : 0;
441 + if ($client_id > 0) {
442 + $client = (new \EasyInvoice\Repositories\ClientRepository())->find($client_id);
443 + if ($client) {
444 + $person = trim((string) $client->getFirstName() . ' ' . (string) $client->getLastName());
445 + if ('' !== $person && '' !== $name && 0 !== strcasecmp($person, $name)) {
446 + $contact = $person;
447 + }
448 + $phone = trim((string) $client->getPhone());
449 + if ('' === $address) {
450 + $address = trim((string) $client->getAddress());
451 + }
452 + if ('' === $email) {
453 + $email = trim((string) $client->getEmail());
454 + }
455 + }
456 + }
457 +
458 + if ('' === $name . $address . $email . $phone . $vat) {
459 + return '';
460 + }
461 +
462 + $output = '';
463 + if ('' !== $name) {
464 + $output .= '<div class="client-name">' . esc_html($name) . '</div>';
465 + }
466 + if ('' !== $contact) {
467 + $output .= '<div class="client-contact">' . esc_html($contact) . '</div>';
468 + }
469 + if ('' !== $address) {
470 + $output .= '<div class="client-address">' . nl2br(esc_html($address)) . '</div>';
471 + }
472 + if ('' !== $email) {
473 + $output .= '<div class="client-email">' . esc_html($email) . '</div>';
474 + }
475 + if ('' !== $phone) {
476 + $output .= '<div class="client-phone">' . esc_html($phone) . '</div>';
477 + }
478 + if ('' !== $vat) {
479 + $output .= '<div class="client-tax-number">' . esc_html(self::taxIdLabel()) . ' ' . esc_html($vat) . '</div>';
480 + }
481 +
482 + /**
483 + * Filter the recipient block rendered by every document design.
484 + *
485 + * @param string $output HTML lines.
486 + * @param mixed $document Invoice or Quote model.
487 + */
488 + return (string) apply_filters('easy_invoice_client_block_html', $output, $document);
489 + }
490 +
491 + /**
307 492 * Generate totals section for invoice/quote templates
308 493 *
309 494 * @param mixed $document Invoice or Quote object
310 495 * @param array $text_settings Text settings array
@@ -316,35 +501,44 @@
316 501 $output = '';
317 502
318 503 // Subtotal
319 504 $output .= '<div class="' . $type . '-total-row">';
320 - $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['sub_total']) . ':</span>';
505 + $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['sub_total']) . '</span>';
321 506 $output .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getSubtotal())) . '</span>';
322 507 $output .= '</div>';
323 508
324 - // Discount (if any)
509 + $discount_row = '';
325 510 if ($document->getDiscountAmount() > 0) {
326 - $output .= '<div class="' . $type . '-total-row">';
327 - $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['discount']);
511 + $discount_row .= '<div class="' . $type . '-total-row">';
512 + $discount_row .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['discount']);
328 513 if ($document->getDiscountType() === 'percentage') {
329 - $output .= ' (' . esc_html($document->getDiscountValue()) . '%)';
514 + $discount_row .= ' (' . esc_html($document->getDiscountValue()) . '%)';
330 515 }
331 - $output .= ':</span>';
332 - $output .= '<span class="' . $type . '-total-value">-' . esc_html($formatter->format($document->getDiscountAmount())) . '</span>';
333 - $output .= '</div>';
516 + $discount_row .= '</span>';
517 + $discount_row .= '<span class="' . $type . '-total-value">-' . esc_html($formatter->format($document->getDiscountAmount())) . '</span>';
518 + $discount_row .= '</div>';
334 519 }
335 -
336 -
337 - // Tax (if any)
520 +
521 + $tax_row = '';
338 522 if ($document->getTaxAmount() > 0) {
339 - $output .= '<div class="' . $type . '-total-row">';
340 - $output .= '<span class="' . $type . '-total-label">' . esc_html(easy_invoice_get_tax_name());
341 - $output .= ' (' . esc_html($document->getTaxRate()) . '%):</span>';
342 - $output .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getTaxAmount())) . '</span>';
343 - $output .= '</div>';
523 + $tax_row .= '<div class="' . $type . '-total-row">';
524 + $tax_row .= '<span class="' . $type . '-total-label">' . esc_html(easy_invoice_get_tax_name());
525 + $tax_row .= ' (' . esc_html($document->getTaxRate()) . '%)</span>';
526 + $tax_row .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getTaxAmount())) . '</span>';
527 + $tax_row .= '</div>';
344 528 }
529 +
530 + // The rows read in the order the figures are computed: a discount taken
531 + // before tax sits above the tax line; one taken off the taxed total sits
532 + // below it. Shown the other way round, the arithmetic does not add up
533 + // on the page.
534 + // The model treats anything but 'before_tax' as after-tax.
535 + $method = (string) ($document->discount_calculation_method ?? '');
536 + $output .= ('before_tax' === $method) ? $discount_row . $tax_row : $tax_row . $discount_row;
345 537
346 - // Allow plugins to add content
538 + // Allow plugins to add content. Captured so it lands between the
539 + // rows and the grand total rather than printing ahead of the block.
540 + ob_start();
347 541 if ($type === 'invoice') {
348 542 do_action('easy_invoice_invoice_totals_after_tax', $document);
349 543 do_action('easy_invoice_invoice_totals_after_discount', $document);
350 544 } else {
@@ -350,14 +544,58 @@
350 544 } else {
351 545 do_action('easy_invoice_quote_totals_after_tax', $document);
352 546 do_action('easy_invoice_quote_totals_after_discount', $document);
353 547 }
548 + $output .= (string) ob_get_clean();
354 549
355 550 // Grand Total
356 551 $output .= '<div class="' . $type . '-total-row ' . $type . '-grand-total">';
357 - $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['total']) . ':</span>';
552 + $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['total']) . '</span>';
358 553 $output .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getTotal())) . '</span>';
359 554 $output .= '</div>';
360 -
555 +
556 + // Money already received: an invoice that is part paid must say what
557 + // is still owed, or the reader pays the total again. Skipped when an
558 + // addon has printed its own payment breakdown above.
559 + if ('invoice' === $type && false === strpos($output, 'partial-payments-breakdown') && is_callable([$document, 'getId'])) {
560 + $invoice_id = (int) $document->getId();
561 + $paid = \EasyInvoice\Services\InvoiceBalance::paid($invoice_id);
562 + $credited = \EasyInvoice\Services\InvoiceBalance::credited($invoice_id);
563 + /**
564 + * Filter whether the totals block lists payments received, credit
565 + * notes and the balance due.
566 + *
567 + * @param bool $show Default true when something has been paid or credited.
568 + * @param object $document Invoice model.
569 + */
570 + if (($paid > 0 || $credited > 0) && apply_filters('easy_invoice_totals_show_payments', true, $document)) {
571 + $balance = max(0, (float) $document->getTotal() - $paid - $credited);
572 + // A credit note corrects the invoice; the reader needs to see
573 + // it counted off before the balance.
574 + foreach (\EasyInvoice\Services\CreditNote::forInvoice($invoice_id) as $credit_id) {
575 + $credit_amount = (float) get_post_meta($credit_id, '_easy_invoice_total', true);
576 + if ($credit_amount <= 0) {
577 + continue;
578 + }
579 + $credit_number = (string) get_post_meta($credit_id, '_easy_invoice_number', true);
580 + $output .= '<div class="invoice-total-row invoice-credit-row">';
581 + /* translators: %s: credit note number. */
582 + $output .= '<span class="invoice-total-label">' . esc_html('' !== $credit_number ? sprintf(__('Credit note %s', 'easy-invoice'), $credit_number) : __('Credit note', 'easy-invoice')) . '</span>';
583 + $output .= '<span class="invoice-total-value">-' . esc_html($formatter->format($credit_amount)) . '</span>';
584 + $output .= '</div>';
585 + }
586 + if ($paid > 0) {
587 + $output .= '<div class="invoice-total-row invoice-paid-row">';
588 + $output .= '<span class="invoice-total-label">' . esc_html__('Paid', 'easy-invoice') . '</span>';
589 + $output .= '<span class="invoice-total-value">-' . esc_html($formatter->format($paid)) . '</span>';
590 + $output .= '</div>';
591 + }
592 + $output .= '<div class="invoice-total-row invoice-balance-due">';
593 + $output .= '<span class="invoice-total-label">' . esc_html__('Balance due', 'easy-invoice') . '</span>';
594 + $output .= '<span class="invoice-total-value">' . esc_html($formatter->format($balance)) . '</span>';
595 + $output .= '</div>';
596 + }
597 + }
598 +
361 599 return $output;
362 600 }
363 601 }