• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Building custom automated PDF financial reports and invoices for WooCommerce using mpdf engine

Building custom automated PDF financial reports and invoices for WooCommerce using mpdf engine

Setting Up the Development Environment and Dependencies

To embark on building custom PDF financial reports and invoices for WooCommerce, we’ll leverage the mPDF library. This requires a robust PHP environment. Ensure you have Composer installed globally, as it’s the de facto standard for PHP dependency management. We’ll be creating a custom WordPress plugin to house our logic, keeping it separate from the WooCommerce core and theme files for maintainability and upgrade safety.

First, let’s create the basic structure of our WordPress plugin. Navigate to your WordPress installation’s wp-content/plugins/ directory and create a new folder, for instance, woocommerce-custom-pdf-reports. Inside this folder, create a main PHP file, woocommerce-custom-pdf-reports.php, with the standard plugin header.

<?php
/**
 * Plugin Name: WooCommerce Custom PDF Reports
 * Plugin URI: https://example.com/plugins/woocommerce-custom-pdf-reports/
 * Description: Generates custom PDF financial reports and invoices for WooCommerce orders.
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://example.com/
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain: woocommerce-custom-pdf-reports
 * Domain Path: /languages
 * WC requires at least: 3.0
 * WC tested up to: 8.0
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

// Plugin activation hook
register_activation_hook( __FILE__, 'wcpdr_activate' );
function wcpdr_activate() {
    // Initial setup on activation
}

// Plugin deactivation hook
register_deactivation_hook( __FILE__, 'wcpdr_deactivate' );
function wcpdr_deactivate() {
    // Cleanup on deactivation
}

// Main plugin logic will be included here or in separate files.
?>

Next, we need to manage the mPDF library. The recommended approach is to use Composer. Navigate to your plugin’s root directory in your terminal and run:

composer require mpdf/mpdf

This will create a vendor directory and a composer.json file. To load the mPDF library within your WordPress plugin, you’ll need to include the Composer autoloader. Add the following line to your main plugin file, preferably after the plugin header:

require_once __DIR__ . '/vendor/autoload.php';

This ensures that all classes from the mPDF library (and any other dependencies you might add later) are automatically available.

Integrating with WooCommerce Order Data

WooCommerce provides hooks and functions to access order data. We’ll create a function that can generate a PDF for a specific order ID. This function will fetch order details, customer information, line items, totals, and any relevant metadata. For this example, we’ll focus on generating an invoice, but the principles apply to other report types.

Let’s define a core function that takes an order ID and returns the PDF content. We’ll need to instantiate the mPDF class and then populate it with HTML content. The HTML will be dynamically generated based on the order data.

use Mpdf\Mpdf;

/**
 * Generates a PDF invoice for a given WooCommerce order.
 *
 * @param int $order_id The ID of the WooCommerce order.
 * @return string|false The PDF content as a string, or false on failure.
 */
