PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.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.3.2, at includes/Models/Invoice.php

933 lines 29.1 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 // Save all fields that exist in the data array (including empty strings to allow clearing fields)
449 // If a field exists in $this->data, it means it was explicitly set, so we should save it
450 if (array_key_exists($field_name, $this->data)) {
451 $value = $this->data[$field_name];
452 update_post_meta($this->id, $meta_key, $value);
453 }
454 }
455
456
457
458
459
460 // Save items
461 $this->saveItems();
462
463 // Allow plugins to save additional meta data
464 do_action('easy_invoice_model_save_meta_data', $this);
465 }
466
467 /**
468 * Save invoice items
469 *
470 * @since 1.0.0
471 */
472 private function saveItems(): void {
473 if (!$this->id) {
474 return;
475 }
476
477 $items_data = [];
478 foreach ($this->items as $item) {
479 if (is_object($item) && method_exists($item, 'toArray')) {
480 $items_data[] = $item->toArray();
481 } else {
482 $items_data[] = $item;
483 }
484 }
485 update_post_meta($this->id, '_easy_invoice_items', $items_data);
486 }
487
488 /**
489 * Calculate totals from items
490 *
491 * @since 1.0.0
492 */
493 public function calculateTotals(): void {
494 // Per-invoice tax_enabled override. Falls back to the global
495 // Settings → Tax → Enable Tax option when the per-invoice
496 // value is unset (eg. invoices created before the per-invoice
497 // override was introduced). Set tax_rate to 0 when disabled
498 // so the rest of the math runs unchanged — keeps tax_rate as
499 // an authored value the user can flip back on without
500 // re-entering it.
501 $tax_enabled_meta = $this->data['tax_enabled'] ?? null;
502 if ($tax_enabled_meta === null || $tax_enabled_meta === '') {
503 $tax_enabled = get_option('easy_invoice_tax_enabled', 'no') === 'yes';
504 } else {
505 $tax_enabled = ($tax_enabled_meta === 'yes' || $tax_enabled_meta === '1'
506 || $tax_enabled_meta === 1 || $tax_enabled_meta === true);
507 }
508
509 $prices_include_tax = ($this->data['prices_include_tax'] ?? 'no') === 'yes';
510 $tax_rate = $tax_enabled ? floatval($this->data['tax_rate'] ?? 0) : 0;
511
512 // Initialize totals
513 $subtotal = 0;
514 $taxable_subtotal = 0;
515 $this->data['tax_amount'] = 0;
516 $this->data['discount_amount'] = 0;
517
518 // First pass: Calculate raw totals
519 foreach ($this->items as $index => $item) {
520 $quantity = 0;
521 $price = 0;
522 $adjust_percentage = 0;
523 $is_taxable = true;
524
525 if (is_object($item) && method_exists($item, 'getAmount')) {
526 $quantity = $item->getQuantity();
527 $price = $item->getPrice();
528 $adjust_percentage = $item->getAdjustPercentage();
529 $is_taxable = $item->isTaxable();
530 } elseif (is_array($item)) {
531 $quantity = isset($item['quantity']) ? (float) $item['quantity'] : 0;
532 $price = isset($item['price']) ? (float) $item['price'] : 0;
533 $adjust_percentage = isset($item['adjust_percentage']) ? (float) $item['adjust_percentage'] : 0;
534 $is_taxable = isset($item['taxable']) ? (bool) $item['taxable'] : true;
535 }
536
537 // If prices include tax and item is taxable, remove tax from price
538 if ($prices_include_tax && $is_taxable && $tax_rate > 0) {
539 $price = $price / (1 + ($tax_rate / 100));
540 }
541
542 // Calculate item total
543 $item_total = $quantity * $price;
544 // Only apply adjust percentage if the adjust field is enabled
545 if ($adjust_percentage != 0 && \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField()) {
546 $item_total = $item_total * (1 + $adjust_percentage / 100);
547 }
548
549 $subtotal += $item_total;
550 if ($is_taxable) {
551 $taxable_subtotal += $item_total;
552 }
553 }
554
555 $this->data['subtotal'] = $subtotal;
556
557 // Get discount calculation method (default to before_tax)
558 $discount_calculation_method = $this->data['discount_calculation_method'] ?? 'before_tax';
559
560 // Calculate initial discount amount
561 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $subtotal > 0) {
562 $this->data['discount_amount'] = ($subtotal * $this->data['discount_value']) / 100;
563 } elseif (($this->data['discount_type'] ?? '') === 'fixed' && ($this->data['discount_value'] ?? 0) > 0) {
564 $this->data['discount_amount'] = $this->data['discount_value'];
565 } else {
566 $this->data['discount_amount'] = 0;
567 }
568
569 // Calculate tax and total based on discount calculation method
570 if ($discount_calculation_method === 'before_tax') {
571 // For before_tax: Apply discount first, then calculate tax on remaining taxable amount
572 $discount_ratio = ($subtotal > 0) ? ($this->data['discount_amount'] / $subtotal) : 0;
573 $taxable_amount = $taxable_subtotal * (1 - $discount_ratio);
574
575 if ($tax_rate > 0) {
576 $this->data['tax_amount'] = ($taxable_amount * $tax_rate) / 100;
577 }
578
579 $this->data['total'] = $subtotal - $this->data['discount_amount'] + $this->data['tax_amount'];
580 } else {
581 // For after_tax: Calculate tax first, then apply discount
582 if ($tax_rate > 0) {
583 $this->data['tax_amount'] = ($taxable_subtotal * $tax_rate) / 100;
584 }
585
586 $total_before_discount = $subtotal + $this->data['tax_amount'];
587
588 // Recalculate percentage discount based on total including tax
589 if (($this->data['discount_type'] ?? '') === 'percentage' && ($this->data['discount_value'] ?? 0) > 0 && $total_before_discount > 0) {
590 $this->data['discount_amount'] = ($total_before_discount * $this->data['discount_value']) / 100;
591 }
592
593 $this->data['total'] = $total_before_discount - $this->data['discount_amount'];
594 }
595
596 // Allow plugins to modify calculations
597 do_action('easy_invoice_model_calculate_totals', $this);
598 }
599
600 /**
601 * Populate client information from client repository
602 *
603 * @since 1.0.0
604 */
605 public function populateClientInfo(): void {
606 if (($this->data['client_id'] ?? 0) > 0) {
607 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
608 $client = $client_repository->find($this->data['client_id']);
609
610 if ($client) {
611 $this->data['customer_name'] = $client->getBusinessClientName() ?: '';
612 $this->data['customer_email'] = $client->getEmail() ?: '';
613 $this->data['customer_address'] = $client->getAddress() ?: '';
614 }
615 }
616 }
617
618 /**
619 * Force populate client info and save (for existing invoices that need client data)
620 *
621 * @since 1.0.0
622 * @return bool
623 */
624 public function forcePopulateClientInfo(): bool {
625 $this->populateClientInfo();
626 return $this->save();
627 }
628
629 /**
630 * Recalculate totals and save (for existing invoices)
631 *
632 * @since 1.0.0
633 * @return bool
634 */
635 public function recalculateAndSave(): bool {
636 // Populate client information if we have a client_id
637 $this->populateClientInfo();
638
639 // Calculate totals from items
640 $this->calculateTotals();
641
642 // Save the updated invoice
643 return $this->save();
644 }
645
646 /**
647 * Convert to array
648 *
649 * @since 1.0.0
650 * @return array
651 */
652 public function toArray(): array {
653 $items_array = [];
654 foreach ($this->items as $item) {
655 if (is_object($item) && method_exists($item, 'toArray')) {
656 $items_array[] = $item->toArray();
657 } else {
658 $items_array[] = $item;
659 }
660 }
661
662 // Start with dynamic data
663 $data = $this->data;
664
665 // Add special properties
666 $data['id'] = $this->id;
667 $data['items'] = $items_array;
668
669 // Allow plugins to modify the array data
670 return apply_filters('easy_invoice_model_to_array', $data, $this);
671 }
672
673 /**
674 * Dynamic getter method
675 *
676 * @since 1.0.0
677 * @param string $name Method name
678 * @param array $arguments Method arguments
679 * @return mixed
680 */
681 public function __call($name, $arguments) {
682 // Handle getter methods (getFieldName)
683 if (strpos($name, 'get') === 0) {
684 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'get' prefix
685 return $this->__get($field_name);
686 }
687
688 // Handle setter methods (setFieldName)
689 if (strpos($name, 'set') === 0) {
690 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'set' prefix
691 $value = $arguments[0] ?? null;
692 $this->__set($field_name, $value);
693 return null;
694 }
695
696 // Handle isset methods (isFieldName)
697 if (strpos($name, 'is') === 0) {
698 $field_name = $this->camelCaseToSnakeCase(substr($name, 2)); // Remove 'is' prefix
699 return (bool) $this->__get($field_name);
700 }
701
702 // Handle has methods (hasFieldName)
703 if (strpos($name, 'has') === 0) {
704 $field_name = $this->camelCaseToSnakeCase(substr($name, 3)); // Remove 'has' prefix
705 return !empty($this->__get($field_name));
706 }
707
708 throw new \BadMethodCallException("Method $name does not exist");
709 }
710
711 /**
712 * Convert camelCase to snake_case
713 *
714 * @since 1.0.0
715 * @param string $camelCase
716 * @return string
717 */
718 private function camelCaseToSnakeCase($camelCase) {
719 return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $camelCase));
720 }
721
722 // Essential methods only
723 public function getId(): int { return $this->id ?? 0; }
724 public function setId(int $id): void { $this->id = $id; }
725 public function getItems(): array { return $this->items; }
726
727 /**
728 * Check if invoice exists
729 *
730 * @return bool
731 */
732 public function exists(): bool {
733 return $this->id > 0 && get_post($this->id) !== null;
734 }
735 public function setItems(array $items): void {
736 $this->items = [];
737 foreach ($items as $item) {
738 if (is_array($item)) {
739 $this->items[] = new InvoiceItem($item);
740 } elseif (is_object($item) && $item instanceof InvoiceItem) {
741 $this->items[] = $item;
742 }
743 }
744 $this->is_modified = true;
745 }
746 public function isModified(): bool { return $this->is_modified; }
747
748 /**
749 * Set meta data for the invoice
750 *
751 * @since 1.0.0
752 * @param string $key Meta key
753 * @param mixed $value Meta value
754 */
755 public function setMetaData(string $key, $value): void {
756 // Store in dynamic data array without the _easy_invoice_ prefix
757 $field_name = easy_invoice_str_replace('_easy_invoice_', '', $key);
758 $this->data[$field_name] = $value;
759 $this->is_modified = true;
760 }
761
762 /**
763 * Get currency code
764 *
765 * @since 1.0.0
766 * @return string Currency code
767 */
768 public function getCurrencyCode(): string {
769 $currency_code = $this->data['currency_code'] ?? 'USD';
770
771 // If currency is set to 'global', resolve to actual global setting
772 if ($currency_code === 'global') {
773 $currency_code = get_option('easy_invoice_currency_code', 'USD');
774 }
775
776 // Ensure currency code is uppercase for consistency
777 return strtoupper($currency_code);
778 }
779
780 /**
781 * Set currency code
782 *
783 * @since 1.0.0
784 * @param string $currency_code Currency code
785 */
786 public function setCurrencyCode(string $currency_code): void {
787 $this->data['currency_code'] = $currency_code;
788 $this->is_modified = true;
789 }
790
791 /**
792 * Get currency position
793 *
794 * @since 1.0.0
795 * @return string Currency position
796 */
797 public function getCurrencyPosition(): string {
798 $currency_position = $this->data['currency_position'] ?? 'left';
799
800 // If currency position is set to 'global', resolve to actual global setting
801 if ($currency_position === 'global') {
802 $currency_position = get_option('easy_invoice_currency_position', 'left');
803 }
804
805 // Map form format to global settings format
806 if ($currency_position === 'before') {
807 $currency_position = 'left';
808 } elseif ($currency_position === 'after') {
809 $currency_position = 'right';
810 }
811
812 return $currency_position;
813 }
814
815 /**
816 * Set currency position
817 *
818 * @since 1.0.0
819 * @param string $currency_position Currency position
820 */
821 public function setCurrencyPosition(string $currency_position): void {
822 $this->data['currency_position'] = $currency_position;
823 $this->is_modified = true;
824 }
825
826 /**
827 * Get raw currency code (without resolving global)
828 *
829 * @since 1.0.0
830 * @return string Raw currency code
831 */
832 public function getRawCurrencyCode(): string {
833 return $this->data['currency_code'] ?? 'global';
834 }
835
836 /**
837 * Get raw currency position (without resolving global)
838 *
839 * @since 1.0.0
840 * @return string Raw currency position
841 */
842 public function getRawCurrencyPosition(): string {
843 return $this->data['currency_position'] ?? 'global';
844 }
845
846 public function getDescription(): string {
847 return $this->data['description'] ?? '';
848 }
849
850 public function setDescription(string $description): void {
851 $this->data['description'] = $description;
852 $this->is_modified = true;
853 }
854
855 public function getTemplate(): string {
856 return $this->data['invoice_template'] ?? 'standard';
857 }
858
859 public function setTemplate(string $template): void {
860 $this->data['invoice_template'] = $template;
861 $this->is_modified = true;
862 }
863
864 /**
865 * Get discount type (percentage, fixed, none)
866 *
867 * @since 1.0.0
868 * @return string
869 */
870 public function getDiscountType(): string {
871 return $this->data['discount_type'] ?? 'none';
872 }
873
874 /**
875 * Get discount value (percentage or fixed amount)
876 *
877 * @since 1.0.0
878 * @return float
879 */
880 public function getDiscountValue(): float {
881 return floatval($this->data['discount_value'] ?? 0);
882 }
883
884 /**
885 * Get discount amount (calculated value)
886 *
887 * @since 1.0.0
888 * @return float
889 */
890 public function getDiscountAmount(): float {
891 $this->calculateTotals();
892 return floatval($this->data['discount_amount'] ?? 0);
893 }
894
895 /**
896 * Get tax rate percentage
897 *
898 * @since 1.0.0
899 * @return float
900 */
901 public function getTaxRate(): float {
902 return floatval($this->data['tax_rate'] ?? 0);
903 }
904
905 /**
906 * Get tax amount (calculated value)
907 *
908 * @since 1.0.0
909 * @return float
910 */
911 public function getTaxAmount(): float {
912 $this->calculateTotals();
913 return floatval($this->data['tax_amount'] ?? 0);
914 }
915
916 /**
917 * Get the total amount for this invoice
918 * Calculate from items
919 *
920 * @since 1.0.0
921 * @return float
922 */
923 public function getTotal(): float {
924 // Calculate from items
925 $this->calculateTotals();
926 $total = (float) ($this->data['total'] ?? 0);
927
928 // Allow plugins to modify the total
929 return apply_filters('easy_invoice_invoice_total', $total, $this);
930 }
931
932
933 }