• 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 FPDF customized scripts

Building custom automated PDF financial reports and invoices for WooCommerce using FPDF customized scripts

Leveraging FPDF for Custom WooCommerce PDF Reports

For e-commerce businesses operating on WooCommerce, the ability to generate custom, automated PDF financial reports and invoices is paramount for accounting, client communication, and internal record-keeping. While WooCommerce offers basic invoice generation, tailoring these documents to specific business needs often requires a more robust solution. This guide details how to integrate the FPDF library directly into custom WordPress plugins to achieve highly customized PDF outputs for WooCommerce orders.

Setting Up the Development Environment

Before diving into code, ensure your development environment is prepared. This typically involves a local WordPress installation with WooCommerce active. You’ll need a code editor and a way to manage custom code, ideally a custom plugin. Avoid modifying core WooCommerce or WordPress files directly; always use hooks and custom plugins.

Integrating FPDF into a Custom Plugin

The first step is to include the FPDF library within your custom plugin. The most straightforward method is to download the FPDF source files and place them in a dedicated directory within your plugin. We’ll then include these files and instantiate the FPDF class.

Plugin Structure and FPDF Inclusion

Create a directory for your plugin, e.g., wp-content/plugins/my-custom-reports/. Inside this, create your main plugin file (e.g., my-custom-reports.php) and a subdirectory for FPDF, e.g., my-custom-reports/includes/fpdf/. Place the FPDF library files (fpdf.php and the font/ directory) into includes/fpdf/.

Your main plugin file will look something like this:

<?php
/**
 * Plugin Name: My Custom WooCommerce Reports
 * Description: Generates custom PDF invoices and reports for WooCommerce orders.
 * Version: 1.0
 * Author: Your Name
 */

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Define plugin path constants
define( 'MY_CUSTOM_REPORTS_PATH', plugin_dir_path( __FILE__ ) );
define( 'MY_CUSTOM_REPORTS_URL', plugin_dir_url( __FILE__ ) );

// Include FPDF library
require_once MY_CUSTOM_REPORTS_PATH . 'includes/fpdf/fpdf.php';

// Load custom classes and functions
require_once MY_CUSTOM_REPORTS_PATH . 'includes/class-my-custom-report-generator.php';
require_once MY_CUSTOM_REPORTS_PATH . 'includes/functions.php';

// Initialize the report generator
add_action( 'plugins_loaded', array( 'My_Custom_Report_Generator', 'instance' ) );

// Add a custom link to the order actions in WooCommerce admin
add_filter( 'woocommerce_admin_order_actions', 'add_custom_report_action_link', 10, 2 );

function add_custom_report_action_link( $actions, $order ) {
    $actions['custom_report'] = array(
        'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=generate_custom_report&order_id=' . $order->get_id() ), 'generate_custom_report' ),
        'name'   => __( 'Custom Report', 'my-custom-reports' ),
        'icon'   => 'dashicons-download',
    );
    return $actions;
}

// AJAX handler for generating the report
add_action( 'wp_ajax_generate_custom_report', 'handle_custom_report_generation' );

function handle_custom_report_generation() {
    if ( ! isset( $_GET['order_id'] ) || ! isset( $_GET['_wpnonce'] ) ) {
        wp_die( __( 'Invalid request.', 'my-custom-reports' ) );
    }

    $order_id = absint( $_GET['order_id'] );

    if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'generate_custom_report' ) ) {
        wp_die( __( 'Security check failed.', 'my-custom-reports' ) );
    }

    if ( ! current_user_can( 'edit_shop_orders' ) ) {
        wp_die( __( 'You do not have permission to perform this action.', 'my-custom-reports' ) );
    }

    $order = wc_get_order( $order_id );

    if ( ! $order ) {
        wp_die( __( 'Order not found.', 'my-custom-reports' ) );
    }

    // Instantiate and generate the report
    $report_generator = new My_Custom_Report_Generator( $order );
    $report_generator->generate_invoice_pdf();
}
?>

Creating a Custom Report Generator Class

We’ll encapsulate the PDF generation logic within a dedicated class. This promotes code organization and reusability. This class will extend FPDF and handle fetching order data, formatting it, and rendering it onto the PDF document.