function wcpdr_generate_invoice_pdf( $order_id ) {
    $order = wc_get_order( $order_id );

    if ( ! $order ) {
        error_log( "WCPDR: Order not found for ID: " . $order_id );
        return false;
    }

    // mPDF configuration
    $mpdf_config = [
        'mode' => 'utf-8',
        'format' => 'A4',
        'orientation' => 'P', // 'P' for Portrait, 'L' for Landscape
        'margin_left' => 15,
        'margin_right' => 15,
        'margin_top' => 16,
        'margin_bottom' => 16,
        'margin_header' => 9,
        'margin_footer' => 9,
        'default_font_size' => 0,
        'default_font' => '',
        'font_path' => '',
        'temp_dir' => '', // Specify a writable directory if needed
        'watermark_img' => '',
        'watermark_text' => '',
        'watermark_text_alpha' => 0.1,
        'default_merc' => false,
        'display_mode' => 'fullpage',
        'autoLangToFont' => true,
        'autoScriptToLang' => true,
        'autoarabic' => true,
        'tabSpaces' => 4,
        'showImageErrors' => true,
        'debug' => false,
        'errorReporting' => E_ALL & ~E_NOTICE,
    ];

    try {
        $mpdf = new Mpdf( $mpdf_config );
    } catch ( \Mpdf\MpdfException $e ) {
        error_log( "WCPDR: mPDF initialization error: " . $e->getMessage() );
        return false;
    }

    // Set some basic document properties
    $mpdf->SetCreator( get_bloginfo( 'name' ) );
    $mpdf->SetAuthor( get_bloginfo( 'name' ) );
    $mpdf->SetTitle( sprintf( __( 'Invoice for Order #%s', 'woocommerce-custom-pdf-reports' ), $order->get_order_number() ) );
    $mpdf->SetSubject( __( 'Invoice', 'woocommerce-custom-pdf-reports' ) );
    $mpdf->SetKeywords( __( 'invoice, order, woocommerce', 'woocommerce-custom-pdf-reports' ) );

    // Add header and footer (optional)
    $header_html = '<div style="text-align: right; font-weight: bold;">' . __( 'Invoice', 'woocommerce-custom-pdf-reports' ) . '</div>';
    $mpdf->SetHTMLHeader( $header_html );

    $footer_html = '<div style="text-align: center; font-size: 9pt;">Page {PAGENO} of {nbpg}</div>';
    $mpdf->SetHTMLFooter( $footer_html );

    // Generate HTML content for the invoice
    $html = wcpdr_get_invoice_html( $order );

    if ( ! $html ) {
        error_log( "WCPDR: Failed to generate HTML for order ID: " . $order_id );
        return false;
    }

    // Write the HTML to the PDF
    try {
        $mpdf->WriteHTML( $html );
    } catch ( \Mpdf\MpdfException $e ) {
        error_log( "WCPDR: mPDF WriteHTML error: " . $e->getMessage() );
        return false;
    }

    // Output the PDF
    // 'I' - Send the file inline to the browser. The default.
    // 'D' - Send to the browser and force a download.
    // 'F' - Save the file on the server.
    // 'S' - Return the document as a string.
    return $mpdf->Output( 'invoice-' . $order->get_order_number() . '.pdf', 'S' );
}

Dynamically Generating Invoice HTML

The core of customization lies in generating the HTML that mPDF will render. This function will fetch and format all necessary order details. We’ll include company information, customer details, order items, taxes, shipping, and totals. For a production-ready solution, consider making these elements configurable via plugin settings.

/**
 * Generates the HTML content for the invoice.
 *
 * @param WC_Order $order The WooCommerce order object.
 * @return string The HTML content for the invoice.
 */
