PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.1.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.1.2
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 / Models / Invoice.php

Invoice.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.1.2, at includes/Models/Invoice.php

906 lines 27.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Invoice Model
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Models;
13
14 use EasyInvoice\Constants\PostTypes;
15 use EasyInvoice\Models\InvoiceItem;
16
17 /**
18 * Invoice Model
19 *
20 * Represents an invoice in the system with extensible architecture for pro features.
21 *
22 * @since 1.0.0
23 */
24 class Invoice {
25 /**
26 * Invoice ID
27 *
28 * @var int
29 */
30 private $id;
31
32 /**
33 * Dynamic data storage for all fields
34 *
35 * @var array
36 */
37 private $data = [];
38
39 /**
40 * Items
41 *
42 * @var array
43 */
44 private $items = [];
45
46 /**
47 * Modified flag
48 *
49 * @var bool
50 */
51 private $is_modified = false;
52
53 /**
54 * Magic method to get dynamic properties
55 *
56 * @since 1.0.0
57 * @param string $name Property name
58 * @return mixed Property value
59 */
60 public function __get($name) {
61 // Handle special properties
62 if ($name === 'id') {
63 return $this->id;
64 }
65
66 // Special handling for total field
67 if ($name === 'total') {
68 return $this->getTotal();
69 }
70
71 // Return from dynamic data array
72 return $this->data[$name] ?? null;
73 }
74
75 /**
76 * Magic method to set dynamic properties
77 *
78 * @since 1.0.0
79 * @param string $name Property name
80 * @param mixed $value Property value
81 */
82 public function __set($name, $value) {
83 // Handle special properties
84 if ($name === 'id') {
85 $this->id = $value;
86 return;
87 }
88
89 // Store in dynamic data array
90 $this->data[$name] = $value;
91 $this->is_modified = true;
92 }
93
94 /**
95 * Magic method to check if property exists
96 *
97 * @since 1.0.0
98 * @param string $name Property name
99 * @return bool
100 */
101 public function __isset($name) {
102 if ($name === 'id') {
103 return isset($this->id);
104 }
105
106 return isset($this->data[$name]);
107 }
108
109 /**
110 * Constructor
111 *
112 * @since 1.0.0
113 * @param \WP_Post|int $invoice Invoice post object or ID
114 */
115 public function __construct($invoice = null) {
116 // Initialize data array dynamically from field configuration
117 $this->data = $this->getDefaultValuesFromConfiguration();
118
119 if ($invoice instanceof \WP_Post) {
120 $this->loadFromPost($invoice);
121 } elseif (is_numeric($invoice)) {
122 $post = get_post($invoice);
123 if ($post && $post->post_type === PostTypes::EASY_INVOICE_POST_TYPE) {
124 $this->loadFromPost($post);
125 }
126 }
127
128 do_action('easy_invoice_invoice_model_constructed', $this);
129 }
130
131 /**
132 * Get default values from field configuration
133 *
134 * @since 1.0.0
135 * @return array
136 */
137 private function getDefaultValuesFromConfiguration(): array {
138 $default_values = [];
139
140 // Get field definitions to determine default values
141 $field_registration = new \EasyInvoice\Forms\Invoice\InvoiceFieldRegistration();
142
143 // Initialize the field registration to ensure fields are registered
144 $field_registration->registerDefaultTabs();
145 $field_registration->registerDefaultFields();
146
147 $field_definitions = [];
148 $tabs = $field_registration->getTabs();
149 foreach ($tabs as $tab_id => $tab) {
150 $tab_fields = $field_registration->getFields($tab_id);
151 foreach ($tab_fields as $field) {
152 $field_definitions[] = $field;
153 }
154 }
155
156 // Extract default values from field configuration
157 foreach ($field_definitions as $field) {
158 $field_name = $field['name'] ?? '';
159 if (empty($field_name)) {
160 continue;
161 }
162
163 $default_value = $field['default_value'] ?? null;
164
165 // Handle callable default values
166 if (is_callable($default_value)) {
167 $default_value = $default_value();
168 }
169
170 // Set appropriate default based on field type
171 if ($default_value !== null) {
172 $default_values[$field_name] = $default_value;
173 } else {
174 // Set type-appropriate defaults
175 $field_type = $field['type'] ?? 'text';
176 switch ($field_type) {
177 case 'number':
178 $default_values[$field_name] = 0.0;
179 break;
180 case 'checkbox':
181 $default_values[$field_name] = false;
182 break;
183 case 'select':
184 $default_values[$field_name] = '';
185 break;
186 case 'array':
187 $default_values[$field_name] = [];
188 break;
189 default:
190 $default_values[$field_name] = '';
191 break;
192 }
193 }
194 }
195
196 return $default_values;
197 }
198
199 /**
200 * Load invoice data from WP_Post
201 *
202 * @since 1.0.0
203 * @param \WP_Post $post
204 */
205 private function loadFromPost(\WP_Post $post): void {
206 $this->id = $post->ID;
207 $this->data['title'] = $post->post_title ?: '';
208 $this->data['created_date'] = $post->post_date ?: '';
209 $this->data['modified_date'] = $post->post_modified ?: '';
210
211 // Load meta data using configuration-driven approach
212 $this->loadMetaData();
213
214 // Load items
215 $this->loadItems();
216
217 // Auto-calculate totals if they're 0 or if we have items but no totals
218 if ((($this->data['total'] ?? 0) == 0 && !empty($this->items)) ||
219 (($this->data['subtotal'] ?? 0) == 0 && !empty($this->items))) {
220 $this->calculateTotals();
221 }
222
223 // Populate client information if we have a client_id but no customer data
224 if (($this->data['client_id'] ?? 0) > 0 && (empty($this->data['customer_name'] ?? '') || empty($this->data['customer_email'] ?? ''))) {
225 $this->populateClientInfo();
226 }
227 }
228
229 /**
230 * Load meta data from database
231 *
232 * @since 1.0.0
233 */
234 private function loadMetaData(): void {
235 if (!$this->id) {
236 return;
237 }
238
239 // Get field definitions to determine which meta keys to load
240 $field_registration = new \EasyInvoice\Forms\Invoice\InvoiceFieldRegistration();
241
242 // Initialize the field registration to ensure fields are registered
243 $field_registration->registerDefaultTabs();
244 $field_registration->registerDefaultFields();
245
246 $field_definitions = [];
247 $tabs = $field_registration->getTabs();
248 foreach ($tabs as $tab_id => $tab) {
249 $tab_fields = $field_registration->getFields($tab_id);
250 foreach ($tab_fields as $field) {
251 $field_definitions[] = $field;
252 }
253 }
254
255 // Load meta data for each field definition
256 foreach ($field_definitions as $field) {
257 $field_name = $field['name'] ?? '';
258 if (empty($field_name)) {
259 continue;
260 }
261
262 $meta_key = '_easy_invoice_' . $field_name;
263 $value = get_post_meta($this->id, $meta_key, true);
264
265 if ($value !== '') {
266 // Store in dynamic data array
267 $this->data[$field_name] = $value;
268 }
269 }
270
271
272
273
274 }
275
276 /**
277 * Load invoice items
278 *
279 * @since 1.0.0
280 */
281 private function loadItems(): void {
282 if (!$this->id) {
283 return;
284 }
285
286 $items_data = get_post_meta($this->id, '_easy_invoice_items', true) ?: [];
287 $this->items = [];
288
289 foreach ($items_data as $item_data) {
290 $this->items[] = new \EasyInvoice\Models\InvoiceItem($item_data);
291 }
292 }
293
294 /**
295 * Ensure the invoice has a proper slug for pretty permalinks
296 *
297 * @since 1.0.0
298 * @return bool True if successful, false otherwise
299 */
300 public function ensureProperSlug(): bool {
301 if (!$this->id) {
302 return false;
303 }
304
305 $post = get_post($this->id);
306 if (!$post || empty($post->post_name)) {
307 // Generate a slug from the title
308 $post_title = $this->data['title'] ?: $this->data['number'] ?: 'Untitled Invoice';
309 $post_name = sanitize_title($post_title);
310
311 // Ensure uniqueness
312 $original_slug = $post_name;
313 $counter = 1;
314 while (get_page_by_path($post_name, OBJECT, PostTypes::EASY_INVOICE_POST_TYPE)) {
315 $post_name = $original_slug . '-' . $counter;
316 $counter++;
317 }
318
319 // Update the post with the new slug
320 $result = wp_update_post([
321 'ID' => $this->id,
322 'post_name' => $post_name,
323 'post_status' => 'publish' // Ensure it's published for proper permalinks
324 ]);
325
326 return !is_wp_error($result);
327 }
328
329 return true;
330 }
331
332 /**
333 * Save the invoice
334 * Always publish invoices to ensure proper permalinks
335 * Invoice status will be stored in post meta (_easy_invoice_status)
336 *
337 * @since 1.0.0
338 * @return bool True if successful, false otherwise
339 */
340 public function save(): bool {
341 // Allow plugins to modify the invoice before saving
342 do_action('easy_invoice_model_before_save', $this);
343
344 // Always publish invoices to ensure proper permalinks
345 // Invoice status will be stored in post meta (_easy_invoice_status)
346 $post_status = 'publish';
347
348 // Generate a proper slug for pretty permalinks
349 $post_title = $this->data['title'] ?: $this->data['number'] ?: 'Untitled Invoice';
350 $post_name = '';
351
352 if ($this->id) {
353 // For existing posts, get the current post to preserve slug if it exists
354 $existing_post = get_post($this->id);
355 if ($existing_post && !empty($existing_post->post_name)) {
356 $post_name = $existing_post->post_name;
357 }
358 }
359
360 // If no slug exists, generate one from the title
361 if (empty($post_name)) {
362 $post_name = sanitize_title($post_title);
363
364 // Ensure uniqueness by appending number if needed
365 $original_slug = $post_name;
366 $counter = 1;
367 while (get_page_by_path($post_name, OBJECT, PostTypes::EASY_INVOICE_POST_TYPE)) {
368 $post_name = $original_slug . '-' . $counter;
369 $counter++;
370 }
371 }
372
373 // Prepare post data
374 $post_data = [
375 'post_title' => $post_title,
376 'post_content' => $this->data['notes'] ?: '', // Ensure post_content is never null
377 'post_status' => $post_status, // Always publish for proper permalinks
378 'post_type' => PostTypes::EASY_INVOICE_POST_TYPE,
379 'post_name' => $post_name, // Add the generated slug
380 ];
381
382 // Update existing post or create new one
383 if ($this->id) {
384 $post_data['ID'] = $this->id;
385 $post_id = wp_update_post($post_data);
386 } else {
387 $post_id = wp_insert_post($post_data);
388 }
389
390 if (is_wp_error($post_id)) {
391 return false;
392 }
393
394 // Update the ID if this was a new post
395 if (!$this->id) {
396 $this->id = $post_id;
397 }
398
399 // Calculate totals from items before saving
400 $this->calculateTotals();
401
402 // Save meta data (including invoice status)
403 $this->saveMetaData();
404
405 // Allow plugins to perform actions after saving
406 do_action('easy_invoice_model_after_save', $this);
407
408 $this->is_modified = false;
409 return true;
410 }
411
412 /**
413 * Save meta data to database
414 *
415 * @since 1.0.0
416 */
417 private function saveMetaData(): void {
418 if (!$this->id) {
419 return;
420 }
421
422 // Get field definitions to determine which meta keys to save
423 $field_registration = new \EasyInvoice\Forms\Invoice\InvoiceFieldRegistration();
424
425 // Initialize the field registration to ensure fields are registered
426 $field_registration->registerDefaultTabs();
427 $field_registration->registerDefaultFields();
428
429 $field_definitions = [];
430 $tabs = $field_registration->getTabs();
431 foreach ($tabs as $tab_id => $tab) {
432 $tab_fields = $field_registration->getFields($tab_id);
433 foreach ($tab_fields as $field) {
434 $field_definitions[] = $field;
435 }
436 }
437
438 // Save meta data for each field definition
439 foreach ($field_definitions as $field) {
440 $field_name = $field['name'] ?? '';
441 if (empty($field_name)) {
442 continue;
443 }
444
445 $meta_key = '_easy_invoice_' . $field_name;
446
447 // Get the value from dynamic data array
448 $value = $this->data[$field_name] ?? null;
449 if ($value !== null) {
450 update_post_meta($this->id, $meta_key, $value);
451 }
452 }
453
454
455
456
457
458 // Save items
459 $this->saveItems();
460
461 // Allow plugins to save additional meta data
462 do_action('easy_invoice_model_save_meta_data', $this);
463 }
464
465 /**
466 * Save invoice items
467 *
468 * @since 1.0.0
469 */
470 private function saveItems(): void {
471 if (!$this->id) {
472 return;
473 }
474
475 $items_data = [];
476 foreach ($this->items as $item) {
477 if (is_object($item) && method_exists($item, 'toArray')) {
478 $items_data[] = $item->toArray();
479 } else {
480 $items_data[] = $item;
481 }
482 }
483 update_post_meta($this->id, '_easy_invoice_items', $items_data);
484 }
485
486 /**
487 * Calculate totals from items
488 *
489 * @since 1.0.0
490 */
491 public function calculateTotals(): void {
492 $prices_include_tax = ($this->data['prices_include_tax'] ?? 'no') === 'yes';
493 $tax_rate = floatval($this->data['tax_rate'] ?? 0);
494
495 // Initialize totals
496 $subtotal = 0;
497 $taxable_subtotal = 0;
498 $this->data['tax_amount'] = 0;
499 $this->data['discount_amount'] = 0;
500
501 // First pass: Calculate raw totals
502 foreach ($this->items as $index => $item) {
503 $quantity = 1;
504 $price = 0;
505 $adjust_percentage = 0;
506 $is_taxable = true;
507
508 if (is_object($item) && method_exists($item, 'getAmount')) {
509 $quantity = $item->getQuantity();
510 $price = $item->getPrice();
511 $adjust_percentage = $item->getAdjustPercentage();
512 $is_taxable = $item->isTaxable();
513 } elseif (is_array($item)) {
514 $quantity = isset($item['quantity']) ? (float) $item['quantity'] : 1;
515 $price = isset($item['price']) ? (float) $item['price'] : 0;
516 $adjust_percentage = isset($item['adjust_percentage']) ? (float) $item['adjust_percentage'] : 0;
517 $is_taxable = isset($item['taxable']) ? (bool) $item['taxable'] : true;
518 }
519
520 // If prices include tax and item is taxable, remove tax from price
521 if ($prices_include_tax && $is_taxable && $tax_rate > 0) {
522 $price = $price / (1 + ($tax_rate / 100));
523 }
524
525 // Calculate item total
526 $item_total = $quantity * $price;
527 if ($adjust_percentage != 0) {
528 $item_total = $item_total * (1 + $adjust_percentage / 100);
529 }
530
531 $subtotal += $item_total;
532 if ($is_taxable) {
533 $taxable_subtotal += $item_total;
534 }
535 }
536
537 $this->data['subtotal'] = $subtotal;
538
539 // Get discount calculation method (default to before_tax)
540 $discount_calculation_method = $this->data['discount_calculation_method'] ?? 'before_tax';
541
542 // Calculate initial discount amount
543 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $subtotal > 0) {
544 $this->data['discount_amount'] = ($subtotal * $this->data['discount_value']) / 100;
545 } elseif (($this->data['discount_type'] ?? '') === 'fixed' && ($this->data['discount_value'] ?? 0) > 0) {
546 $this->data['discount_amount'] = $this->data['discount_value'];
547 } else {
548 $this->data['discount_amount'] = 0;
549 }
550
551 // Calculate tax and total based on discount calculation method
552 if ($discount_calculation_method === 'before_tax') {
553 // For before_tax: Apply discount first, then calculate tax on remaining taxable amount
554 $discount_ratio = ($subtotal > 0) ? ($this->data['discount_amount'] / $subtotal) : 0;
555 $taxable_amount = $taxable_subtotal * (1 - $discount_ratio);
556
557 if ($tax_rate > 0) {
558 $this->data['tax_amount'] = ($taxable_amount * $tax_rate) / 100;
559 }
560
561 $this->data['total'] = $subtotal - $this->data['discount_amount'] + $this->data['tax_amount'];
562 } else {
563 // For after_tax: Calculate tax first, then apply discount
564 if ($tax_rate > 0) {
565 $this->data['tax_amount'] = ($taxable_subtotal * $tax_rate) / 100;
566 }
567
568 $total_before_discount = $subtotal + $this->data['tax_amount'];
569
570 // Recalculate percentage discount based on total including tax
571 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $total_before_discount > 0) {
572 $this->data['discount_amount'] = ($total_before_discount * $this->data['discount_value']) / 100;
573 }
574
575 $this->data['total'] = $total_before_discount - $this->data['discount_amount'];
576 }
577
578 // Allow plugins to modify calculations
579 do_action('easy_invoice_model_calculate_totals', $this);
580 }
581
582 /**
583 * Populate client information from client repository
584 *
585 * @since 1.0.0
586 */
587 public function populateClientInfo(): void {
588 if (($this->data['client_id'] ?? 0) > 0) {
589 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
590 $client = $client_repository->find($this->data['client_id']);
591
592 if ($client) {
593 $this->data['customer_name'] = $client->getBusinessClientName() ?: '';
594 $this->data['customer_email'] = $client->getEmail() ?: '';
595 $this->data['customer_address'] = $client->getAddress() ?: '';
596 }
597 }
598 }
599
600 /**
601 * Force populate client info and save (for existing invoices that need client data)
602 *
603 * @since 1.0.0
604 * @return bool
605 */
606 public function forcePopulateClientInfo(): bool {
607 $this->populateClientInfo();
608 return $this->save();
609 }
610
611 /**
612 * Recalculate totals and save (for existing invoices)
613 *
614 * @since 1.0.0
615 * @return bool
616 */
617 public function recalculateAndSave(): bool {
618 // Populate client information if we have a client_id
619 $this->populateClientInfo();
620
621 // Calculate totals from items
622 $this->calculateTotals();
623
624 // Save the updated invoice
625 return $this->save();
626 }
627
628 /**
629 * Convert to array
630 *
631 * @since 1.0.0
632 * @return array
633 */
634 public function toArray(): array {
635 $items_array = [];
636 foreach ($this->items as $item) {
637 if (is_object($item) && method_exists($item, 'toArray')) {
638 $items_array[] = $item->toArray();
639 } else {
640 $items_array[] = $item;
641 }
642 }
643
644 // Start with dynamic data
645 $data = $this->data;
646
647 // Add special properties
648 $data['id'] = $this->id;
649 $data['items'] = $items_array;
650
651 // Allow plugins to modify the array data
652 return apply_filters('easy_invoice_model_to_array', $data, $this);
653 }
654
655 /**
656 * Dynamic getter method
657 *
658 * @since 1.0.0
659 * @param string $name Method name
660 * @param array $arguments Method arguments
661 * @return mixed
662 */
663 public function __call($name, $arguments) {
664 // Handle getter methods (getFieldName)
665 if (strpos($name, 'get') === 0) {
666 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'get' prefix
667 return $this->__get($field_name);
668 }
669
670 // Handle setter methods (setFieldName)
671 if (strpos($name, 'set') === 0) {
672 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'set' prefix
673 $value = $arguments[0] ?? null;
674 $this->__set($field_name, $value);
675 return null;
676 }
677
678 // Handle isset methods (isFieldName)
679 if (strpos($name, 'is') === 0) {
680 $field_name = $this->camelCaseToSnakeCase(substr($name, 2)); // Remove 'is' prefix
681 return (bool) $this->__get($field_name);
682 }
683
684 // Handle has methods (hasFieldName)
685 if (strpos($name, 'has') === 0) {
686 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'has' prefix
687 return !empty($this->__get($field_name));
688 }
689
690 throw new \BadMethodCallException("Method $name does not exist");
691 }
692
693 /**
694 * Convert camelCase to snake_case
695 *
696 * @since 1.0.0
697 * @param string $camelCase
698 * @return string
699 */
700 private function camelCaseToSnakeCase($camelCase) {
701 return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $camelCase));
702 }
703
704 // Essential methods only
705 public function getId(): int { return $this->id ?? 0; }
706 public function setId(int $id): void { $this->id = $id; }
707 public function getItems(): array { return $this->items; }
708 public function setItems(array $items): void {
709 $this->items = [];
710 foreach ($items as $item) {
711 if (is_array($item)) {
712 $this->items[] = new InvoiceItem($item);
713 } elseif (is_object($item) && $item instanceof InvoiceItem) {
714 $this->items[] = $item;
715 }
716 }
717 $this->is_modified = true;
718 }
719 public function isModified(): bool { return $this->is_modified; }
720
721 /**
722 * Set meta data for the invoice
723 *
724 * @since 1.0.0
725 * @param string $key Meta key
726 * @param mixed $value Meta value
727 */
728 public function setMetaData(string $key, $value): void {
729 // Store in dynamic data array without the _easy_invoice_ prefix
730 $field_name = easy_invoice_str_replace('_easy_invoice_', '', $key);
731 $this->data[$field_name] = $value;
732 $this->is_modified = true;
733 }
734
735 /**
736 * Get currency code
737 *
738 * @since 1.0.0
739 * @return string Currency code
740 */
741 public function getCurrencyCode(): string {
742 $currency_code = $this->data['currency_code'] ?? 'USD';
743
744 // If currency is set to 'global', resolve to actual global setting
745 if ($currency_code === 'global') {
746 $currency_code = get_option('easy_invoice_currency_code', 'USD');
747 }
748
749 // Ensure currency code is uppercase for consistency
750 return strtoupper($currency_code);
751 }
752
753 /**
754 * Set currency code
755 *
756 * @since 1.0.0
757 * @param string $currency_code Currency code
758 */
759 public function setCurrencyCode(string $currency_code): void {
760 $this->data['currency_code'] = $currency_code;
761 $this->is_modified = true;
762 }
763
764 /**
765 * Get currency position
766 *
767 * @since 1.0.0
768 * @return string Currency position
769 */
770 public function getCurrencyPosition(): string {
771 $currency_position = $this->data['currency_position'] ?? 'left';
772
773 // If currency position is set to 'global', resolve to actual global setting
774 if ($currency_position === 'global') {
775 $currency_position = get_option('easy_invoice_currency_position', 'left');
776 }
777
778 // Map form format to global settings format
779 if ($currency_position === 'before') {
780 $currency_position = 'left';
781 } elseif ($currency_position === 'after') {
782 $currency_position = 'right';
783 }
784
785 return $currency_position;
786 }
787
788 /**
789 * Set currency position
790 *
791 * @since 1.0.0
792 * @param string $currency_position Currency position
793 */
794 public function setCurrencyPosition(string $currency_position): void {
795 $this->data['currency_position'] = $currency_position;
796 $this->is_modified = true;
797 }
798
799 /**
800 * Get raw currency code (without resolving global)
801 *
802 * @since 1.0.0
803 * @return string Raw currency code
804 */
805 public function getRawCurrencyCode(): string {
806 return $this->data['currency_code'] ?? 'global';
807 }
808
809 /**
810 * Get raw currency position (without resolving global)
811 *
812 * @since 1.0.0
813 * @return string Raw currency position
814 */
815 public function getRawCurrencyPosition(): string {
816 return $this->data['currency_position'] ?? 'global';
817 }
818
819 public function getDescription(): string {
820 return $this->data['description'] ?? '';
821 }
822
823 public function setDescription(string $description): void {
824 $this->data['description'] = $description;
825 $this->is_modified = true;
826 }
827
828 public function getTemplate(): string {
829 return $this->data['invoice_template'] ?? 'standard';
830 }
831
832 public function setTemplate(string $template): void {
833 $this->data['invoice_template'] = $template;
834 $this->is_modified = true;
835 }
836
837 /**
838 * Get discount type (percentage, fixed, none)
839 *
840 * @since 1.0.0
841 * @return string
842 */
843 public function getDiscountType(): string {
844 return $this->data['discount_type'] ?? 'none';
845 }
846
847 /**
848 * Get discount value (percentage or fixed amount)
849 *
850 * @since 1.0.0
851 * @return float
852 */
853 public function getDiscountValue(): float {
854 return floatval($this->data['discount_value'] ?? 0);
855 }
856
857 /**
858 * Get discount amount (calculated value)
859 *
860 * @since 1.0.0
861 * @return float
862 */
863 public function getDiscountAmount(): float {
864 $this->calculateTotals();
865 return floatval($this->data['discount_amount'] ?? 0);
866 }
867
868 /**
869 * Get tax rate percentage
870 *
871 * @since 1.0.0
872 * @return float
873 */
874 public function getTaxRate(): float {
875 return floatval($this->data['tax_rate'] ?? 0);
876 }
877
878 /**
879 * Get tax amount (calculated value)
880 *
881 * @since 1.0.0
882 * @return float
883 */
884 public function getTaxAmount(): float {
885 $this->calculateTotals();
886 return floatval($this->data['tax_amount'] ?? 0);
887 }
888
889 /**
890 * Get the total amount for this invoice
891 * Calculate from items
892 *
893 * @since 1.0.0
894 * @return float
895 */
896 public function getTotal(): float {
897 // Calculate from items
898 $this->calculateTotals();
899 $total = (float) ($this->data['total'] ?? 0);
900
901 // Allow plugins to modify the total
902 return apply_filters('easy_invoice_invoice_total', $total, $this);
903 }
904
905
906 }