class-my-custom-report-generator.php

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class My_Custom_Report_Generator extends FPDF {

    protected $order;
    protected $margin_left = 15;
    protected $margin_top = 15;
    protected $margin_right = 15;
    protected $margin_bottom = 15;
    protected $current_y;

    public function __construct( $order ) {
        parent::__construct();
        $this->order = $order;
        $this->SetAutoPageBreak( true, $this->margin_bottom );
        $this->SetMargins( $this->margin_left, $this->margin_top, $this->margin_right );
        $this->current_y = $this->margin_top;
    }

    public static function instance( $order ) {
        return new self( $order );
    }

    public function generate_invoice_pdf() {
        $this->AddPage();
        $this->SetFont( 'Arial', '', 12 );

        $this->Header(); // Call custom header
        $this->Body();   // Call custom body
        $this->Footer(); // Call custom footer

        $this->Output( 'I', 'invoice-' . $this->order->get_id() . '.pdf' ); // 'I' for inline display
    }

    // Page header
    function Header() {
        // Company Logo
        $logo_path = MY_CUSTOM_REPORTS_URL . 'assets/images/company-logo.png'; // Ensure this path is correct
        if ( file_exists( str_replace( MY_CUSTOM_REPORTS_URL, WP_PLUGIN_DIR . '/', $logo_path ) ) ) {
            $this->Image( str_replace( MY_CUSTOM_REPORTS_URL, WP_PLUGIN_DIR . '/', $logo_path ), $this->margin_left, 10, 30 );
        }

        // Company Name and Address
        $this->SetFont( 'Arial', 'B', 16 );
        $this->Cell( 0, 10, get_bloginfo( 'name' ), 0, 1, 'R' );
        $this->SetFont( 'Arial', '', 10 );
        $this->Cell( 0, 5, 'Your Company Address Line 1', 0, 1, 'R' );
        $this->Cell( 0, 5, 'Your Company Address Line 2', 0, 1, 'R' );
        $this->Cell( 0, 5, 'Your City, Postal Code', 0, 1, 'R' );
        $this->Ln(10);

        // Invoice Title
        $this->SetFont( 'Arial', 'B', 20 );
        $this->Cell( 0, 10, __( 'INVOICE', 'my-custom-reports' ), 0, 1, 'C' );
        $this->Ln(10);

        // Order Details
        $this->SetFont( 'Arial', 'B', 12 );
        $this->Cell( 40, 7, __( 'Invoice Number:', 'my-custom-reports' ), 0, 0, 'L' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 60, 7, $this->order->get_id(), 0, 0, 'L' );
        $this->SetFont( 'Arial', 'B', 12 );
        $this->Cell( 40, 7, __( 'Invoice Date:', 'my-custom-reports' ), 0, 0, 'L' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 0, 7, $this->order->get_date_created()->format( 'Y-m-d' ), 0, 1, 'L' );

        $this->SetFont( 'Arial', 'B', 12 );
        $this->Cell( 40, 7, __( 'Order Number:', 'my-custom-reports' ), 0, 0, 'L' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 60, 7, $this->order->get_order_number(), 0, 0, 'L' );
        $this->SetFont( 'Arial', 'B', 12 );
        $this->Cell( 40, 7, __( 'Order Date:', 'my-custom-reports' ), 0, 0, 'L' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 0, 7, $this->order->get_date_created()->format( 'Y-m-d H:i:s' ), 0, 1, 'L' );
        $this->Ln(10);

        // Billing Address
        $this->SetFont( 'Arial', 'B', 12 );
        $this->Cell( 0, 7, __( 'Bill To:', 'my-custom-reports' ), 0, 1, 'L' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 0, 6, $this->order->get_billing_first_name() . ' ' . $this->order->get_billing_last_name(), 0, 1, 'L' );
        $this->Cell( 0, 6, $this->order->get_billing_company(), 0, 1, 'L' );
        $this->Cell( 0, 6, $this->order->get_billing_address_1(), 0, 1, 'L' );
        $this->Cell( 0, 6, $this->order->get_billing_address_2(), 0, 1, 'L' );
        $this->Cell( 0, 6, $this->order->get_billing_city() . ', ' . $this->order->get_billing_postcode(), 0, 1, 'L' );
        $this->Cell( 0, 6, $this->order->get_billing_country(), 0, 1, 'L' );
        $this->Ln(10);

        // Shipping Address (if applicable)
        if ( $this->order->get_formatted_shipping_address() ) {
            $this->SetFont( 'Arial', 'B', 12 );
            $this->Cell( 0, 7, __( 'Ship To:', 'my-custom-reports' ), 0, 1, 'L' );
            $this->SetFont( 'Arial', '', 12 );
            $this->MultiCell( 0, 6, $this->order->get_formatted_shipping_address(), 0, 'L' );
            $this->Ln(10);
        }

        // Table Header
        $this->SetFont( 'Arial', 'B', 12 );
        $this->SetFillColor( 220, 220, 220 ); // Light grey background
        $this->Cell( 100, 8, __( 'Description', 'my-custom-reports' ), 1, 0, 'C', true );
        $this->Cell( 30, 8, __( 'Quantity', 'my-custom-reports' ), 1, 0, 'C', true );
        $this->Cell( 30, 8, __( 'Price', 'my-custom-reports' ), 1, 0, 'C', true );
        $this->Cell( 30, 8, __( 'Total', 'my-custom-reports' ), 1, 1, 'C', true );
        $this->current_y = $this->GetY(); // Update current Y position after header
    }

    // Page body
    function Body() {
        $this->SetFont( 'Arial', '', 10 );
        $items = $this->order->get_items();

        foreach ( $items as $item_id => $item ) {
            $product_name = $item->get_name();
            $quantity = $item->get_quantity();
            $price = $item->get_total() / $quantity; // Unit price
            $line_total = $item->get_total();

            // Handle product variations if any
            if ( $item->get_variation_id() ) {
                $variation_data = $item->get_variation();
                $variation_string = array();
                foreach ( $variation_data as $key => $value ) {
                    $attribute_name = wc_attribute_label( str_replace( 'attribute_', '', $key ) );
                    $variation_string[] = $attribute_name . ': ' . $value;
                }
                $product_name .= ' (' . implode( ', ', $variation_string ) . ')';
            }

            // Add item row
            $this->SetX( $this->margin_left );
            $this->MultiCell( 100, 6, $product_name, 1, 'L' );
            $this->SetXY( $this->margin_left + 100, $this->GetY() - $this->GetStringHeight( 100, $product_name ) ); // Adjust Y if MultiCell wrapped

            $this->SetX( $this->margin_left + 100 );
            $this->Cell( 30, $this->GetStringHeight( 100, $product_name ), $quantity, 1, 0, 'C' );

            $this->SetX( $this->margin_left + 130 );
            $this->Cell( 30, $this->GetStringHeight( 100, $product_name ), wc_price( $price ), 1, 0, 'R' );

            $this->SetX( $this->margin_left + 160 );
            $this->Cell( 30, $this->GetStringHeight( 100, $product_name ), wc_price( $line_total ), 1, 1, 'R' );
        }
        $this->current_y = $this->GetY(); // Update current Y position
    }

    // Page footer
    function Footer() {
        // Position at 1.5 cm from bottom
        $this->SetY( -15 );

        // Total Section
        $this->SetFont( 'Arial', 'B', 12 );
        $this->SetX( $this->margin_left + 100 );
        $this->Cell( 30, 7, __( 'Subtotal:', 'my-custom-reports' ), 0, 0, 'R' );
        $this->SetFont( 'Arial', '', 12 );
        $this->Cell( 30, 7, wc_price( $this->order->get_subtotal() ), 0, 1, 'R' );

        // Display taxes if applicable
        if ( $this->order->get_total_tax() > 0 ) {
            $this->SetX( $this->margin_left + 100 );
            $this->SetFont( 'Arial', 'B', 12 );
            $this->Cell( 30, 7, __( 'Tax:', 'my-custom-reports' ), 0, 0, 'R' );
            $this->SetFont( 'Arial', '', 12 );
            $this->Cell( 30, 7, wc_price( $this->order->get_total_tax() ), 0, 1, 'R' );
        }

        // Display shipping if applicable
        if ( $this->order->get_shipping_total() > 0 ) {
            $this->SetX( $this->margin_left + 100 );
            $this->SetFont( 'Arial', 'B', 12 );
            $this->Cell( 30, 7, __( 'Shipping:', 'my-custom-reports' ), 0, 0, 'R' );
            $this->SetFont( 'Arial', '', 12 );
            $this->Cell( 30, 7, wc_price( $this->order->get_shipping_total() ), 0, 1, 'R' );
        }

        // Display discounts if applicable
        if ( $this->order->get_discount_total() > 0 ) {
            $this->SetX( $this->margin_left + 100 );
            $this->SetFont( 'Arial', 'B', 12 );
            $this->Cell( 30, 7, __( 'Discount:', 'my-custom-reports' ), 0, 0, 'R' );
            $this->SetFont( 'Arial', '', 12 );
            $this->Cell( 30, 7, '-' . wc_price( $this->order->get_discount_total() ), 0, 1, 'R' );
        }

        // Grand Total
        $this->SetX( $this->margin_left + 100 );
        $this->SetFont( 'Arial', 'B', 14 );
        $this->Cell( 30, 8, __( 'Total:', 'my-custom-reports' ), 1, 0, 'R', true );
        $this->SetFont( 'Arial', 'B', 14 );
        $this->Cell( 30, 8, wc_price( $this->order->get_total() ), 1, 1, 'R', true );

        // Line break before footer text
        $this->Ln(10);

        // Footer Text
        $this->SetFont( 'Arial', 'I', 8 );
        $this->Cell( 0, 10, __( 'Thank you for your business!', 'my-custom-reports' ), 0, 0, 'C' );

        // Page number
        $this->SetY( -15 ); // Re-position for page number
        $this->SetFont( 'Arial', 'I', 8 );
        $this->Cell( 0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'R' );
    }

    /**
     * Helper to get string height for MultiCell
     * @param float $width
     * @param string $text
     * @return float
     */
    private function GetStringHeight( $width, $text ) {
        $lines = $this->getStringSplitLines( $text, $width );
        return count( $lines ) * $this->font_size;
    }

    /**
     * Helper to split string into lines based on width
     * @param string $text
     * @param float $width
     * @return array
     */
    private function getStringSplitLines( $text, $width ) {
        // This is a simplified version. FPDF has internal methods for this,
        // but accessing them directly can be complex. For robust solutions,
        // consider using FPDF's internal _splitlines or similar if accessible.
        // For this example, we'll assume a basic split.
        $lines = array();
        $current_line = '';
        $words = explode( ' ', $text );

        foreach ( $words as $word ) {
            if ( $this->GetStringWidth( $current_line . ' ' . $word ) <= $width ) {
                $current_line .= ' ' . $word;
            } else {
                $lines[] = trim( $current_line );
                $current_line = $word;
            }
        }
        $lines[] = trim( $current_line );
        return $lines;
    }
}
?>