function wcpdr_get_invoice_html( $order ) {
    $company_name = get_bloginfo( 'name' );
    $company_address = get_option( 'woocommerce_store_address' ) . ', ' . get_option( 'woocommerce_store_city' ) . ', ' . get_option( 'woocommerce_store_postcode' );
    $company_email = get_option( 'admin_email' ); // Or a specific store email option if available
    $company_phone = get_option( 'woocommerce_store_phone' );

    $billing_address = $order->get_formatted_billing_address();
    $shipping_address = $order->get_formatted_shipping_address();

    $order_date = $order->get_date_created()->date( 'Y-m-d H:i:s' );
    $order_number = $order->get_order_number();
    $order_status = wc_get_order_status_name( $order->get_status() );

    $items = $order->get_items();
    $totals = $order->get_order_item_totals();

    ob_start();
    ?>
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Invoice #</title>
        <style>
            body { font-family: sans-serif; line-height: 1.6; }
            .invoice-box { max-width: 800px; margin: auto; padding: 30px; border: 1px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, .15); font-size: 16px; line-height: 24px; color: #555; }
            .invoice-box table { width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }
            .invoice-box table td { padding: 5px; vertical-align: top; }
            .invoice-box table tr td:nth-child(2) { text-align: right; }
            .invoice-box table tr.top table tr td { padding-bottom: 20px; }
            .invoice-box table tr.top table tr.heading td { background: #eee; border-bottom: 1px solid #ddd; font-weight: bold; }
            .invoice-box table tr.item td { border-bottom: 1px solid #eee; }
            .invoice-box table tr.item.last td { border-bottom: none; }
            .invoice-box table tr.total td { border-top: 2px solid #eee; font-weight: bold; }
            .invoice-box table tr.text td { text-align: right; }
            .invoice-box table tr.text.last td { border-bottom: none; }
            .company-details { text-align: right; }
            .customer-details { text-align: left; }
            .order-details { text-align: right; }
            .items-table { width: 100%; border-collapse: collapse; }
            .items-table th, .items-table td { border: 1px solid #eee; padding: 8px; text-align: left; }
            .items-table th { background-color: #f2f2f2; }
            .items-table .text-right { text-align: right; }
        </style>
    </head>
    <body>
        <div class="invoice-box">
            <table cellpadding="0" cellspacing="0">
                <tr class="top">
                    <td colspan="2">
                        <table>
                            <tr>
                                <td class="company-details">
                                    <strong></strong><br>
                                    <br>
                                    <br>
                                    
                                </td>
                                <td class="order-details">
                                    Invoice:<br>
                                    #<br>
                                    <br>
                                    Status: 
                                </td>
                            </tr>
                        </table>
                    </td>
                </tr>
                <tr class="heading">
                    <td>Payment Method</td>
                    <td class="text-right">get_payment_method_title() ); ?></td>
                </tr>
                <tr class="item">
                    <td colspan="2">
                        <table class="items-table">
                            <thead>
                                <tr>
                                    <th></th>
                                    <th class="text-right"></th>
                                    <th class="text-right"></th>
                                    <th class="text-right"></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ( $items as $item_id => $item ) : ?>
                                    <tr class="item">
                                        <td>get_name() ); ?></td>
                                        <td class="text-right">get_quantity() ); ?></td>
                                        <td class="text-right">get_total() / $item->get_quantity(), array( 'currency' => $order->get_currency() ) ) ); ?></td>
                                        <td class="text-right">get_total(), array( 'currency' => $order->get_currency() ) ) ); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </td>
                </tr>
                <tr class="total">
                    <td colspan="2">
                        <table style="width: 50%; margin-left: auto;">
                            <?php foreach ( $totals as $key => $total ) : ?>
                                <tr>
                                    <td><?php echo esc_html( $total['label'] ); ?>:</td>
                                    <td class="text-right"><?php echo wp_kses_post( $total['value'] ); ?></td>
                                </tr>
                            <?php endforeach; ?>
                        </table>
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <h3></h3>
                        <p><?php echo nl2br( esc_html( $billing_address ) ); ?></p>
                    </td>
                </tr>
                <?php if ( $order->has_shipping_address() ) : ?>
                <tr>
                    <td colspan="2">
                        <h3></h3>
                        <p><?php echo nl2br( esc_html( $shipping_address ) ); ?></p>
                    </td>
                </tr>
                <?php endif; ?>
            </table>
        </div>
    </body>
    </html>
    



Triggering PDF Generation

To make this useful, we need a way to trigger the PDF generation. Common scenarios include: generating an invoice when an order is completed, providing a download link on the customer's "My Account" page, or offering a bulk generation tool in the admin area. For this example, we'll add a meta box to the order edit screen in the WordPress admin to generate an invoice on demand.

We'll use the add_meta_boxes action to add our meta box to the WooCommerce order edit screen.

/**
 * Add meta box to order edit screen.
 */
function wcpdr_add_order_meta_box() {
    add_meta_box(
        'wcpdr_invoice_generator',
        __( 'Custom PDF Invoice', 'woocommerce-custom-pdf-reports' ),
        'wcpdr_render_invoice_meta_box',
        'shop_order', // Post type
        'side',       // Context (normal, side, advanced)
        'default'     // Priority
    );
}
add_action( 'add_meta_boxes', 'wcpdr_add_order_meta_box' );

/**
 * Render the content of the meta box.
 *
 * @param WP_Post $post The current post object.
 */
function wcpdr_render_invoice_meta_box( $post ) {
    $order_id = $post->ID;
    $order = wc_get_order( $order_id );

    if ( ! $order ) {
        echo '<p>' . esc_html__( 'Order not found.', 'woocommerce-custom-pdf-reports' ) . '</p>';
        return;
    }

    // Check if the order is in a state where an invoice is typically generated (e.g., completed, processing)
    $allowed_statuses = apply_filters( 'wcpdr_allowed_invoice_statuses', array( 'completed', 'processing' ) );
    if ( ! in_array( $order->get_status(), $allowed_statuses ) ) {
        echo '<p>' . sprintf( esc_html__( 'Invoices can typically be generated for orders with status: %s.', 'woocommerce-custom-pdf-reports' ), implode( ', ', $allowed_statuses ) ) . '</p>';
        // Optionally, you could still allow generation but add a warning.
    }

    // Nonce for security
    wp_nonce_field( 'wcpdr_generate_invoice_nonce', 'wcpdr_invoice_nonce' );

    // Generate download link if PDF already exists (optional, requires saving to file)
    // For now, we'll just provide a button to generate and download.

    echo '<p>' . esc_html__( 'Click the button below to generate and download the PDF invoice for this order.', 'woocommerce-custom-pdf-reports' ) . '</p>';
    echo '<p class="submit">';
    // Using a form to submit the request
    echo '<form method="post" action="">';
    echo '<input type="hidden" name="wcpdr_order_id" value="' . esc_attr( $order_id ) . '" />';
    echo '<button type="submit" name="wcpdr_generate_invoice" class="button button-primary">' . esc_html__( 'Download Invoice PDF', 'woocommerce-custom-pdf-reports' ) . '</button>';
    echo '</form>';
    echo '</p>';
}

