PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
2.4.3 2.4.4 2.4.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 All 60 releases
easy-invoice / includes / EasyInvoice.php

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

886 lines 36.9 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: PayPal, then one gateway per offline
230 // method (bank transfer, cheque, cash and any the merchant added).
231 // The single "Manual Payment" gateway of earlier versions is gone;
232 // setDefaultPaymentMethods() carries its enabled state over.
233 $gateways = [ new Gateways\PayPalGateway() ];
234 foreach ( Services\OfflinePayments::methods() as $method ) {
235 $gateways[] = new Gateways\OfflineGateway( $method );
236 }
237
238 foreach ( $gateways as $gateway ) {
239 $this->gateway_manager->registerGateway( $gateway );
240 }
241
242 // Initialize all gateways.
243 $this->gateway_manager->initGateways();
244
245 // Set default payment methods if not already set.
246 $this->setDefaultPaymentMethods();
247 }
248 }
249
250 /**
251 * Set default payment methods and their details.
252 *
253 * @since 1.0.0
254 * @access private
255 * @return void
256 */
257 private function setDefaultPaymentMethods() {
258 $payment_methods = get_option( 'easy_invoice_payment_methods', null );
259 if ( null === $payment_methods ) {
260 // First run: an invoice with no way to pay is a dead end, and bank
261 // transfer needs no account with anyone. The merchant can switch it
262 // off under Settings → Payment Methods.
263 update_option( 'easy_invoice_payment_methods', [ 'bank_transfer' ] );
264 return;
265 }
266 $payment_methods = is_array( $payment_methods ) ? $payment_methods : [];
267
268 // Sites that had "Manual Payment" switched on keep an offline option:
269 // its three sub-choices are gateways of their own now.
270 if ( in_array( 'manual', $payment_methods, true ) ) {
271 $payment_methods = array_values( array_diff( $payment_methods, [ 'manual' ] ) );
272 foreach ( [ 'bank_transfer', 'cheque', 'cash' ] as $offline ) {
273 if ( ! in_array( $offline, $payment_methods, true ) ) {
274 $payment_methods[] = $offline;
275 }
276 }
277 update_option( 'easy_invoice_payment_methods', array_unique( $payment_methods ) );
278 }
279 }
280
281 /**
282 * Get payment gateway manager.
283 *
284 * @since 1.0.0
285 * @return PaymentGatewayManager
286 */
287 public function getGatewayManager(): PaymentGatewayManager {
288 if ( ! $this->gateway_manager ) {
289 $this->initPaymentGateways();
290 }
291 return $this->gateway_manager;
292 }
293
294 /**
295 * Register custom post types.
296 *
297 * @since 1.0.0
298 * @return void
299 */
300 public function registerPostTypes() {
301 // Register Invoice post type.
302 register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, [
303 'labels' => [
304 'name' => _x( 'Invoices', 'post type general name', 'easy-invoice' ),
305 'singular_name' => _x( 'Invoice', 'post type singular name', 'easy-invoice' ),
306 'menu_name' => _x( 'Invoices', 'admin menu', 'easy-invoice' ),
307 'name_admin_bar' => _x( 'Invoice', 'add new on admin bar', 'easy-invoice' ),
308 'add_new' => _x( 'Add New', 'invoice', 'easy-invoice' ),
309 'add_new_item' => __( 'Add New Invoice', 'easy-invoice' ),
310 'new_item' => __( 'New Invoice', 'easy-invoice' ),
311 'edit_item' => __( 'Edit Invoice', 'easy-invoice' ),
312 'view_item' => __( 'View Invoice', 'easy-invoice' ),
313 'all_items' => __( 'All Invoices', 'easy-invoice' ),
314 'search_items' => __( 'Search Invoices', 'easy-invoice' ),
315 'parent_item_colon' => __( 'Parent Invoices:', 'easy-invoice' ),
316 'not_found' => __( 'No invoices found.', 'easy-invoice' ),
317 'not_found_in_trash' => __( 'No invoices found in Trash.', 'easy-invoice' )
318 ],
319 'description' => __( 'Invoices for Easy Invoice plugin.', 'easy-invoice' ),
320 'public' => true,
321 'publicly_queryable' => true,
322 'show_ui' => false,
323 'show_in_menu' => false,
324 'query_var' => true,
325 'rewrite' => [ 'slug' => 'invoice' ],
326 'capability_type' => 'post',
327 'has_archive' => false,
328 // Keep invoices out of site search, search feeds and any archive-style
329 // query. They are only ever reachable as an authorised single document
330 // (see TemplateLoader::enforceDocumentAccess). Without this, `?s=INV`
331 // and `?feed=rss2&post_type=easy_invoice` listed invoice titles and
332 // permalinks to anonymous visitors — enough to enumerate every invoice
333 // on the site even though the documents themselves are gated.
334 'exclude_from_search' => true,
335 'hierarchical' => false,
336 'menu_position' => null,
337 'supports' => [ 'title', 'editor', 'custom-fields' ]
338 ]);
339
340 // Register Quote post type.
341 $quote_post_type = \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE;
342
343 $result = register_post_type( $quote_post_type, [
344 'labels' => [
345 'name' => _x( 'Quotes', 'post type general name', 'easy-invoice' ),
346 'singular_name' => _x( 'Quote', 'post type singular name', 'easy-invoice' ),
347 'menu_name' => _x( 'Quotes', 'admin menu', 'easy-invoice' ),
348 'name_admin_bar' => _x( 'Quote', 'add new on admin bar', 'easy-invoice' ),
349 'add_new' => _x( 'Add New', 'quote', 'easy-invoice' ),
350 'add_new_item' => __( 'Add New Quote', 'easy-invoice' ),
351 'new_item' => __( 'New Quote', 'easy-invoice' ),
352 'edit_item' => __( 'Edit Quote', 'easy-invoice' ),
353 'view_item' => __( 'View Quote', 'easy-invoice' ),
354 'all_items' => __( 'All Quotes', 'easy-invoice' ),
355 'search_items' => __( 'Search Quotes', 'easy-invoice' ),
356 'parent_item_colon' => __( 'Parent Quotes:', 'easy-invoice' ),
357 'not_found' => __( 'No quotes found.', 'easy-invoice' ),
358 'not_found_in_trash' => __( 'No quotes found in Trash.', 'easy-invoice' )
359 ],
360 'description' => __( 'Quotes for Easy Invoice plugin.', 'easy-invoice' ),
361 'public' => true,
362 'publicly_queryable' => true,
363 'show_ui' => false,
364 'show_in_menu' => false,
365 'query_var' => true,
366 'rewrite' => [ 'slug' => 'easy-invoice-quote', 'with_front' => false ],
367 'capability_type' => 'post',
368 'has_archive' => false,
369 // Same reasoning as the invoice post type above.
370 'exclude_from_search' => true,
371 'hierarchical' => false,
372 'menu_position' => null,
373 'supports' => [ 'title', 'editor', 'custom-fields' ]
374 ]);
375
376
377
378 // Register Payment post type.
379 register_post_type( 'easy_invoice_payment', [
380 'labels' => [
381 'name' => _x( 'Payments', 'post type general name', 'easy-invoice' ),
382 'singular_name' => _x( 'Payment', 'post type singular name', 'easy-invoice' ),
383 'menu_name' => _x( 'Payments', 'admin menu', 'easy-invoice' ),
384 'name_admin_bar' => _x( 'Payment', 'add new on admin bar', 'easy-invoice' ),
385 'add_new' => _x( 'Add New', 'payment', 'easy-invoice' ),
386 'add_new_item' => __( 'Add New Payment', 'easy-invoice' ),
387 'new_item' => __( 'New Payment', 'easy-invoice' ),
388 'edit_item' => __( 'Edit Payment', 'easy-invoice' ),
389 'view_item' => __( 'View Payment', 'easy-invoice' ),
390 'all_items' => __( 'All Payments', 'easy-invoice' ),
391 'search_items' => __( 'Search Payments', 'easy-invoice' ),
392 'parent_item_colon' => __( 'Parent Payments:', 'easy-invoice' ),
393 'not_found' => __( 'No payments found.', 'easy-invoice' ),
394 'not_found_in_trash' => __( 'No payments found in Trash.', 'easy-invoice' )
395 ],
396 'description' => __( 'Payments for Easy Invoice plugin.', 'easy-invoice' ),
397 'public' => false,
398 'publicly_queryable' => false,
399 // Payments are managed on the plugin's own Payments screen; the
400 // stock post editor knows none of their fields and its "Add New"
401 // left nameless auto-drafts behind.
402 'show_ui' => false,
403 'show_in_menu' => false,
404 'query_var' => true,
405 'rewrite' => [ 'slug' => 'payment' ],
406 'capability_type' => 'post',
407 'has_archive' => false,
408 'hierarchical' => false,
409 'menu_position' => null,
410 'supports' => [ 'title', 'author', 'custom-fields' ],
411 'show_in_rest' => false,
412 ] );
413
414 // Credit notes. Not publicly queryable and with no rewrite: unlike an
415 // invoice, a credit note is never handed to a customer through a
416 // permalink — it reaches them as a PDF attached to the correction being
417 // explained, so there is no front-end URL to protect in the first place.
418 register_post_type( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_CREDIT_NOTE_POST_TYPE, [
419 'labels' => [
420 'name' => _x( 'Credit Notes', 'post type general name', 'easy-invoice' ),
421 'singular_name' => _x( 'Credit Note', 'post type singular name', 'easy-invoice' ),
422 'menu_name' => _x( 'Credit Notes', 'admin menu', 'easy-invoice' ),
423 'all_items' => __( 'All Credit Notes', 'easy-invoice' ),
424 'edit_item' => __( 'Edit Credit Note', 'easy-invoice' ),
425 'view_item' => __( 'View Credit Note', 'easy-invoice' ),
426 'search_items' => __( 'Search Credit Notes', 'easy-invoice' ),
427 'not_found' => __( 'No credit notes found.', 'easy-invoice' ),
428 'not_found_in_trash' => __( 'No credit notes found in Trash.', 'easy-invoice' ),
429 ],
430 'description' => __( 'Credit notes issued against invoices.', 'easy-invoice' ),
431 'public' => false,
432 'publicly_queryable' => false,
433 'exclude_from_search'=> true,
434 'show_ui' => false,
435 'show_in_menu' => false,
436 'query_var' => false,
437 'rewrite' => false,
438 'capability_type' => 'post',
439 'has_archive' => false,
440 'hierarchical' => false,
441 'supports' => [ 'title', 'author', 'custom-fields' ],
442 'show_in_rest' => false,
443 ] );
444
445
446 // Flush rewrite rules after post type registration.
447 //
448 // This is throttled to once every 5 minutes (see flushRewriteRules below).
449 // An unconditional `flush_rewrite_rules(true)` used to follow this call,
450 // which meant every single request — this method runs on `init` priority 0 —
451 // regenerated the whole rule set and wrote the `rewrite_rules` option. That
452 // is one of the most expensive things a plugin can do per request, and it
453 // made the throttle above pointless.
454 //
455 // Activation still flushes explicitly (see easy_invoice_activate), so new
456 // installs and permalink changes are covered without the per-request cost.
457 $this->flushRewriteRules();
458 }
459
460 /**
461 * Flush rewrite rules to ensure custom post type URLs work
462 */
463 private function flushRewriteRules() {
464 // Only flush if we haven't done it recently
465 $last_flush = get_option('easy_invoice_last_rewrite_flush', 0);
466 $current_time = time();
467
468 if ($current_time - $last_flush > 300) { // 5 minutes
469 flush_rewrite_rules();
470 update_option('easy_invoice_last_rewrite_flush', $current_time);
471 }
472 }
473
474 /**
475 * Manually flush rewrite rules (public method for admin use)
476 */
477 public function forceFlushRewriteRules() {
478 flush_rewrite_rules();
479 update_option('easy_invoice_last_rewrite_flush', time());
480 update_option('easy_invoice_flush_rewrite_rules', false);
481 update_option('easy_invoice_post_types_registered', false);
482 }
483
484 /**
485 * Add columns to invoice list table.
486 *
487 * @since 1.0.0
488 * @param array $columns List of columns.
489 * @return array Modified list of columns.
490 */
491 public function addInvoiceColumns( $columns ) {
492 $date_column = isset( $columns['date'] ) ? $columns['date'] : '';
493 unset( $columns['date'] );
494
495 $columns['invoice_number'] = __( 'Invoice #', 'easy-invoice' );
496 $columns['client'] = __( 'Client', 'easy-invoice' );
497 $columns['amount'] = __( 'Amount', 'easy-invoice' );
498 $columns['status'] = __( 'Status', 'easy-invoice' );
499 $columns['issue_date'] = __( 'Issue Date', 'easy-invoice' );
500 $columns['due_date'] = __( 'Due Date', 'easy-invoice' );
501
502 if ( $date_column ) {
503 $columns['date'] = $date_column;
504 }
505
506 return $columns;
507 }
508
509 /**
510 * Populate custom columns for invoice list table.
511 *
512 * @since 1.0.0
513 * @param string $column Column name.
514 * @param int $post_id Post ID.
515 * @return void
516 */
517 public function populateInvoiceColumns( $column, $post_id ) {
518 switch ( $column ) {
519 case 'invoice_number':
520 $invoice_number = get_post_meta( $post_id, '_invoice_number', true );
521 echo esc_html( ! empty( $invoice_number ) ? $invoice_number : "INV-{$post_id}" );
522 break;
523 case 'client':
524 $client_id = get_post_meta( $post_id, '_client_id', true );
525 if ( $client_id ) {
526 $client = get_post( $client_id );
527 if ( $client ) {
528 echo esc_html( $client->post_title );
529 } else {
530 echo esc_html__('—', 'easy-invoice');
531 }
532 } else {
533 echo esc_html__('—', 'easy-invoice');
534 }
535 break;
536 case 'amount':
537 $total = get_post_meta( $post_id, '_invoice_total', true );
538 if ( ! empty( $total ) ) {
539 echo esc_html( '$' . number_format( $total, 2 ) );
540 } else {
541 echo esc_html__('—', 'easy-invoice');
542 }
543 break;
544 case 'status':
545 $status = get_post_meta( $post_id, '_payment_status', true );
546 if ( ! empty( $status ) ) {
547 echo '<span class="invoice-status status-' . esc_attr( $status ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
548 } else {
549 echo esc_html__('—', 'easy-invoice');
550 }
551 break;
552 case 'issue_date':
553 $issue_date = get_post_meta( $post_id, '_issue_date', true );
554 if ( ! empty( $issue_date ) ) {
555 echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $issue_date ) ) );
556 } else {
557 echo esc_html__('—', 'easy-invoice');
558 }
559 break;
560 case 'due_date':
561 $due_date = get_post_meta( $post_id, '_due_date', true );
562 if ( ! empty( $due_date ) ) {
563 echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $due_date ) ) );
564 } else {
565 echo '—';
566 }
567 break;
568 }
569 }
570
571 /**
572 * Add columns to payment list table.
573 *
574 * @since 1.0.0
575 * @param array $columns List of columns.
576 * @return array Modified list of columns.
577 */
578 public function addPaymentColumns( $columns ) {
579 $date_column = isset( $columns['date'] ) ? $columns['date'] : '';
580 unset( $columns['date'] );
581
582 $columns['invoice'] = __( 'Invoice', 'easy-invoice' );
583 $columns['amount'] = __( 'Amount', 'easy-invoice' );
584 $columns['method'] = __( 'Method', 'easy-invoice' );
585 $columns['status'] = __( 'Status', 'easy-invoice' );
586 $columns['transaction_id'] = __( 'Transaction ID', 'easy-invoice' );
587
588 if ( $date_column ) {
589 $columns['date'] = $date_column;
590 }
591
592 return $columns;
593 }
594
595 /**
596 * Populate custom columns for payment list table.
597 *
598 * @since 1.0.0
599 * @param string $column Column name.
600 * @param int $post_id Post ID.
601 * @return void
602 */
603 public function populatePaymentColumns( $column, $post_id ) {
604 switch ( $column ) {
605 case 'invoice':
606 $invoice_id = get_post_meta( $post_id, '_invoice_id', true );
607 if ( $invoice_id ) {
608 $invoice = get_post( $invoice_id );
609 if ( $invoice ) {
610 $invoice_number = get_post_meta( $invoice_id, '_invoice_number', true );
611 if ( ! $invoice_number ) {
612 $invoice_number = "INV-{$invoice_id}";
613 }
614 echo '<a href="' . esc_url( admin_url( 'post.php?post=' . $invoice_id . '&action=edit' ) ) . '">' . esc_html( $invoice_number ) . '</a>';
615 } else {
616 echo '—';
617 }
618 } else {
619 echo '—';
620 }
621 break;
622 case 'amount':
623 $amount = get_post_meta( $post_id, '_amount', true );
624 $currency = get_post_meta( $post_id, '_currency_symbol', true ) ?: '$';
625 if ( ! empty( $amount ) ) {
626 echo esc_html( $currency . number_format( $amount, 2 ) );
627 } else {
628 echo '—';
629 }
630 break;
631 case 'method':
632 $method = get_post_meta( $post_id, '_payment_method', true );
633 if ( ! empty( $method ) ) {
634 echo esc_html( ucfirst( $method ) );
635 } else {
636 echo '—';
637 }
638 break;
639 case 'status':
640 $status = get_post_meta( $post_id, '_status', true );
641 if ( ! empty( $status ) ) {
642 echo '<span class="payment-status status-' . esc_attr( $status ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
643 } else {
644 echo '—';
645 }
646 break;
647 case 'transaction_id':
648 $transaction_id = get_post_meta( $post_id, '_transaction_id', true );
649 if ( ! empty( $transaction_id ) ) {
650 echo esc_html( $transaction_id );
651 } else {
652 echo '—';
653 }
654 break;
655 }
656 }
657
658 /**
659 * Register custom post statuses.
660 *
661 * @since 1.0.0
662 * @return void
663 */
664 public function registerCustomStatuses() {
665 // Register custom post statuses for invoices and payments.
666 $statuses = [
667 'pending-bank' => [
668 'label' => _x( 'Pending Bank Transfer', 'Invoice status', 'easy-invoice' ),
669 'public' => true,
670 'exclude_from_search' => false,
671 'show_in_admin_all_list' => true,
672 'show_in_admin_status_list' => true,
673 /* translators: %s: number of items. */
674 'label_count' => _n_noop(
675 'Pending Bank Transfer <span class="count">(%s)</span>',
676 'Pending Bank Transfer <span class="count">(%s)</span>',
677 'easy-invoice'
678 ),
679 ],
680 'pending-cheque' => [
681 'label' => _x( 'Pending Cheque', 'Invoice status', 'easy-invoice' ),
682 'public' => true,
683 'exclude_from_search' => false,
684 'show_in_admin_all_list' => true,
685 'show_in_admin_status_list' => true,
686 /* translators: %s: number of items. */
687 'label_count' => _n_noop(
688 'Pending Cheque <span class="count">(%s)</span>',
689 'Pending Cheque <span class="count">(%s)</span>',
690 'easy-invoice'
691 ),
692 ],
693 ];
694
695 foreach ( $statuses as $status => $args ) {
696 register_post_status( $status, $args );
697 }
698 }
699
700 /**
701 * Handle admin action to flush rewrite rules
702 */
703 public function handleFlushRewriteRules() {
704 if (!current_user_can('manage_options')) {
705 wp_die('Unauthorized');
706 }
707
708 check_admin_referer('flush_easy_invoice_rewrite_rules');
709
710 $this->forceFlushRewriteRules();
711
712 // Get the referer to determine which page to redirect back to
713 $referer = wp_get_referer();
714 $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1'); // Default fallback
715
716 if ($referer) {
717 // Check if user was on invoice page
718 if (strpos($referer, 'page=easy-invoice-all') !== false) {
719 $redirect_url = admin_url('admin.php?page=easy-invoice-all&rewrite_flushed=1');
720 } elseif (strpos($referer, 'page=easy-quote-all') !== false) {
721 $redirect_url = admin_url('admin.php?page=easy-quote-all&rewrite_flushed=1');
722 }
723 }
724
725 wp_safe_redirect($redirect_url);
726 exit;
727 }
728
729 /**
730 * Handle admin action to manually register post types
731 */
732 public function handleRegisterPostTypes() {
733 if (!current_user_can('manage_options')) {
734 wp_die('Unauthorized');
735 }
736
737 check_admin_referer('register_easy_invoice_post_types');
738
739 $this->registerPostTypes();
740 $this->forceFlushRewriteRules();
741
742 // Get the referer to determine which page to redirect back to
743 $referer = wp_get_referer();
744 $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1'); // Default fallback
745
746 if ($referer) {
747 // Check if user was on invoice page
748 if (strpos($referer, 'page=easy-invoice-all') !== false) {
749 $redirect_url = admin_url('admin.php?page=easy-invoice-all&post_types_registered=1');
750 } elseif (strpos($referer, 'page=easy-quote-all') !== false) {
751 $redirect_url = admin_url('admin.php?page=easy-quote-all&post_types_registered=1');
752 }
753 }
754
755 wp_safe_redirect($redirect_url);
756 exit;
757 }
758
759 /**
760 * Disable admin notices on Easy Invoice pages for clean UI.
761 *
762 * @since 1.0.0
763 * @return void
764 */
765 public function disableAdminNoticesOnEasyInvoicePages() {
766 // Get current page
767 $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : '';
768
769 // Static list of core Easy Invoice page slugs. Addon-owned page
770 // slugs (Item Library, Template Builder, enterprise addon pages,
771 // export manager, etc.) are appended dynamically below from
772 // AddonRegistry so new addons automatically suppress WP notices
773 // on their pages without needing to edit this list.
774 $easy_invoice_pages = [
775 'easy-invoice', // Dashboard
776 'easy-invoice-all', // All invoices
777 'easy-invoice-builder', // Invoice builder
778 'easy-invoice-preview', // Invoice preview
779 'easy-quote-all', // All quotes
780 'easy-invoice-quote-builder', // Quote builder
781 'easy-quote-preview', // Quote preview
782 'easy-invoice-payments', // All payments
783 'easy-invoice-payment-new', // Add new payment
784 'easy-invoice-clients', // All clients
785 'easy-invoice-client-edit', // Edit client
786 'easy-invoice-client-view', // View client
787 'easy-invoice-reports', // Reports
788 'easy-invoice-settings', // Main settings
789 'easy-invoice-email-settings', // Email settings
790 'easy-invoice-email-settings-general',
791 'easy-invoice-email-settings-invoice',
792 'easy-invoice-email-settings-quote',
793 'easy-invoice-email-settings-payment',
794 'easy-invoice-pro-settings', // Pro settings
795 'easy-invoice-pro-translations',// Pro translations
796 'easy-invoice-pro-item-library',// Item Library (Pro)
797 'easy-invoice-templates', // Template Builder listing (Pro)
798 'easy-invoice-templates-new', // Template Builder "Create New" (Pro)
799 'easy-invoice-template-builder',// Template Builder canvas (Pro)
800 'easy-invoice-export', // Export Data (bulk_operations addon)
801 'easy-invoice-addons', // Addons grid
802 'easy-invoice-migration', // Migration page
803 'easy-invoice-license', // License page
804 'easy-invoice-free-vs-pro',
805 'easy-invoice-join-community',
806 'easy-invoice-import',
807 ];
808
809 // Dynamically include every addon's `settings_url` page slug.
810 // This means enterprise addons (time-tracking, dunning, white-label,
811 // team-roles, webhooks) and any future addon automatically get
812 // notice-suppression without needing to update this array.
813 if ( class_exists( '\\EasyInvoice\\Addons\\AddonRegistry' ) ) {
814 foreach ( \EasyInvoice\Addons\AddonRegistry::all() as $ei_addon ) {
815 if ( empty( $ei_addon['settings_url'] ) ) {
816 continue;
817 }
818 $parts = wp_parse_url( $ei_addon['settings_url'] );
819 if ( empty( $parts['query'] ) ) {
820 continue;
821 }
822 parse_str( $parts['query'], $qs );
823 if ( ! empty( $qs['page'] ) ) {
824 $easy_invoice_pages[] = $qs['page'];
825 }
826 }
827 $easy_invoice_pages = array_values( array_unique( $easy_invoice_pages ) );
828 }
829
830 // Any Easy Invoice screen, including ones the list above cannot know about.
831 //
832 // The list is built from each addon's `settings_url`, which is only an addon's
833 // PRIMARY page. An addon that registers a second screen — Accounting Sync's
834 // "Sync Log" is the one that exists today — was therefore left out, and WordPress
835 // rendered its notices there. Those notices are emitted before this plugin's
836 // markup, so they land outside the app shell's content column and are clipped by
837 // the fixed sidebar: the page opened with truncated text across the top and the
838 // real heading pushed far down. It read as a broken page rather than a styled one.
839 //
840 // Matching on the slug prefix instead covers every Easy Invoice screen that
841 // exists now and any added later, and it lines up with how AdminAssets decides
842 // to load the admin CSS (`strpos($hook, 'easy-invoice') !== false`) — the two
843 // should always agree on what counts as one of our screens.
844 $is_easy_invoice_page = in_array( $page, $easy_invoice_pages, true )
845 || strpos( $page, 'easy-invoice' ) === 0
846 || strpos( $page, 'easy-quote' ) === 0;
847
848 if ( $is_easy_invoice_page ) {
849 // Don't remove our review notice - keep it for free users
850 // Store our notice callback temporarily
851 $review_notice_callback = false;
852 $review_notice_priority = false;
853
854 // Find our review notice hook
855 global $wp_filter;
856 if (isset($wp_filter['admin_notices']->callbacks)) {
857 foreach ($wp_filter['admin_notices']->callbacks as $priority => $callbacks) {
858 foreach ($callbacks as $callback) {
859 if (is_array($callback['function']) &&
860 is_object($callback['function'][0]) &&
861 $callback['function'][0] instanceof \EasyInvoice\Services\ReviewNoticeService &&
862 $callback['function'][1] === 'displayAdminNotice') {
863 $review_notice_callback = $callback['function'];
864 $review_notice_priority = $priority;
865 break 2;
866 }
867 }
868 }
869 }
870
871 // Remove all admin notices by removing the action
872 remove_all_actions( 'admin_notices' );
873
874 // Also remove network and user admin notices
875 remove_all_actions( 'network_admin_notices' );
876 remove_all_actions( 'user_admin_notices' );
877 remove_all_actions( 'all_admin_notices' );
878
879 // Re-add our review notice if it was found
880 if ($review_notice_callback !== false && $review_notice_priority !== false) {
881 add_action('admin_notices', $review_notice_callback, $review_notice_priority);
882 }
883 }
884 }
885 }
886