Customizing the PDF Output

The My_Custom_Report_Generator class extends FPDF, allowing us to override its methods like Header(), Footer(), and add new methods for specific content. The example above demonstrates:

  • Including a company logo.
  • Adding company contact information.
  • Displaying invoice and order details.
  • Populating billing and shipping addresses.
  • Creating a table for order items with quantity, unit price, and line total.
  • Calculating and displaying subtotal, tax, shipping, discounts, and the grand total.
  • Adding a footer with a thank you message and page numbers.

Key FPDF Methods Used:

  • AddPage(): Adds a new page to the document.
  • SetFont(family, style, size): Sets the font for subsequent text.
  • Cell(width, height, text, border, ln, align, fill, link): Draws a cell (a rectangular area).
  • MultiCell(width, height, text, border, align, fill): Draws a multi-line cell.
  • Image(file, x, y, w, h, type, link): Embeds an image.
  • Ln(h): Moves the current position down by h units.
  • SetXY(x, y): Sets the current position.
  • GetY(): Gets the current Y position.
  • SetAutoPageBreak(auto, margin): Configures automatic page breaks.
  • SetMargins(left, top, right): Sets page margins.
  • Output(name, dest): Outputs the PDF. 'I' for inline, 'D' for download, 'F' for file.

Handling Order Data and WooCommerce Functions

Within the generator class, we access WooCommerce order data using the WC_Order object methods:

  • $this->order->get_id(): Gets the order ID.
  • $this->order->get_order_number(): Gets the human-readable order number.
  • $this->order->get_date_created(): Gets the order creation date.
  • $this->order->get_billing_...() and $this->order->get_shipping_...(): For address details.
  • $this->order->get_items(): Retrieves all line items in the order.
  • $item->get_name(), $item->get_quantity(), $item->get_total(): For item details.
  • $this->order->get_subtotal(), $this->order->get_total_tax(), $this->order->get_shipping_total(), $this->order->get_discount_total(), $this->order->get_total(): For order totals.

We also leverage WooCommerce's formatting functions like wc_price() for currency display and get_bloginfo('name') for the site title.

Triggering PDF Generation

The example uses a common WordPress pattern: adding an AJAX action to trigger the PDF generation. This is initiated from the WooCommerce order edit screen in the WordPress admin.

Adding a Custom Action Link

The woocommerce_admin_order_actions filter is used to add a custom button to the order actions meta box on the order edit page.

add_filter( 'woocommerce_admin_order_actions', 'add_custom_report_action_link', 10, 2 );

function add_custom_report_action_link( $actions, $order ) {
    $actions['custom_report'] = array(
        'url'    => wp_nonce_url( admin_url( 'admin-ajax.php?action=generate_custom_report&order_id=' . $order->get_id() ), 'generate_custom_report' ),
        'name'   => __( 'Custom Report', 'my-custom-reports' ),
        'icon'   => 'dashicons-download',
    );
    return $actions;
}