/**
 * Handle the PDF generation request from the meta box.
 */
function wcpdr_handle_invoice_generation() {
    // Check if our button was clicked
    if ( ! isset( $_POST['wcpdr_generate_invoice'] ) || ! isset( $_POST['wcpdr_order_id'] ) ) {
        return;
    }

    // Verify nonce
    if ( ! isset( $_POST['wcpdr_invoice_nonce'] ) || ! wp_verify_nonce( $_POST['wcpdr_invoice_nonce'], 'wcpdr_generate_invoice_nonce' ) ) {
        wp_die( esc_html__( 'Security check failed!', 'woocommerce-custom-pdf-reports' ) );
    }

    // Check user capabilities
    if ( ! current_user_can( 'edit_shop_orders' ) ) {
        wp_die( esc_html__( 'You do not have permission to generate invoices.', 'woocommerce-custom-pdf-reports' ) );
    }

    $order_id = intval( $_POST['wcpdr_order_id'] );
    $order = wc_get_order( $order_id );

    if ( ! $order ) {
        wp_die( esc_html__( 'Invalid order ID.', 'woocommerce-custom-pdf-reports' ) );
    }

    // Generate the PDF
    $pdf_content = wcpdr_generate_invoice_pdf( $order_id );

    if ( $pdf_content ) {
        // Output the PDF for download
        header( 'Content-Type: application/pdf' );
        header( 'Content-Disposition: attachment; filename="invoice-' . $order->get_order_number() . '.pdf"' );
        header( 'Content-Length: ' . strlen( $pdf_content ) );
        header( 'Cache-Control: private, max-age=0, must-revalidate' );
        header( 'Pragma: public' );
        echo $pdf_content;
        exit; // Important to exit after sending the file
    } else {
        wp_die( esc_html__( 'Failed to generate PDF invoice.', 'woocommerce-custom-pdf-reports' ) );
    }
}
// Hook into admin_init to handle the form submission
add_action( 'admin_init', 'wcpdr_handle_invoice_generation' );

Advanced Considerations and Customization

This basic setup provides a foundation. For a production-ready plugin, consider the following:

  • Styling: The provided CSS is inline for simplicity. For more complex designs, consider using external CSS files that mPDF can load, or embed more sophisticated CSS within the <style> tags. mPDF supports most CSS 2.1 features.
  • Internationalization: Ensure all strings are translatable using WordPress's internationalization functions (__(), _e(), esc_html__(), etc.) and provide a .pot file.
  • Error Handling: Implement more robust error logging and user feedback mechanisms.
  • Configuration: Allow users to configure company details, logo, invoice templates, and which order statuses trigger automatic PDF generation via the WordPress settings API.
  • Saving PDFs: Instead of forcing a download, you might want to save PDFs to a specific directory on the server (e.g., wp-content/uploads/woocommerce-pdf-invoices/) and link to them from the order details or customer account. This requires careful management of file permissions and storage.
  • Hooks and Filters: Expose filters throughout the PDF generation process (e.g., to modify order data before HTML generation, to add custom sections to the PDF, or to change mPDF configuration).
  • Performance: For sites with a very high volume of orders, consider asynchronous PDF generation using background job queues (e.g., WP-Cron with a robust queueing system or external services).
  • Security: Always sanitize and validate all inputs, especially when dealing with user-generated content or data fetched from the database. Use nonces for all form submissions.
  • Template System: For maximum flexibility, implement a template system where users can select and even edit different invoice/report templates.

By following these steps and considering the advanced points, you can build a powerful and flexible custom PDF reporting solution for WooCommerce.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala