PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / EasyInvoice.php

EasyInvoice.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/EasyInvoice.php

871 lines 35.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main EasyInvoice Plugin Class
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;
13
14 use EasyInvoice\PostTypes\InvoicePostType;
15 use EasyInvoice\PostTypes\PaymentPostType;
16
17 /**
18 * EasyInvoice main plugin class.
19 *
20 * @since 1.0.0
21 */
22 class EasyInvoice {
23 /**
24 * Plugin instance.
25 *
26 * @since 1.0.0
27 * @access private
28 * @var EasyInvoice
29 */
30 private static $instance = null;
31
32 /**
33 * Payment gateway manager instance.
34 *
35 * @since 1.0.0
36 * @access private
37 * @var PaymentGatewayManager
38 */
39 private $gateway_manager;
40
41 /**
42 * Get plugin instance.
43 *
44 * @since 1.0.0
45 * @return EasyInvoice
46 */
47 public static function getInstance(): EasyInvoice {
48 if ( null === self::$instance ) {
49 self::$instance = new self();
50 }
51 return self::$instance;
52 }
53
54 /**
55 * Initialize the plugin.
56 *
57 * @since 1.0.0
58 * @return void
59 */
60 public function init() {
61 // Define plugin constants.
62 $this->defineConstants();
63
64 // Initialize payment gateways first.
65 $this->initPaymentGateways();
66
67 // Note: Post types are now registered separately with proper timing
68
69 // Initialize template loader
70 $template_loader = new TemplateLoader();
71 $template_loader->init();
72
73 // Flush rewrite rules on first load to ensure public quote URLs work
74 if (get_option('easy_invoice_flush_rewrite_rules', false) === false) {
75 add_action('init', function() {
76 flush_rewrite_rules();
77 update_option('easy_invoice_flush_rewrite_rules', true);
78 }, 20);
79 }
80
81 // Also flush rewrite rules when post types are registered
82 add_action('init', function() {
83 if (get_option('easy_invoice_post_types_registered', false) === false) {
84 flush_rewrite_rules();
85 update_option('easy_invoice_post_types_registered', true);
86 }
87 }, 25);
88
89 // Initialize admin functionality if in admin area.
90 if ( is_admin() ) {
91 $this->initAdmin();
92 }
93
94
95
96 // Initialize background services
97 \EasyInvoice\Services\QuoteExpirationService::init();
98 }
99
100 /**
101 * Define plugin constants.
102 *
103 * @since 1.0.0
104 * @access private
105 * @return void
106 */
107 private function defineConstants(): void {
108 if ( ! defined( 'EASY_INVOICE_VERSION' ) ) {
109 $fallback = '2.1.18';
110 if ( defined( 'EASY_INVOICE_PLUGIN_FILE' ) && function_exists( 'get_file_data' ) ) {
111 $headers = get_file_data(
112 EASY_INVOICE_PLUGIN_FILE,
113 array( 'version' => 'Version' ),
114 'plugin'
115 );
116 $fallback = ! empty( $headers['version'] ) ? $headers['version'] : $fallback;
117 }
118 define( 'EASY_INVOICE_VERSION', $fallback );
119 }
120
121 if ( ! defined( 'EASY_INVOICE_FILE' ) ) {
122 define( 'EASY_INVOICE_FILE', dirname( dirname( __FILE__ ) ) . '/easy-invoice.php' );
123 }
124
125 if ( ! defined( 'EASY_INVOICE_PATH' ) ) {
126 define( 'EASY_INVOICE_PATH', plugin_dir_path( EASY_INVOICE_FILE ) );
127 }
128
129 if ( ! defined( 'EASY_INVOICE_URL' ) ) {
130 define( 'EASY_INVOICE_URL', plugin_dir_url( EASY_INVOICE_FILE ) );
131 }
132
133 if ( ! defined( 'EASY_INVOICE_ASSETS_URL' ) ) {
134 define( 'EASY_INVOICE_ASSETS_URL', EASY_INVOICE_URL . 'assets/' );
135 }
136 }
137
138 /**
139 * Initialize admin functionality.
140 *
141 * @since 1.0.0
142 * @access private
143 * @return void
144 */
145 private function initAdmin() {
146 // Load admin class if we're in the admin area.
147 $admin = new Admin\EasyInvoiceAdmin();
148 $admin->init();
149
150 // Initialize AJAX handlers.
151 $ajax = new Admin\EasyInvoiceAjax();
152 $ajax->init();
153
154 // Show draft invoices in the main list.
155 add_action( 'pre_get_posts', [ $this, 'modifyInvoiceAdminQuery' ] );
156
157 // Add custom columns to invoice list.
158 add_filter( 'manage_easy_invoice_posts_columns', [ $this, 'addInvoiceColumns' ] );
159 add_action( 'manage_easy_invoice_posts_custom_column', [ $this, 'populateInvoiceColumns' ], 10, 2 );
160
161 // Add custom columns to payment list.
162 add_filter( 'manage_easy_invoice_payment_posts_columns', [ $this, 'addPaymentColumns' ] );
163 add_action( 'manage_easy_invoice_payment_posts_custom_column', [ $this, 'populatePaymentColumns' ], 10, 2 );
164
165 // Add admin action to flush rewrite rules
166 add_action('admin_post_flush_easy_invoice_rewrite_rules', [$this, 'handleFlushRewriteRules']);
167
168 // NOTE: 'admin_post_fix_easy_invoice_quote_slugs' was registered here against
169 // [$this, 'handleFixQuoteSlugs'] — a method that does not exist on this class
170 // (or anywhere else in the plugin). Hitting that endpoint was an immediate
171 // fatal. Nothing in the UI ever linked to it, so the registration is removed
172 // rather than the method being written. Re-add both together if the
173 // quote-slug repair tool is ever actually needed.
174
175 // Add admin action to manually register post types
176 add_action('admin_post_register_easy_invoice_post_types', [$this, 'handleRegisterPostTypes']);
177
178 // Disable admin notices on Easy Invoice pages for clean UI
179 add_action( 'admin_notices', [ $this, 'disableAdminNoticesOnEasyInvoicePages' ], 1 );
180 }
181
182 /**
183 * Modify the main query for easy_invoice post type in admin to include drafts.
184 *
185 * @since 1.0.0
186 * @param \WP_Query $query The WP_Query instance (passed by reference).
187 * @return void
188 */
189 public function modifyInvoiceAdminQuery( \WP_Query $query ) {
190 // Check if we are on the main query for the 'easy_invoice' post type admin page.
191 if ( ! is_admin() || ! $query->is_main_query() || $query->get( 'post_type' ) !== 'easy_invoice' ) {
192 return;
193 }
194
195 // Check if a specific post_status is already set in the query.
196 $current_status = $query->get( 'post_status' );
197 if ( empty( $current_status ) || $current_status === '' ||
198 ( is_array( $current_status ) && count( $current_status ) === 1 && $current_status[0] === 'any' ) ) {
199
200 // Also check for explicit views like 'all'.
201 if ( isset( $_GET['post_status'] ) && $_GET['post_status'] !== 'all' ) {
202 return;
203 }
204
205 // Include all needed statuses.
206 $query->set( 'post_status', [
207 'publish',
208 'private',
209 'draft',
210 'pending',
211 'future',
212 'pending-bank',
213 'pending-cheque'
214 ] );
215 }
216 }
217
218 /**
219 * Initialize payment gateways.
220 *
221 * @since 1.0.0
222 * @access private
223 * @return void
224 */
225 private function initPaymentGateways() {
226 if ( ! $this->gateway_manager ) {
227 $this->gateway_manager = new PaymentGatewayManager();
228
229 // Register payment gateways.
230 $gateways = [
231 new Gateways\PayPalGateway(),
232 new Gateways\ManualGateway(),
233 ];
234
235 foreach ( $gateways as $gateway ) {
236 $this->gateway_manager->registerGateway( $gateway );
237 }
238
239 // Initialize all gateways.
240 $this->gateway_manager->initGateways();
241
242 // Set default payment methods if not already set.
243 $this->setDefaultPaymentMethods();
244 }
245 }
246
247 /**
248 * Set default payment methods and their details.
249 *
250 * @since 1.0.0
251 * @access private
252 * @return void
253 */
254 private function setDefaultPaymentMethods() {
255 $payment_methods = get_option( 'easy_invoice_payment_methods', [] );
256
257 $defaults_updated = false;
258
259 // Bank Transfer, Cheque, and Cash payment defaults moved to Pro plugin
260
261 if ( $defaults_updated ) {
262 update_option( 'easy_invoice_payment_methods', array_unique( $payment_methods ) );
263 }
264 }
265
266 /**
267 * Get payment gateway manager.
268 *
269 * @since 1.0.0
270 * @return PaymentGatewayManager
271 */
272 public function getGatewayManager(): PaymentGatewayManager {
273 if ( ! $this->gateway_manager ) {
274 $this->initPaymentGateways();
275 }
276 return $this->gateway_manager;
277 }
278
279 /**
280 * Register custom post types.
281 *
282 * @since 1.0.0
283 * @return void
284 */
285 public function registerPostTypes() {
286 // Register Invoice post type.
287 register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, [
288 'labels' => [
289 'name' => _x( 'Invoices', 'post type general name', 'easy-invoice' ),
290 'singular_name' => _x( 'Invoice', 'post type singular name', 'easy-invoice' ),
291 'menu_name' => _x( 'Invoices', 'admin menu', 'easy-invoice' ),
292 'name_admin_bar' => _x( 'Invoice', 'add new on admin bar', 'easy-invoice' ),
293 'add_new' => _x( 'Add New', 'invoice', 'easy-invoice' ),
294 'add_new_item' => __( 'Add New Invoice', 'easy-invoice' ),
295 'new_item' => __( 'New Invoice', 'easy-invoice' ),
296 'edit_item' => __( 'Edit Invoice', 'easy-invoice' ),
297 'view_item' => __( 'View Invoice', 'easy-invoice' ),
298 'all_items' => __( 'All Invoices', 'easy-invoice' ),
299 'search_items' => __( 'Search Invoices', 'easy-invoice' ),
300 'parent_item_colon' => __( 'Parent Invoices:', 'easy-invoice' ),
301 'not_found' => __( 'No invoices found.', 'easy-invoice' ),
302 'not_found_in_trash' => __( 'No invoices found in Trash.', 'easy-invoice' )
303 ],
304 'description' => __( 'Invoices for Easy Invoice plugin.', 'easy-invoice' ),
305 'public' => true,
306 'publicly_queryable' => true,
307 'show_ui' => false,
308 'show_in_menu' => false,
309 'query_var' => true,
310 'rewrite' => [ 'slug' => 'invoice' ],
311 'capability_type' => 'post',
312 'has_archive' => false,
313 // Keep invoices out of site search, search feeds and any archive-style
314 // query. They are only ever reachable as an authorised single document
315 // (see TemplateLoader::enforceDocumentAccess). Without this, `?s=INV`
316 // and `?feed=rss2&post_type=easy_invoice` listed invoice titles and
317 // permalinks to anonymous visitors — enough to enumerate every invoice
318 // on the site even though the documents themselves are gated.
319 'exclude_from_search' => true,
320 'hierarchical' => false,
321 'menu_position' => null,
322 'supports' => [ 'title', 'editor', 'custom-fields' ]
323 ]);
324
325 // Register Quote post type.
326 $quote_post_type = \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE;
327
328 $result = register_post_type( $quote_post_type, [
329 'labels' => [
330 'name' => _x( 'Quotes', 'post type general name', 'easy-invoice' ),
331 'singular_name' => _x( 'Quote', 'post type singular name', 'easy-invoice' ),
332 'menu_name' => _x( 'Quotes', 'admin menu', 'easy-invoice' ),
333 'name_admin_bar' => _x( 'Quote', 'add new on admin bar', 'easy-invoice' ),
334 'add_new' => _x( 'Add New', 'quote', 'easy-invoice' ),
335 'add_new_item' => __( 'Add New Quote', 'easy-invoice' ),
336 'new_item' => __( 'New Quote', 'easy-invoice' ),
337 'edit_item' => __( 'Edit Quote', 'easy-invoice' ),
338 'view_item' => __( 'View Quote', 'easy-invoice' ),
339 'all_items' => __( 'All Quotes', 'easy-invoice' ),
340 'search_items' => __( 'Search Quotes', 'easy-invoice' ),
341 'parent_item_colon' => __( 'Parent Quotes:', 'easy-invoice' ),
342 'not_found' => __( 'No quotes found.', 'easy-invoice' ),
343 'not_found_in_trash' => __( 'No quotes found in Trash.', 'easy-invoice' )
344 ],
345 'description' => __( 'Quotes for Easy Invoice plugin.', 'easy-invoice' ),
346 'public' => true,
347 'publicly_queryable' => true,
348 'show_ui' => false,
349 'show_in_menu' => false,
350 'query_var' => true,
351 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ],
352 'capability_type' => 'post',
353 'has_archive' => false,
354 // Same reasoning as the invoice post type above.
355 'exclude_from_search' => true,
356 'hierarchical' => false,
357 'menu_position' => null,
358 'supports' => [ 'title', 'editor', 'custom-fields' ]
359 ]);
360
361
362
363 // Register Payment post type.
364 register_post_type( 'easy_invoice_payment', [
365 'labels' => [
366 'name' => _x( 'Payments', 'post type general name', 'easy-invoice' ),
367 'singular_name' => _x( 'Payment', 'post type singular name', 'easy-invoice' ),
368 'menu_name' => _x( 'Payments', 'admin menu', 'easy-invoice' ),
369 'name_admin_bar' => _x( 'Payment', 'add new on admin bar', 'easy-invoice' ),
370 'add_new' => _x( 'Add New', 'payment', 'easy-invoice' ),
371 'add_new_item' => __( 'Add New Payment', 'easy-invoice' ),
372 'new_item' => __( 'New Payment', 'easy-invoice' ),
373 'edit_item' => __( 'Edit Payment', 'easy-invoice' ),
374 'view_item' => __( 'View Payment', 'easy-invoice' ),
375 'all_items' => __( 'All Payments', 'easy-invoice' ),
376 'search_items' => __( 'Search Payments', 'easy-invoice' ),
377 'parent_item_colon' => __( 'Parent Payments:', 'easy-invoice' ),
378 'not_found' => __( 'No payments found.', 'easy-invoice' ),
379 'not_found_in_trash' => __( 'No payments found in Trash.', 'easy-invoice' )
380 ],
381 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ),
382 'public' => false,
383 'publicly_queryable' => false,
384 // Payments are managed on the plugin's own Payments screen; the
385 // stock post editor knows none of their fields and its "Add New"
386 // left nameless auto-drafts behind.
387 'show_ui' => false,
388 'show_in_menu' => false,
389 'query_var' => true,
390 'rewrite' => [ 'slug' => 'payment' ],
391 'capability_type' => 'post',
392 'has_archive' => false,
393 'hierarchical' => false,
394 'menu_position' => null,
395 'supports' => [ 'title', 'author', 'custom-fields' ],
396 'show_in_rest' => false,
397 ] );
398
399 // Credit notes. Not publicly queryable and with no rewrite: unlike an
400 // invoice, a credit note is never handed to a customer through a
401 // permalink — it reaches them as a PDF attached to the correction being
402 // explained, so there is no front-end URL to protect in the first place.
403 register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE, [
404 'labels' => [
405 'name' => _x( 'Credit Notes', 'post type general name', 'easy-invoice' ),
406 'singular_name' => _x( 'Credit Note', 'post type singular name', 'easy-invoice' ),
407 'menu_name' => _x( 'Credit Notes', 'admin menu', 'easy-invoice' ),
408 'all_items' => __( 'All Credit Notes', 'easy-invoice' ),
409 'edit_item' => __( 'Edit Credit Note', 'easy-invoice' ),
410 'view_item' => __( 'View Credit Note', 'easy-invoice' ),
411 'search_items' => __( 'Search Credit Notes', 'easy-invoice' ),
412 'not_found' => __( 'No credit notes found.', 'easy-invoice' ),
413 'not_found_in_trash' => __( 'No credit notes found in Trash.', 'easy-invoice' ),
414 ],
415 'description' => __( 'Credit notes issued against invoices.', 'easy-invoice' ),
416 'public' => false,
417 'publicly_queryable' => false,
418 'exclude_from_search'=> true,
419 'show_ui' => false,
420 'show_in_menu' => false,
421 'query_var' => false,
422 'rewrite' => false,
423 'capability_type' => 'post',
424 'has_archive' => false,
425 'hierarchical' => false,
426 'supports' => [ 'title', 'author', 'custom-fields' ],
427 'show_in_rest' => false,
428 ] );
429
430
431 // Flush rewrite rules after post type registration.
432 //
433 // This is throttled to once every 5 minutes (see flushRewriteRules below).
434 // An unconditional `flush_rewrite_rules(true)` used to follow this call,
435 // which meant every single request — this method runs on `init` priority 0 —
436 // regenerated the whole rule set and wrote the `rewrite_rules` option. That
437 // is one of the most expensive things a plugin can do per request, and it
438 // made the throttle above pointless.
439 //
440 // Activation still flushes explicitly (see easy_invoice_activate), so new
441 // installs and permalink changes are covered without the per-request cost.
442 $this->flushRewriteRules();
443 }
444
445 /**
446 * Flush rewrite rules to ensure custom post type URLs work
447 */
448 private function flushRewriteRules() {
449 // Only flush if we haven't done it recently
450 $last_flush = get_option('easy_invoice_last_rewrite_flush', 0);
451 $current_time = time();
452
453 if ($current_time - $last_flush > 300) { // 5 minutes
454 flush_rewrite_rules();
455 update_option('easy_invoice_last_rewrite_flush', $current_time);
456 }
457 }
458
459 /**
460 * Manually flush rewrite rules (public method for admin use)
461 */
462 public function forceFlushRewriteRules() {
463 flush_rewrite_rules();
464 update_option('easy_invoice_last_rewrite_flush', time());
465 update_option('easy_invoice_flush_rewrite_rules', false);
466 update_option('easy_invoice_post_types_registered', false);
467 }
468
469 /**
470 * Add columns to invoice list table.
471 *
472 * @since 1.0.0
473 * @param array $columns List of columns.
474 * @return array Modified list of columns.
475 */
476 public function addInvoiceColumns( $columns ) {
477 $date_column = isset( $columns['date'] ) ? $columns['date'] : '';
478 unset( $columns['date'] );
479
480 $columns['invoice_number'] = __( 'Invoice #', 'easy-invoice' );
481 $columns['client'] = __( 'Client', 'easy-invoice' );
482 $columns['amount'] = __( 'Amount', 'easy-invoice' );
483 $columns['status'] = __( 'Status', 'easy-invoice' );
484 $columns['issue_date'] = __( 'Issue Date', 'easy-invoice' );
485 $columns['due_date'] = __( 'Due Date', 'easy-invoice' );
486
487 if ( $date_column ) {
488 $columns['date'] = $date_column;
489 }
490
491 return $columns;
492 }
493
494 /**
495 * Populate custom columns for invoice list table.
496 *
497 * @since 1.0.0
498 * @param string $column Column name.
499 * @param int $post_id Post ID.
500 * @return void
501 */
502 public function populateInvoiceColumns( $column, $post_id ) {
503 switch ( $column ) {
504 case 'invoice_number':
505 $invoice_number = get_post_meta( $post_id, '_invoice_number', true );
506 echo esc_html( ! empty( $invoice_number ) ? $invoice_number : "INV-{$post_id}" );
507 break;
508 case 'client':
509 $client_id = get_post_meta( $post_id, '_client_id', true );
510 if ( $client_id ) {
511 $client = get_post( $client_id );
512 if ( $client ) {
513 echo esc_html( $client->post_title );
514 } else {
515 echo esc_html__('', 'easy-invoice');
516 }
517 } else {
518 echo esc_html__('', 'easy-invoice');
519 }
520 break;
521 case 'amount':
522 $total = get_post_meta( $post_id, '_invoice_total', true );
523 if ( ! empty( $total ) ) {
524 echo esc_html( '$' . number_format( $total, 2 ) );
525 } else {
526 echo esc_html__('', 'easy-invoice');
527 }
528 break;
529 case 'status':
530 $status = get_post_meta( $post_id, '_payment_status', true );
531 if ( ! empty( $status ) ) {
532 echo '<span class="invoice-status status-' . esc_attr( $status ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
533 } else {
534 echo esc_html__('', 'easy-invoice');
535 }
536 break;
537 case 'issue_date':
538 $issue_date = get_post_meta( $post_id, '_issue_date', true );
539 if ( ! empty( $issue_date ) ) {
540 echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $issue_date ) ) );
541 } else {
542 echo esc_html__('', 'easy-invoice');
543 }
544 break;
545 case 'due_date':
546 $due_date = get_post_meta( $post_id, '_due_date', true );
547 if ( ! empty( $due_date ) ) {
548 echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $due_date ) ) );
549 } else {
550 echo '';
551 }
552 break;
553 }
554 }
555
556 /**
557 * Add columns to payment list table.
558 *
559 * @since 1.0.0
560 * @param array $columns List of columns.
561 * @return array Modified list of columns.
562 */
563 public function addPaymentColumns( $columns ) {
564 $date_column = isset( $columns['date'] ) ? $columns['date'] : '';
565 unset( $columns['date'] );
566
567 $columns['invoice'] = __( 'Invoice', 'easy-invoice' );
568 $columns['amount'] = __( 'Amount', 'easy-invoice' );
569 $columns['method'] = __( 'Method', 'easy-invoice' );
570 $columns['status'] = __( 'Status', 'easy-invoice' );
571 $columns['transaction_id'] = __( 'Transaction ID', 'easy-invoice' );
572
573 if ( $date_column ) {
574 $columns['date'] = $date_column;
575 }
576
577 return $columns;
578 }
579
580 /**
581 * Populate custom columns for payment list table.
582 *
583 * @since 1.0.0
584 * @param string $column Column name.
585 * @param int $post_id Post ID.
586 * @return void
587 */
588 public function populatePaymentColumns( $column, $post_id ) {
589 switch ( $column ) {
590 case 'invoice':
591 $invoice_id = get_post_meta( $post_id, '_invoice_id', true );
592 if ( $invoice_id ) {
593 $invoice = get_post( $invoice_id );
594 if ( $invoice ) {
595 $invoice_number = get_post_meta( $invoice_id, '_invoice_number', true );
596 if ( ! $invoice_number ) {
597 $invoice_number = "INV-{$invoice_id}";
598 }
599 echo '<a href="' . esc_url( admin_url( 'post.php?post=' . $invoice_id . '&action=edit' ) ) . '">' . esc_html( $invoice_number ) . '</a>';
600 } else {
601 echo '';
602 }
603 } else {
604 echo '';
605 }
606 break;
607 case 'amount':
608 $amount = get_post_meta( $post_id, '_amount', true );
609 $currency = get_post_meta( $post_id, '_currency_symbol', true ) ?: '$';
610 if ( ! empty( $amount ) ) {
611 echo esc_html( $currency . number_format( $amount, 2 ) );
612 } else {
613 echo '';
614 }
615 break;
616 case 'method':
617 $method = get_post_meta( $post_id, '_payment_method', true );
618 if ( ! empty( $method ) ) {
619 echo esc_html( ucfirst( $method ) );
620 } else {
621 echo '';
622 }
623 break;
624 case 'status':
625 $status = get_post_meta( $post_id, '_status', true );
626 if ( ! empty( $status ) ) {
627 echo '<span class="payment-status status-' . esc_attr( $status ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
628 } else {
629 echo '';
630 }
631 break;
632 case 'transaction_id':
633 $transaction_id = get_post_meta( $post_id, '_transaction_id', true );
634 if ( ! empty( $transaction_id ) ) {
635 echo esc_html( $transaction_id );
636 } else {
637 echo '';
638 }
639 break;
640 }
641 }
642
643 /**
644 * Register custom post statuses.
645 *
646 * @since 1.0.0
647 * @return void
648 */
649 public function registerCustomStatuses() {
650 // Register custom post statuses for invoices and payments.
651 $statuses = [
652 'pending-bank' => [
653 'label' => _x( 'Pending Bank Transfer', 'Invoice status', 'easy-invoice' ),
654 'public' => true,
655 'exclude_from_search' => false,
656 'show_in_admin_all_list' => true,
657 'show_in_admin_status_list' => true,
658 /* translators: %s: number of items. */
659 'label_count' => _n_noop(
660 'Pending Bank Transfer <span class="count">(%s)</span>',
661 'Pending Bank Transfer <span class="count">(%s)</span>',
662 'easy-invoice'
663 ),
664 ],
665 'pending-cheque' => [
666 'label' => _x( 'Pending Cheque', 'Invoice status', 'easy-invoice' ),
667 'public' => true,
668 'exclude_from_search' => false,
669 'show_in_admin_all_list' => true,
670 'show_in_admin_status_list' => true,
671 /* translators: %s: number of items. */
672 'label_count' => _n_noop(
673 'Pending Cheque <span class="count">(%s)</span>',
674 'Pending Cheque <span class="count">(%s)</span>',
675 'easy-invoice'
676 ),
677 ],
678 ];
679
680 foreach ( $statuses as $status => $args ) {
681 register_post_status( $status, $args );
682 }
683 }
684
685 /**
686 * Handle admin action to flush rewrite rules
687 */
688 public function handleFlushRewriteRules() {
689 if (!current_user_can('manage_options')) {
690 wp_die('Unauthorized');
691 }
692
693 check_admin_referer('flush_easy_invoice_rewrite_rules');
694
695 $this->forceFlushRewriteRules();
696
697 // Get the referer to determine which page to redirect back to
698 $referer = wp_get_referer();
699 $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); // Default fallback
700
701 if ($referer) {
702 // Check if user was on invoice page
703 if (strpos($referer, 'page=easy-invoice-all') !== false) {
704 $redirect_url = admin_url('admin.php?page=easy-invoice-all&rewrite_flushed=1');
705 } elseif (strpos($referer, 'page=easy-quote-all') !== false) {
706 $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1');
707 }
708 }
709
710 wp_safe_redirect($redirect_url);
711 exit;
712 }
713
714 /**
715 * Handle admin action to manually register post types
716 */
717 public function handleRegisterPostTypes() {
718 if (!current_user_can('manage_options')) {
719 wp_die('Unauthorized');
720 }
721
722 check_admin_referer('register_easy_invoice_post_types');
723
724 $this->registerPostTypes();
725 $this->forceFlushRewriteRules();
726
727 // Get the referer to determine which page to redirect back to
728 $referer = wp_get_referer();
729 $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); // Default fallback
730
731 if ($referer) {
732 // Check if user was on invoice page
733 if (strpos($referer, 'page=easy-invoice-all') !== false) {
734 $redirect_url = admin_url('admin.php?page=easy-invoice-all&post_types_registered=1');
735 } elseif (strpos($referer, 'page=easy-quote-all') !== false) {
736 $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1');
737 }
738 }
739
740 wp_safe_redirect($redirect_url);
741 exit;
742 }
743
744 /**
745 * Disable admin notices on Easy Invoice pages for clean UI.
746 *
747 * @since 1.0.0
748 * @return void
749 */
750 public function disableAdminNoticesOnEasyInvoicePages() {
751 // Get current page
752 $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
753
754 // Static list of core Easy Invoice page slugs. Addon-owned page
755 // slugs (Item Library, Template Builder, enterprise addon pages,
756 // export manager, etc.) are appended dynamically below from
757 // AddonRegistry so new addons automatically suppress WP notices
758 // on their pages without needing to edit this list.
759 $easy_invoice_pages = [
760 'easy-invoice', // Dashboard
761 'easy-invoice-all', // All invoices
762 'easy-invoice-builder', // Invoice builder
763 'easy-invoice-preview', // Invoice preview
764 'easy-quote-all', // All quotes
765 'easy-invoice-quote-builder', // Quote builder
766 'easy-quote-preview', // Quote preview
767 'easy-invoice-payments', // All payments
768 'easy-invoice-payment-new', // Add new payment
769 'easy-invoice-clients', // All clients
770 'easy-invoice-client-edit', // Edit client
771 'easy-invoice-client-view', // View client
772 'easy-invoice-reports', // Reports
773 'easy-invoice-settings', // Main settings
774 'easy-invoice-email-settings', // Email settings
775 'easy-invoice-email-settings-general',
776 'easy-invoice-email-settings-invoice',
777 'easy-invoice-email-settings-quote',
778 'easy-invoice-email-settings-payment',
779 'easy-invoice-pro-settings', // Pro settings
780 'easy-invoice-pro-translations',// Pro translations
781 'easy-invoice-pro-item-library',// Item Library (Pro)
782 'easy-invoice-templates', // Template Builder listing (Pro)
783 'easy-invoice-templates-new', // Template Builder "Create New" (Pro)
784 'easy-invoice-template-builder',// Template Builder canvas (Pro)
785 'easy-invoice-export', // Export Data (bulk_operations addon)
786 'easy-invoice-addons', // Addons grid
787 'easy-invoice-migration', // Migration page
788 'easy-invoice-license', // License page
789 'easy-invoice-free-vs-pro',
790 'easy-invoice-join-community',
791 'easy-invoice-import',
792 ];
793
794 // Dynamically include every addon's `settings_url` page slug.
795 // This means enterprise addons (time-tracking, dunning, white-label,
796 // team-roles, webhooks) and any future addon automatically get
797 // notice-suppression without needing to update this array.
798 if ( class_exists( '\\EasyInvoice\\Addons\\AddonRegistry' ) ) {
799 foreach ( \EasyInvoice\Addons\AddonRegistry::all() as $ei_addon ) {
800 if ( empty( $ei_addon['settings_url'] ) ) {
801 continue;
802 }
803 $parts = wp_parse_url( $ei_addon['settings_url'] );
804 if ( empty( $parts['query'] ) ) {
805 continue;
806 }
807 parse_str( $parts['query'], $qs );
808 if ( ! empty( $qs['page'] ) ) {
809 $easy_invoice_pages[] = $qs['page'];
810 }
811 }
812 $easy_invoice_pages = array_values( array_unique( $easy_invoice_pages ) );
813 }
814
815 // Any Easy Invoice screen, including ones the list above cannot know about.
816 //
817 // The list is built from each addon's `settings_url`, which is only an addon's
818 // PRIMARY page. An addon that registers a second screen — Accounting Sync's
819 // "Sync Log" is the one that exists today — was therefore left out, and WordPress
820 // rendered its notices there. Those notices are emitted before this plugin's
821 // markup, so they land outside the app shell's content column and are clipped by
822 // the fixed sidebar: the page opened with truncated text across the top and the
823 // real heading pushed far down. It read as a broken page rather than a styled one.
824 //
825 // Matching on the slug prefix instead covers every Easy Invoice screen that
826 // exists now and any added later, and it lines up with how AdminAssets decides
827 // to load the admin CSS (`strpos($hook, 'easy-invoice') !== false`) — the two
828 // should always agree on what counts as one of our screens.
829 $is_easy_invoice_page = in_array( $page, $easy_invoice_pages, true )
830 || strpos( $page, 'easy-invoice' ) === 0
831 || strpos( $page, 'easy-quote' ) === 0;
832
833 if ( $is_easy_invoice_page ) {
834 // Don't remove our review notice - keep it for free users
835 // Store our notice callback temporarily
836 $review_notice_callback = false;
837 $review_notice_priority = false;
838
839 // Find our review notice hook
840 global $wp_filter;
841 if (isset($wp_filter['admin_notices']->callbacks)) {
842 foreach ($wp_filter['admin_notices']->callbacks as $priority => $callbacks) {
843 foreach ($callbacks as $callback) {
844 if (is_array($callback['function']) &&
845 is_object($callback['function'][0]) &&
846 $callback['function'][0] instanceof \EasyInvoice\Services\ReviewNoticeService &&
847 $callback['function'][1] === 'displayAdminNotice') {
848 $review_notice_callback = $callback['function'];
849 $review_notice_priority = $priority;
850 break 2;
851 }
852 }
853 }
854 }
855
856 // Remove all admin notices by removing the action
857 remove_all_actions( 'admin_notices' );
858
859 // Also remove network and user admin notices
860 remove_all_actions( 'network_admin_notices' );
861 remove_all_actions( 'user_admin_notices' );
862 remove_all_actions( 'all_admin_notices' );
863
864 // Re-add our review notice if it was found
865 if ($review_notice_callback !== false && $review_notice_priority !== false) {
866 add_action('admin_notices', $review_notice_callback, $review_notice_priority);
867 }
868 }
869 }
870 }
871