AJAX Handler

The wp_ajax_generate_custom_report hook handles the AJAX request. It performs security checks (nonce verification, user capabilities) and then instantiates the report generator.

add_action( 'wp_ajax_generate_custom_report', 'handle_custom_report_generation' );

function handle_custom_report_generation() {
    // ... (security checks and order retrieval) ...

    $report_generator = new My_Custom_Report_Generator( $order );
    $report_generator->generate_invoice_pdf();
}

Advanced Customizations and Considerations

Internationalization (i18n)

For multi-language sites, ensure all translatable strings are wrapped in WordPress translation functions like __() and _e(). Load your plugin's text domain correctly.

Error Handling and Logging

Implement robust error handling. Log errors to a file or use WordPress's error logging mechanisms for debugging production issues. This is crucial for identifying problems with FPDF rendering, data retrieval, or file permissions.

Custom Report Types

This framework can be extended to generate other types of reports, such as:

  • Sales Summaries: Aggregate sales data over a period (daily, weekly, monthly).
  • Product Performance Reports: Detail sales volume and revenue per product.
  • Customer Reports: Summarize customer purchase history.

For these, you would create new methods within the generator class or separate classes, querying the WordPress database directly (using $wpdb) or leveraging WooCommerce's reporting APIs.

PDF Output Destinations

The Output() method in FPDF offers flexibility:

  • 'I': Send the file inline to the browser (default in example).
  • 'D': Send to the browser and force a download.
  • 'F': Save the file on the server.
  • 'S': Return the document as a string.

For saving to the server ('F'), ensure your plugin has write permissions to the target directory (e.g., wp-content/uploads/custom-reports/). You'll need to create this directory and manage file cleanup.

Styling and Branding

Beyond basic text and logos, FPDF allows for advanced styling:

  • Fonts: Embed custom TrueType fonts for specific branding.
  • Colors: Use SetDrawColor(), SetFillColor(), SetTextColor().
  • Shapes: Draw lines, rectangles, and polygons.
  • Page Layout: Control orientation (portrait/landscape) and page size.

Conclusion

By integrating FPDF into a custom WordPress plugin, you gain granular control over the generation of financial reports and invoices for WooCommerce. This approach provides a scalable and maintainable solution for businesses requiring tailored PDF outputs, moving beyond the limitations of default WooCommerce features. Remember to prioritize security, error handling, and code organization for production-ready implementations.

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

  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Google Analytics v4 REST handlers
  • Troubleshooting REST API CORS authorization failures in production when using modern Understrap styling structures wrappers
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Twilio SMS Gateway connectors
  • Optimizing WooCommerce cart response times by lazy loading custom real estate agent listings assets
  • Debugging Guide: Diagnosing SQL query deadlocks in multi-site network environments with modern tools

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 (168)
  • WordPress Plugin Development (180)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Google Analytics v4 REST handlers
  • Troubleshooting REST API CORS authorization failures in production when using modern Understrap styling structures wrappers
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Twilio SMS Gateway connectors

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