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

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