# easy-invoice/2.4.3/includes/Import/CsvImporter.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.3. 323 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.3/code/includes/Import/CsvImporter.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.3/raw/includes/Import/CsvImporter.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.3/code/includes/Import/CsvImporter.php#L10-L20`.

```php
<?php
/**
 * Import clients and invoices from CSV files.
 *
 * @package Easy_Invoice
 * @subpackage Import
 */

namespace EasyInvoice\Import;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * CSV is the lowest common denominator: FreshBooks, Wave, Zoho, Invoice
 * Ninja and a spreadsheet all produce one. Columns are matched by header
 * name, loosely -- "Client Email", "customer_email" and "Email" all land in
 * the same place -- so most exports work without editing the file.
 *
 * Two files:
 *
 *   clients.csv   one row per client: name, email, company, address, phone,
 *                 website, notes
 *   invoices.csv  one row per line item, the invoice number repeated on each
 *                 (the way FreshBooks and Zoho export): number, date,
 *                 due_date, status, client_email, client_name, currency,
 *                 tax_rate, discount, item, description, quantity, price,
 *                 amount_paid, paid_date, notes, terms
 *
 * A file with only the header-level columns and no item columns imports each
 * row as one invoice with a single line for the total.
 */
class CsvImporter extends Importer {

    /** @var string */
    private $clients_csv = '';

    /** @var string */
    private $invoices_csv = '';

    /** Header aliases, lowercase, punctuation removed. */
    const ALIASES = [
        'number'       => [ 'number', 'invoicenumber', 'invoiceid', 'invoice', 'id', 'no', 'ref', 'reference' ],
        'date'         => [ 'date', 'invoicedate', 'issuedate', 'issued', 'dateissued', 'created' ],
        'due_date'     => [ 'duedate', 'due', 'datedue', 'paymentdue' ],
        'status'       => [ 'status', 'invoicestatus', 'state' ],
        'client_email' => [ 'clientemail', 'customeremail', 'email', 'emailaddress', 'billtoemail' ],
        'client_name'  => [ 'clientname', 'customername', 'client', 'customer', 'name', 'billto', 'organization', 'company' ],
        'currency'     => [ 'currency', 'currencycode' ],
        'tax_rate'     => [ 'taxrate', 'tax', 'taxpercent', 'vatrate', 'vat' ],
        'discount'     => [ 'discount', 'discountpercent', 'discountamount' ],
        'item'         => [ 'item', 'itemname', 'product', 'service', 'lineitem', 'title' ],
        'description'  => [ 'description', 'itemdescription', 'details', 'desc' ],
        'quantity'     => [ 'quantity', 'qty', 'hours', 'units' ],
        'price'        => [ 'price', 'rate', 'unitprice', 'unitcost', 'amount' ],
        'total'        => [ 'total', 'invoicetotal', 'grandtotal', 'amountdue' ],
        'amount_paid'  => [ 'amountpaid', 'paid', 'paidamount', 'payments' ],
        'paid_date'    => [ 'paiddate', 'datepaid', 'paymentdate' ],
        'notes'        => [ 'notes', 'note', 'memo', 'message' ],
        'terms'        => [ 'terms', 'termsandconditions', 'paymentterms' ],
        // clients
        'first_name'   => [ 'firstname', 'first' ],
        'last_name'    => [ 'lastname', 'last', 'surname' ],
        'address'      => [ 'address', 'billingaddress', 'street', 'fulladdress' ],
        'phone'        => [ 'phone', 'telephone', 'mobile', 'phonenumber' ],
        'website'      => [ 'website', 'url', 'web' ],
    ];

    /**
     * @param string $clients_csv  Path to clients file or ''.
     * @param string $invoices_csv Path to invoices file or ''.
     */
    public function __construct( string $clients_csv = '', string $invoices_csv = '' ) {
        $this->clients_csv  = $clients_csv;
        $this->invoices_csv = $invoices_csv;
    }

    public function source(): string {
        return 'csv';
    }

    public function label(): string {
        return __( 'CSV file', 'easy-invoice' );
    }

    public function available(): bool {
        return true;
    }

    public function preview(): array {
        $clients  = $this->clients_csv ? count( $this->rows( $this->clients_csv ) ) : 0;
        $invoices = $this->invoices_csv ? count( $this->groupInvoices( $this->rows( $this->invoices_csv ) ) ) : 0;
        return [ 'clients' => $clients, 'invoices' => $invoices, 'quotes' => 0, 'payments' => 0, 'already' => 0 ];
    }

    public function run(): array {
        if ( $this->clients_csv ) {
            foreach ( $this->rows( $this->clients_csv ) as $i => $r ) {
                $name = (string) ( $r['client_name'] ?? '' );
                list( $first, $last ) = $this->splitName( $name );
                $this->findOrCreateClient( [
                    'source_id'  => 'c:' . md5( strtolower( (string) ( $r['client_email'] ?? $name ?: $i ) ) ),
                    'email'      => (string) ( $r['client_email'] ?? '' ),
                    'first_name' => (string) ( $r['first_name'] ?? $first ),
                    'last_name'  => (string) ( $r['last_name'] ?? $last ),
                    'business'   => (string) ( $r['company'] ?? ( isset( $r['first_name'] ) ? $name : '' ) ),
                    'address'    => (string) ( $r['address'] ?? '' ),
                    'phone'      => (string) ( $r['phone'] ?? '' ),
                    'website'    => (string) ( $r['website'] ?? '' ),
                    'extra_info' => (string) ( $r['notes'] ?? '' ),
                ] );
            }
        }

        if ( $this->invoices_csv ) {
            foreach ( $this->groupInvoices( $this->rows( $this->invoices_csv ) ) as $number => $rows ) {
                $this->importInvoice( (string) $number, $rows );
            }
        }

        return $this->report();
    }

    /**
     * One invoice from its rows (one per line item).
     *
     * @param string $number Invoice number.
     * @param array  $rows   Mapped rows.
     * @return void
     */
    private function importInvoice( string $number, array $rows ): void {
        $h     = $rows[0];
        $email = sanitize_email( (string) ( $h['client_email'] ?? '' ) );
        $name  = (string) ( $h['client_name'] ?? '' );
        $client_id = 0;
        if ( '' !== $email || '' !== $name ) {
            list( $first, $last ) = $this->splitName( $name );
            $client_id = $this->findOrCreateClient( [
                'source_id'  => 'c:' . md5( strtolower( $email ?: $name ) ),
                'email'      => $email,
                'first_name' => $first,
                'last_name'  => $last,
                'business'   => $name,
            ] );
        }

        $items = [];
        foreach ( $rows as $r ) {
            $item_name = trim( (string) ( $r['item'] ?? '' ) );
            $desc      = trim( (string) ( $r['description'] ?? '' ) );
            if ( '' === $item_name && '' === $desc ) {
                continue;
            }
            $items[] = [
                'name'        => '' !== $item_name ? $item_name : strtok( $desc, "\n" ),
                'description' => '' !== $item_name ? $desc : '',
                'quantity'    => isset( $r['quantity'] ) && '' !== $r['quantity'] ? $this->toNumber( $r['quantity'] ) : 1,
                'price'       => $this->toNumber( $r['price'] ?? 0 ),
                'taxable'     => true,
            ];
        }
        if ( empty( $items ) ) {
            $items[] = [
                'name'     => (string) ( $h['description'] ?? $h['notes'] ?? '' ) ?: sprintf( /* translators: %s: invoice number. */ __( 'Invoice %s', 'easy-invoice' ), $number ),
                'quantity' => 1,
                'price'    => $this->toNumber( $h['total'] ?? $h['price'] ?? 0 ),
                'taxable'  => false,
            ];
        }

        $status_raw = strtolower( trim( (string) ( $h['status'] ?? '' ) ) );
        $status_map = [ 'paid' => 'paid', 'complete' => 'paid', 'completed' => 'paid', 'draft' => 'draft', 'sent' => 'available', 'unpaid' => 'available', 'open' => 'available', 'outstanding' => 'available', 'overdue' => 'available', 'partial' => 'available', 'void' => 'cancelled', 'cancelled' => 'cancelled', 'canceled' => 'cancelled' ];
        $status     = $status_map[ $status_raw ] ?? 'available';

        $invoice_id = $this->createInvoice( [
            'source_id'      => 'i:' . $number,
            'title'          => (string) ( $h['description'] ?? '' ) ?: sprintf( /* translators: %s: invoice number. */ __( 'Invoice %s', 'easy-invoice' ), $number ),
            'number'         => $number,
            'status'         => $status,
            'issue_date'     => $this->toDate( $h['date'] ?? '' ) ?: current_time('Y-m-d'),
            'due_date'       => $this->toDate( $h['due_date'] ?? '' ),
            'created'        => $this->toDateTime( $h['date'] ?? '' ),
            'client_id'      => $client_id,
            'customer_name'  => $name,
            'customer_email' => $email,
            'items'          => $items,
            'tax_rate'       => $this->toNumber( $h['tax_rate'] ?? 0 ),
            'discount_type'  => false !== strpos( (string) ( $h['discount'] ?? '' ), '%' ) ? 'percentage' : 'fixed',
            'discount_value' => $this->toNumber( $h['discount'] ?? 0 ),
            'currency_code'  => strtoupper( trim( (string) ( $h['currency'] ?? '' ) ) ),
            'notes'          => (string) ( $h['notes'] ?? '' ),
            'terms'          => (string) ( $h['terms'] ?? '' ),
        ] );

        if ( $invoice_id <= 0 ) {
            return;
        }
        $paid = $this->toNumber( $h['amount_paid'] ?? 0 );
        if ( $paid <= 0 && 'paid' === $status ) {
            $inv  = new \EasyInvoice\Models\Invoice( get_post( $invoice_id ) );
            $paid = (float) $inv->getTotal();
        }
        if ( $paid > 0 ) {
            $this->createPayment( $invoice_id, [
                'source_id' => 'p:' . $number,
                'amount'    => $paid,
                'date'      => $this->toDateTime( $h['paid_date'] ?? '' ) ?: ( $this->toDateTime( $h['date'] ?? '' ) ?: current_time( 'mysql' ) ),
                'method'    => 'imported',
                'status'    => 'completed',
                'notes'     => __( 'Imported from CSV', 'easy-invoice' ),
            ] );
        }
    }

    /**
     * Rows keyed by canonical column names.
     *
     * @param string $path File.
     * @return array<int,array<string,string>>
     */
    private function rows( string $path ): array {
        if ( ! is_readable( $path ) ) {
            $this->notes[] = sprintf( 'Cannot read %s.', basename( $path ) );
            return [];
        }
        $fh = fopen( $path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- streaming a large upload row by row.
        if ( ! $fh ) {
            return [];
        }
        $first = fgets( $fh );
        rewind( $fh );
        $delimiter = ( substr_count( (string) $first, ';' ) > substr_count( (string) $first, ',' ) ) ? ';' : ( substr_count( (string) $first, "\t" ) > substr_count( (string) $first, ',' ) ? "\t" : ',' );

        $header = fgetcsv( $fh, 0, $delimiter, '"', '\\' );
        if ( ! is_array( $header ) ) {
            fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions
            return [];
        }
        // Strip a BOM from the first header cell.
        $header[0] = preg_replace( '/^\xEF\xBB\xBF/', '', (string) $header[0] );
        $map = [];
        foreach ( $header as $i => $col ) {
            $key = $this->canonical( (string) $col );
            if ( '' !== $key && ! isset( $map[ $key ] ) ) {
                $map[ $key ] = $i;
            }
        }
        $rows = [];
        while ( ( $line = fgetcsv( $fh, 0, $delimiter, '"', '\\' ) ) !== false ) {
            if ( 1 === count( $line ) && null === $line[0] ) {
                continue;
            }
            $row = [];
            foreach ( $map as $key => $i ) {
                $row[ $key ] = isset( $line[ $i ] ) ? trim( (string) $line[ $i ] ) : '';
            }
            if ( '' === implode( '', $row ) ) {
                continue;
            }
            $rows[] = $row;
        }
        fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions
        return $rows;
    }

    /**
     * Group line-item rows by invoice number.
     *
     * @param array $rows Mapped rows.
     * @return array<string,array>
     */
    private function groupInvoices( array $rows ): array {
        $groups = [];
        $auto   = 0;
        foreach ( $rows as $r ) {
            $n = trim( (string) ( $r['number'] ?? '' ) );
            if ( '' === $n ) {
                $n = 'CSV-' . ( ++$auto );
            }
            $groups[ $n ][] = $r;
        }
        return $groups;
    }

    /**
     * Header text → canonical key, '' when unknown.
     *
     * @param string $col Header cell.
     * @return string
     */
    private function canonical( string $col ): string {
        $norm = preg_replace( '/[^a-z0-9]/', '', strtolower( $col ) );
        if ( '' === $norm ) {
            return '';
        }
        if ( 'company' === $norm || 'business' === $norm || 'businessname' === $norm || 'companyname' === $norm ) {
            return 'company';
        }
        foreach ( self::ALIASES as $key => $aliases ) {
            if ( in_array( $norm, $aliases, true ) ) {
                return $key;
            }
        }
        return '';
    }

    /**
     * Header rows for the template files.
     *
     * @param string $which 'clients' or 'invoices'.
     * @return string CSV text.
     */
    public static function template( string $which ): string {
        if ( 'clients' === $which ) {
            return "name,email,company,address,phone,website,notes\n\"Jane Doe\",jane@example.com,\"Doe Studio\",\"12 Bridge Street, Berlin\",+49 30 1234567,https://doe.example,\"Prefers invoices on the 1st\"\n";
        }
        return "number,date,due_date,status,client_email,client_name,currency,tax_rate,discount,item,description,quantity,price,amount_paid,paid_date,notes,terms\n"
            . "INV-1001,2026-03-01,2026-03-31,paid,jane@example.com,\"Doe Studio\",EUR,19,0,\"Design retainer\",\"March\",1,1200,1428,2026-03-20,\"Thanks!\",\"Net 30\"\n"
            . "INV-1001,2026-03-01,2026-03-31,paid,jane@example.com,\"Doe Studio\",EUR,19,0,\"Hosting\",\"March\",1,50,,,,\n";
    }
}

```
