PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
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
easy-invoice / includes / Helpers / TemplateTextHelper.php

TemplateTextHelper.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at includes/Helpers/TemplateTextHelper.php

601 lines 29.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Template Text Helper
4 *
5 * Centralized helper for retrieving text settings used in invoice and quote templates.
6 * This prevents duplication and makes text management easier.
7 *
8 * @package EasyInvoice
9 * @since 2.0.0
10 */
11
12 namespace EasyInvoice\Helpers;
13
14 // Prevent direct access
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 class TemplateTextHelper {
20
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 /**
91 * Get all text settings for templates
92 *
93 * @return array Array of text settings
94 */
95 public static function getAllTextSettings(): array {
96 return [
97 // Invoice specific
98 'invoice_number' => \EasyInvoice\Controllers\SettingsController::getTextInvoiceNumber(),
99 'invoice_date' => \EasyInvoice\Controllers\SettingsController::getTextInvoiceDate(),
100 'due_date' => \EasyInvoice\Controllers\SettingsController::getTextDueDate(),
101 'total_due' => \EasyInvoice\Controllers\SettingsController::getTextTotalDue(),
102
103 // Quote specific
104 'quote_number' => \EasyInvoice\Controllers\SettingsController::getTextQuoteNumber(),
105 'quote_date' => \EasyInvoice\Controllers\SettingsController::getTextQuoteDate(),
106 'valid_until' => \EasyInvoice\Controllers\SettingsController::getTextValidUntil(),
107
108 // Common labels
109 'to' => \EasyInvoice\Controllers\SettingsController::getTextTo(),
110 'service' => \EasyInvoice\Controllers\SettingsController::getTextService(),
111 'qty' => \EasyInvoice\Controllers\SettingsController::getTextQty(),
112 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
113 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
114 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
115 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
116 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
117 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
118 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
119
120 // Button labels
121 'print' => \EasyInvoice\Controllers\SettingsController::getTextPrint(),
122 'download_pdf' => \EasyInvoice\Controllers\SettingsController::getTextDownloadPdf(),
123 'send_email' => \EasyInvoice\Controllers\SettingsController::getTextSendEmail(),
124 'pay_now' => \EasyInvoice\Controllers\SettingsController::getTextPayNow(),
125 'accept_quote' => \EasyInvoice\Controllers\SettingsController::getTextAcceptQuote(),
126 'decline_quote' => \EasyInvoice\Controllers\SettingsController::getTextDeclineQuote(),
127 'decline_reason' => \EasyInvoice\Controllers\SettingsController::getTextDeclineReason(),
128 ];
129 }
130
131 /**
132 * Get company information from settings
133 *
134 * @return array Company information
135 */
136 public static function getCompanyInfo(): array {
137 $info = [
138 'name' => get_option('easy_invoice_company_name', 'Your Company Name'),
139 'address' => get_option('easy_invoice_company_address', ''),
140 'email' => get_option('easy_invoice_company_email', ''),
141 'phone' => get_option('easy_invoice_company_phone', ''),
142 'website' => get_option('easy_invoice_company_website', ''),
143 'logo' => get_option('easy_invoice_company_logo', ''),
144 'tax_number' => get_option('easy_invoice_tax_number', '') ?: get_option('easy_invoice_company_vat_number', ''),
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);
155 }
156
157 /**
158 * Get invoice specific text settings
159 *
160 * @return array Invoice text settings
161 */
162 public static function getInvoiceTextSettings(): array {
163 return [
164 'invoice_number' => \EasyInvoice\Controllers\SettingsController::getTextInvoiceNumber(),
165 'invoice_date' => \EasyInvoice\Controllers\SettingsController::getTextInvoiceDate(),
166 'due_date' => \EasyInvoice\Controllers\SettingsController::getTextDueDate(),
167 'total_due' => \EasyInvoice\Controllers\SettingsController::getTextTotalDue(),
168 'to' => \EasyInvoice\Controllers\SettingsController::getTextTo(),
169 'service' => \EasyInvoice\Controllers\SettingsController::getTextService(),
170 'qty' => \EasyInvoice\Controllers\SettingsController::getTextQty(),
171 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
172 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
173 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
174 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
175 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
176 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
177 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
178 'print' => \EasyInvoice\Controllers\SettingsController::getTextPrint(),
179 'download_pdf' => \EasyInvoice\Controllers\SettingsController::getTextDownloadPdf(),
180 'send_email' => \EasyInvoice\Controllers\SettingsController::getTextSendEmail(),
181 'pay_now' => \EasyInvoice\Controllers\SettingsController::getTextPayNow(),
182 ];
183 }
184
185 /**
186 * Get quote specific text settings
187 *
188 * @return array Quote text settings
189 */
190 public static function getQuoteTextSettings(): array {
191 return [
192 'quote_number' => \EasyInvoice\Controllers\SettingsController::getTextQuoteNumber(),
193 'quote_date' => \EasyInvoice\Controllers\SettingsController::getTextQuoteDate(),
194 'valid_until' => \EasyInvoice\Controllers\SettingsController::getTextValidUntil(),
195 'to' => \EasyInvoice\Controllers\SettingsController::getTextTo(),
196 'service' => \EasyInvoice\Controllers\SettingsController::getTextService(),
197 'qty' => \EasyInvoice\Controllers\SettingsController::getTextQty(),
198 'rate_price' => \EasyInvoice\Controllers\SettingsController::getTextRatePrice(),
199 'adjust' => \EasyInvoice\Controllers\SettingsController::getTextAdjust(),
200 'sub_total' => \EasyInvoice\Controllers\SettingsController::getTextSubTotal(),
201 'total' => \EasyInvoice\Controllers\SettingsController::getTextTotal(),
202 'line_total' => \EasyInvoice\Controllers\SettingsController::getTextLineTotal(),
203 'tax' => \EasyInvoice\Controllers\SettingsController::getTextTax(),
204 'discount' => \EasyInvoice\Controllers\SettingsController::getTextDiscount(),
205 'print' => \EasyInvoice\Controllers\SettingsController::getTextPrint(),
206 'download_pdf' => \EasyInvoice\Controllers\SettingsController::getTextDownloadPdf(),
207 'send_email' => \EasyInvoice\Controllers\SettingsController::getTextSendEmail(),
208 'accept_quote' => \EasyInvoice\Controllers\SettingsController::getTextAcceptQuote(),
209 'decline_quote' => \EasyInvoice\Controllers\SettingsController::getTextDeclineQuote(),
210 'decline_reason' => \EasyInvoice\Controllers\SettingsController::getTextDeclineReason(),
211 ];
212 }
213
214 /**
215 * Generate "From" section content for templates
216 *
217 * @param array $company_info Company information array
218 * @param bool $show_label Whether to show the "From" label
219 * @param string $style Style variant for the "From" section
220 * @return string Generated HTML for "From" section
221 */
222 public static function generateFromSection($company_info, $show_label = true, $style = 'default'): string {
223 $output = '';
224
225 switch ($style) {
226 case 'minimal':
227 // Minimal style - includes company name and details
228 $output .= '<div class="company-info-minimal">';
229 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
230 if ($company_info['address']) {
231 $output .= '<div class="company-address">' . nl2br(esc_html($company_info['address'])) . '</div>';
232 }
233 if ($company_info['email']) {
234 $output .= '<div class="company-email">' . esc_html($company_info['email']) . '</div>';
235 }
236 if ($company_info['phone']) {
237 $output .= '<div class="company-phone">' . esc_html($company_info['phone']) . '</div>';
238 }
239 if ($company_info['website']) {
240 $output .= '<div class="company-website">' . esc_html($company_info['website']) . '</div>';
241 }
242 $output .= '</div>';
243 break;
244
245 case 'details-only':
246 // Details only style - excludes company name (for use with separate company-name div)
247 $output .= '<div class="company-info-details-only">';
248 if ($company_info['address']) {
249 $output .= '<div class="company-address">' . nl2br(esc_html($company_info['address'])) . '</div>';
250 }
251 if ($company_info['email']) {
252 $output .= '<div class="company-email">' . esc_html($company_info['email']) . '</div>';
253 }
254 if ($company_info['phone']) {
255 $output .= '<div class="company-phone">' . esc_html($company_info['phone']) . '</div>';
256 }
257 if ($company_info['website']) {
258 $output .= '<div class="company-website">' . esc_html($company_info['website']) . '</div>';
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 }
265 $output .= '</div>';
266 break;
267
268 case 'elegant':
269 // Elegant style - subtle design
270 $output .= '<div class="company-info-elegant">';
271 if ($show_label) {
272 $output .= '<h3 class="from-label">' . esc_html(\EasyInvoice\Controllers\SettingsController::getTextFrom()) . '</h3>';
273 }
274 $output .= '<div class="company-details">';
275 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
276 if ($company_info['address']) {
277 $output .= '<div class="company-address">' . nl2br(esc_html($company_info['address'])) . '</div>';
278 }
279 if ($company_info['email']) {
280 $output .= '<div class="company-email">' . esc_html($company_info['email']) . '</div>';
281 }
282 if ($company_info['phone']) {
283 $output .= '<div class="company-phone">' . esc_html($company_info['phone']) . '</div>';
284 }
285 if ($company_info['website']) {
286 $output .= '<div class="company-website">' . esc_html($company_info['website']) . '</div>';
287 }
288 $output .= '</div>';
289 $output .= '</div>';
290 break;
291
292 case 'modern':
293 // Modern style - clean and bold
294 $output .= '<div class="company-info-modern">';
295 if ($show_label) {
296 $output .= '<h2 class="from-label">' . esc_html(\EasyInvoice\Controllers\SettingsController::getTextFrom()) . ':</h2>';
297 }
298 $output .= '<div class="address-content">';
299 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
300 if ($company_info['address']) {
301 $output .= nl2br(esc_html($company_info['address'])) . '<br>';
302 }
303 if ($company_info['email']) {
304 $output .= esc_html($company_info['email']) . '<br>';
305 }
306 if ($company_info['phone']) {
307 $output .= esc_html($company_info['phone']) . '<br>';
308 }
309 if ($company_info['website']) {
310 $output .= esc_html($company_info['website']);
311 }
312 $output .= '</div>';
313 $output .= '</div>';
314 break;
315
316 case 'professional':
317 // Professional style - structured layout
318 $output .= '<div class="company-info-professional">';
319 if ($show_label) {
320 $output .= '<h2 class="from-label">' . esc_html(\EasyInvoice\Controllers\SettingsController::getTextFrom()) . ':</h2>';
321 }
322 $output .= '<div class="address-content">';
323 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
324 if ($company_info['address']) {
325 $output .= nl2br(esc_html($company_info['address'])) . '<br>';
326 }
327 if ($company_info['email']) {
328 $output .= esc_html($company_info['email']) . '<br>';
329 }
330 if ($company_info['phone']) {
331 $output .= esc_html($company_info['phone']) . '<br>';
332 }
333 if ($company_info['website']) {
334 $output .= esc_html($company_info['website']);
335 }
336 $output .= '</div>';
337 $output .= '</div>';
338 break;
339
340 case 'creative':
341 // Creative style - artistic design
342 $output .= '<div class="company-info-creative">';
343 if ($show_label) {
344 $output .= '<h2 class="from-label">' . esc_html(\EasyInvoice\Controllers\SettingsController::getTextFrom()) . '</h2>';
345 }
346 $output .= '<div class="address-content">';
347 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
348 if ($company_info['address']) {
349 $output .= nl2br(esc_html($company_info['address'])) . '<br>';
350 }
351 if ($company_info['email']) {
352 $output .= esc_html($company_info['email']) . '<br>';
353 }
354 if ($company_info['phone']) {
355 $output .= esc_html($company_info['phone']) . '<br>';
356 }
357 if ($company_info['website']) {
358 $output .= esc_html($company_info['website']);
359 }
360 $output .= '</div>';
361 $output .= '</div>';
362 break;
363
364 default:
365 // Default style - standard layout
366 $output .= '<div class="company-info-default">';
367 if ($show_label) {
368 $output .= '<h2 class="from-label">' . esc_html(\EasyInvoice\Controllers\SettingsController::getTextFrom()) . ':</h2>';
369 }
370 $output .= '<div class="address-content">';
371 $output .= '<div class="company-name">' . esc_html($company_info['name']) . '</div>';
372 if ($company_info['address']) {
373 $output .= nl2br(esc_html($company_info['address'])) . '<br>';
374 }
375 if ($company_info['email']) {
376 $output .= esc_html($company_info['email']) . '<br>';
377 }
378 if ($company_info['phone']) {
379 $output .= esc_html($company_info['phone']) . '<br>';
380 }
381 if ($company_info['website']) {
382 $output .= esc_html($company_info['website']);
383 }
384 $output .= '</div>';
385 $output .= '</div>';
386 break;
387 }
388
389 return $output;
390 }
391
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 /**
492 * Generate totals section for invoice/quote templates
493 *
494 * @param mixed $document Invoice or Quote object
495 * @param array $text_settings Text settings array
496 * @param mixed $formatter Formatter object
497 * @param string $type 'invoice' or 'quote'
498 * @return string Generated HTML for totals section
499 */
500 public static function generateTotalsSection($document, array $text_settings, $formatter, string $type = 'invoice'): string {
501 $output = '';
502
503 // Subtotal
504 $output .= '<div class="' . $type . '-total-row">';
505 $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['sub_total']) . '</span>';
506 $output .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getSubtotal())) . '</span>';
507 $output .= '</div>';
508
509 $discount_row = '';
510 if ($document->getDiscountAmount() > 0) {
511 $discount_row .= '<div class="' . $type . '-total-row">';
512 $discount_row .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['discount']);
513 if ($document->getDiscountType() === 'percentage') {
514 $discount_row .= ' (' . esc_html($document->getDiscountValue()) . '%)';
515 }
516 $discount_row .= '</span>';
517 $discount_row .= '<span class="' . $type . '-total-value">-' . esc_html($formatter->format($document->getDiscountAmount())) . '</span>';
518 $discount_row .= '</div>';
519 }
520
521 $tax_row = '';
522 if ($document->getTaxAmount() > 0) {
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>';
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;
537
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();
541 if ($type === 'invoice') {
542 do_action('easy_invoice_invoice_totals_after_tax', $document);
543 do_action('easy_invoice_invoice_totals_after_discount', $document);
544 } else {
545 do_action('easy_invoice_quote_totals_after_tax', $document);
546 do_action('easy_invoice_quote_totals_after_discount', $document);
547 }
548 $output .= (string) ob_get_clean();
549
550 // Grand Total
551 $output .= '<div class="' . $type . '-total-row ' . $type . '-grand-total">';
552 $output .= '<span class="' . $type . '-total-label">' . esc_html($text_settings['total']) . '</span>';
553 $output .= '<span class="' . $type . '-total-value">' . esc_html($formatter->format($document->getTotal())) . '</span>';
554 $output .= '</div>';
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
599 return $output;
600 }
601 }