Search code examples
phpxmlquickbooksqbxml

Consolibyte QB for Windows API error trying to create an estimate... why?


XML that gets sent to quickbooks via Add Estimate request found within the quickbooks_log table is as follows:

<?xml version="1.0" encoding="utf-8"?>
        <?qbxml version="13.0"?>
        <QBXML>
            <QBXMLMsgsRq onError="stopOnError">
                <EstimateAddRq requestID="212">
    <EstimateAdd>
        <CustomerRef>
            <ListID>800007A5-1480913677</ListID>
        </CustomerRef>
        <TxnDate>2016-12-04</TxnDate>
        <BillAddress>
            <Addr1>2532 S. Franklin Street</Addr1>
            <City>Philadelphia</City>
            <Province>PA</Province>
            <PostalCode>19148</PostalCode>
            <Country>United States</Country>
        </BillAddress>
        <ShipAddress>
            <Addr1>2406 E. York Street</Addr1>
            <Addr2>Apartment #2B</Addr2>
            <City>Philadelphia</City>
            <Province>PA</Province>
            <PostalCode>19125</PostalCode>
            <Country>United States</Country>
        </ShipAddress>
        <IsToBeEmailed>true</IsToBeEmailed>
        <EstimateLineAdd>
            <ItemRef>
                <ListID>800000C1-1480913684</ListID>
            </ItemRef>
            <Quantity>45</Quantity>
        </EstimateLineAdd>
        <EstimateLineAdd>
            <ItemRef>
                <ListID>800000BE-1480913680</ListID>
            </ItemRef>
            <Quantity>10</Quantity>
        </EstimateLineAdd>
        <EstimateLineAdd>
            <ItemRef>
                <ListID>800000C0-1480913683</ListID>
            </ItemRef>
            <Quantity>500</Quantity>
        </EstimateLineAdd>
        <EstimateLineAdd>
            <ItemRef>
                <ListID>800000BD-1480913679</ListID>
            </ItemRef>
            <Quantity>5</Quantity>
            <Amount>0.00</Amount>
            <Other1>NO BID</Other1>
        </EstimateLineAdd>
        <EstimateLineAdd>
            <ItemRef>
                <ListID>800000BF-1480913681</ListID>
            </ItemRef>
            <Quantity>10</Quantity>
        </EstimateLineAdd>
    </EstimateAdd>
</EstimateAddRq>

            </QBXMLMsgsRq>
        </QBXML>

Creating of the Estimate function is as follows:

function hunter_create_estimate()
{
    global $wpdb;

    $submission_id = !empty($_POST['submission_id']) ? $_POST['submission_id'] : 0;
    $submit_time = !empty($_POST['submit_time']) ? $_POST['submit_time'] : '';

    $results = array(
        'type' => 'error',
        'message' => 'An Error Occurred when trying to create an Estimate for this Quote Request. Please try again.'
    );

    if (empty($submission_id) || empty($submit_time) || !isset($_POST['instance']))
    {
        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }

    if (!isset($_POST['_wpnonce_create_estimate_from_' . $submission_id]))
    {
        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }

    $nonce = $_POST['_wpnonce_create_estimate_from_' . $submission_id];

    if (!wp_verify_nonce($nonce, 'hunter-create-estimate'))
    {
        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }

    if (!current_user_can('manage_options'))
    {
        $results['message'] = 'Sorry, but you do not have permission to create estimates.';
        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }
    $instance = !empty($_POST['instance']) ? (int) $_POST['instance'] : 0;
    $instance = empty($instance) ? 0 : $instance;

    $form_name = get_option('quickbooks_cf7_form', 'Personal Info');
    $form_status = $wpdb->get_var('
        SELECT MAX(IF(field_name = "status", field_value, NULL)) AS status
        FROM ' . $wpdb->prefix . 'cf7dbplugin_submits
        WHERE form_name = "' . $form_name . '" AND submit_time = "' . $submit_time . '" AND instance = ' . $instance);

    if ($form_status != 'quote_sent')
    {
        $results['message'] = 'You have to send the quote to the Customer before you will be able to create an Estimate from it in Quickbooks.';
        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }

    $quote_data = get_transient('submission-quote-sent_' . $submission_id);

    if (!empty($quote_data) && !empty($quote_data[$instance]) && !empty($quote_data[$instance][0]))
    {
        ini_set('memory_limit', '-1');
        ini_set('max_input_time', '-1');
        ini_set('max_execution_time', '-1');
        set_time_limit(0);

        $data = $quote_data[$instance][0];
        $csv_lines = explode("\n", $data['csv_items_file']);
        $csv_items = processItemsInCSV($csv_lines);

        if (!empty($csv_items))
            $data = array_merge($data, $csv_items);

        if (function_exists('date_default_timezone_set'))
            date_default_timezone_set('America/New_York');

        $output = $qb_ajax_submissions = $estimate_data = array();
        $noninventory_items = $estimate_lineitems = array();

        foreach($data['parts'] as $index => $part)
        {
            $noninventory_items[$index] = array(
                'noBid' => $part['noBid'],
                'name' => htmlspecialchars(stripslashes($part['part']), ENT_NOQUOTES),
                'quantity' => stripdoublequotes($part['quantity']),
                'vendor' => !empty($part['vendor']) ? htmlspecialchars(stripslashes($part['vendor']), ENT_NOQUOTES) : '',
                'source' => htmlspecialchars(stripdoublequotes($part['source']), ENT_NOQUOTES),
                'type' => htmlspecialchars(stripdoublequotes($part['type']), ENT_NOQUOTES),
                'cost' => !empty($part['cost']) ? stripdoublequotes($part['cost']) : 0,
                'price' => !empty($part['price']) ? stripdoublequotes($part['price']) : 0,
                'effectDate' => !empty($part['effectDate']) ? $part['effectDate'] : '',
                'purchase_desc' => '', // Default
                'sales_desc' => '' // Default
            );

            $itemDescriptions = $wpdb->get_row("
                SELECT IF(qi.PurchaseDesc IS NULL OR qi.PurchaseDesc = '', qvi.ItemDescription, qi.PurchaseDesc) AS purchase_desc, IF(qi.SalesDesc IS NULL OR qi.SalesDesc = '', qvi.ItemDescription, qi.SalesDesc) AS sales_desc
                FROM " . $wpdb->prefix . "quickbook_vendor_items AS qvi, " . $wpdb->prefix . "quickbook_items AS qi
                WHERE (qvi.EstimateID = 0 AND qvi.ItemName = '" . addcslashes($part['part'], "'") . "' AND qvi.IsActive = 1) OR qi.Name = '" . addcslashes($part['part'], "'") . "'", ARRAY_A);

            if (!empty($itemDescriptions))
            {
                $noninventory_items[$index]['purchase_desc'] = !empty($itemDescriptions['purchase_desc']) ? htmlspecialchars($itemDescriptions['purchase_desc'], ENT_NOQUOTES) : '';
                $noninventory_items[$index]['sales_desc'] = !empty($itemDescriptions['sales_desc']) ? htmlspecialchars($itemDescriptions['sales_desc'], ENT_NOQUOTES) : '';
            }
        }

        // Adding the Class file.
        require_once(get_stylesheet_directory() . '/QuickBooks.php');

        $fsubmit_time = str_replace('.', '_', $submit_time);
        delete_transient('vQBIDs_' . $submission_id . '_' . $fsubmit_time);

        $estimate_data = array(
            'action' => 'create_estimate',
            'submission_id' => $submission_id,
            'submit_time' => $submit_time,
            'instance' => $instance,
            'additional_columns' => array(
                'PaymentTerm' => array(
                    'value' => !empty($data['payment_term']) ? htmlspecialchars($data['payment_term'], ENT_NOQUOTES) : '',
                    'format' => '%s'
                ),
                'OrderNotes' => array(
                    'value' => !empty($data['order_notes']) ? htmlspecialchars(stripslashes($data['order_notes']), ENT_NOQUOTES) : '',
                    'format' => '%s'
                ),
                'PreparedBy' => array(
                    'value' => !empty($data['prepared_by']) ? htmlspecialchars($data['prepared_by'], ENT_NOQUOTES) : '',
                    'format' => '%s'
                ),
                'QuoteSent' => array(
                    'value' => !empty($data['quote_sent_timestamp']) ? date('Y-m-d H:i:s', (int) $data['quote_sent_timestamp']) : date('Y-m-d H:i:s'),
                    'format' => '%s'
                )
            ),
            'additional_secondary_columns' => array(
                'items' => array_column($data['parts'], 'part'),
                'columns' => array(
                    'ConditionCode' => array(
                        'values' => array_column($data['parts'], 'conditionCode'),
                        'format' => '%s'
                    ),
                    'DeliveryTerm' => array(
                        'values' => array_column($data['parts'], 'deliveryTerm'),
                        'format' => '%s'
                    )
                )
            )
        );

        $item_data = array(
            'submission_id' => $submission_id,
            'submit_time' => $submit_time
        );

        $customer_listid = $wpdb->get_var("SELECT ListID
            FROM " . $wpdb->prefix . "quickbook_customers
            WHERE Email = '" . $data['email'] . "'");

        $addresses = array(
            'BillAddress' => array(
                'billing_addr1' => 'Addr1',
                'billing_addr2' => 'Addr2',
                'billing_city' => 'City',
                'billing_state' => 'State',
                'billing_zip' => 'PostalCode',
                'billing_country' => 'Country'
            ),
            'ShipAddress' => array(
                'shipping_addr1' => 'Addr1',
                'shipping_addr2' => 'Addr2',
                'shipping_city' => 'City',
                'shipping_state' => 'State',
                'shipping_zip' => 'PostalCode',
                'shipping_country' => 'Country'
            )
        );

        if (empty($customer_listid))
        {
            // NEW CUSTOMER
            $data['has_email'] = false;

            $customer_data = array(
                'request' => array(
                    'Name' => $data['fName'] . ' ' . $data['lName'],
                    'CompanyName' => htmlspecialchars(html_entity_decode($data['company']), ENT_NOQUOTES),
                    'FirstName' => $data['fName'],
                    'LastName' => $data['lName'],
                    'BillAddress' => array(),
                    'ShipAddress' => array(),
                    'Phone' => !empty($data['tel']) ? $data['tel'] : '',
                    'Email' => $data['email']
                ),
                'submit_time' => $submit_time,
                'instance' => $instance
            );

            foreach($addresses as $address_type => $address)
            {
                if ($address_type == 'ShipAddress'  && empty($data['has_shipping']))
                    continue;

                foreach($address as $type => $key)
                    if (!empty($data[$type]))
                        $customer_data['request'][$address_type][$key] = htmlspecialchars($data[$type], ENT_NOQUOTES);
            }

            if (!isset($Queue))
            {
                $dsn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
                $Queue = new QuickBooks_WebConnector_Queue($dsn);
            }

            // High Priority
            $Queue->enqueue(QUICKBOOKS_ADD_CUSTOMER, null, 30, $customer_data);
        }
        else
        {
            $data['has_email'] = true;
            $estimate_data['ListID'] = $customer_listid;
        }

        $vendors = array_column($data['parts'], 'vendor');
        $vendors = !empty($vendors) ? array_filter($vendors) : array();
        if (!empty($vendors))
        {
            $qbdb_vendors = $wpdb->get_results("
                SELECT ListID, Name
                FROM " . $wpdb->prefix . "quickbook_vendors
                WHERE Name IN ('" . implode('\',\'', array_map('addslashes', $vendors)) . "')
            ", ARRAY_A);
        }

        $vendor_db = array(
            'Names' => array(),
            'ListIDs' => array()
        );
        $vendors_to_create = $items_to_create = array();
        if (!empty($qbdb_vendors))
        {
            foreach($qbdb_vendors as $qvendors)
            {
                $vendor_db['Names'][] = $qvendors['Name'];
                foreach($noninventory_items as $in => $ni)
                {
                    if ($ni['vendor'] == $qvendors['Name'])
                        $noninventory_items[$in]['VendorListID'] = $qvendors['ListID'];
                }
                $vendor_db['ListIDs'][] = $qvendors['ListID'];
            }
            $vendors_to_create = array_unique(array_diff($vendors, $vendor_db['Names']));
        }
        else
        {
            if (!empty($vendors))
                $vendors_to_create = array_unique($vendors);
        }
        if (!empty($vendors_to_create))
        {
            if (!isset($Queue))
            {
                $dsn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
                $Queue = new QuickBooks_WebConnector_Queue($dsn);
            }

            foreach($vendors_to_create as $vendor_name)
                $Queue->enqueue(QUICKBOOKS_ADD_VENDOR, null, 30, htmlspecialchars($vendor_name, ENT_NOQUOTES));
        }

        $items = array_column($data['parts'], 'part');
        $qbdb_items = $wpdb->get_results("
            SELECT ListID, Name, EditSequence
            FROM " . $wpdb->prefix . "quickbook_items
            WHERE Name IN ('" . implode('\',\'', array_map('addslashes', $items)) . "')
        ", ARRAY_A);

        $item_db = array(
            'Names' => array(),
            'ListIDs' => array(),
            'EditSequences' => array()
        );

        if (!empty($qbdb_items))
        {
            foreach($qbdb_items as $qitems)
            {
                $item_db['Names'][] = $qitems['Name'];

                foreach($noninventory_items as $in => $ni)
                {
                    if ($ni['name'] == $qitems['Name'])
                        $noninventory_items[$in]['ItemListID'] = $qitems['ListID'];
                }

                $item_db['ListIDs'][] = $qitems['ListID'];
                $item_db['EditSequences'][] = $qitems['EditSequence'];
            }
            $items_to_create = array_unique(array_diff($items, $item_db['Names']));
        }
        else
        {
            // New Items
            $items_to_create = array_unique($items);
        }

        if (!empty($items_to_create))
        {
            if (!isset($Queue))
            {
                $dsn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
                $Queue = new QuickBooks_WebConnector_Queue($dsn);
            }

            // Adding items.
            foreach($items_to_create as $item_name)
                $Queue->enqueue(QUICKBOOKS_ADD_NONINVENTORYITEM, null, 20, array_merge($item_data, array('name' => htmlspecialchars($item_name, ENT_NOQUOTES), 'all_items' => $noninventory_items)));
        }

        if (!empty($item_db['Names']))
        {
            if (!isset($Queue))
            {
                $dsn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
                $Queue = new QuickBooks_WebConnector_Queue($dsn);
            }

            foreach($item_db['Names'] as $iK => $iKName)
                $Queue->enqueue(QUICKBOOKS_MOD_NONINVENTORYITEM, null, 20, array_merge($item_data, array('item_ListID' => $item_db['ListIDs'][$iK], 'item_EditSequence' => $item_db['EditSequences'][$iK], 'item_Name' => htmlspecialchars($iKName, ENT_NOQUOTES), 'all_items' => $noninventory_items)));
        }
        foreach($addresses as $address_type => $address)
        {
            // Is shipping?
            if ($address_type == 'ShipAddress'  && empty($data['has_shipping']))
                continue;

            foreach($address as $type => $key)
                $estimate_data[$address_type][$key] = !empty($data[$type]) ? htmlspecialchars($data[$type], ENT_NOQUOTES) : '';
        }
        // Submit the Estimate Now...
        if (!isset($Queue))
        {
            $dsn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
            $Queue = new QuickBooks_WebConnector_Queue($dsn);
        }

        $estimate_data = array_merge($estimate_data, array('all_items' => $noninventory_items));

        $Queue->enqueue(QUICKBOOKS_ADD_ESTIMATE, null, 10, $estimate_data);

        $wpdb->update(
            $wpdb->prefix . 'cf7dbplugin_submits',
            array(
                'field_value' => 'quote_approved'
            ),
            array(
                'form_name' => get_option('quickbooks_cf7_form', 'Personal Info'),
                'submit_time' => $submit_time,
                'instance' => $instance,
                'field_name' => 'status'
            ),
            array('%s'),
            array('%s', '%s', '%d', '%s')
        );

        // success...
        $results = array('type' => 'success', 'message' => sprintf('An Estimate for Quote #%1$s-%2$d has been successfully queued in Quickbooks.  Once it has been created in Quickbooks, it will show up in the Quotes Tab.  If an error occurs while trying to create the estimate, the quote will remain available from within this section to resubmit again.', $submission_id, $instance));

        set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
        wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
        exit(0);
    }

    // If error occurred, this gets sent to the transient...
    set_transient('create-estimate-results', $results, HOUR_IN_SECONDS);
    wp_redirect(admin_url('admin.php?page=quickbook-rfqs'));
    exit(0);
}

Now here's the functions for handling the request and response for QUICKBOOKS_ADD_ESTIMATE

function _quickbooks_estimate_add_request($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $version, $locale)
{
    global $wpdb;

    $Estimate = new QuickBooks_QBXML_Object_Estimate();

    $form_instance = !empty($extra['instance']) ? (int) $extra['instance'] : 0;
    $form_instance = empty($form_instance) ? 0 : $form_instance;

    if (!empty($extra['ListID']))
        $Estimate->setCustomerListID($extra['ListID']);
    else
    {
        // Get the customer list id
        $customers_id = $wpdb->get_var("SELECT field_value FROM " . $wpdb->prefix . "cf7dbplugin_submits WHERE submit_time = " . $extra['submit_time'] . " AND instance = " . $form_instance . " AND field_name = 'customer_id' AND form_name = '" . get_option('quickbooks_cf7_form', 'Personal Info') . "'");

        $customers_listid = $wpdb->get_var("SELECT ListID FROM " . $wpdb->prefix . "quickbook_customers WHERE id = " . intval($customers_id));

        $Estimate->setCustomerListID($customers_listid);
    }

    $Estimate->setTxnDate(date('Y-m-d', time()));

    $province = !empty($extra['BillAddress']['Country']) ? htmlspecialchars_decode($extra['BillAddress']['State'], ENT_NOQUOTES) : '';
    $state = empty($extra['BillAddress']['Country']) ? htmlspecialchars_decode($extra['BillAddress']['State'], ENT_NOQUOTES) : '';
    $Estimate->setBillAddress(htmlspecialchars_decode($extra['BillAddress']['Addr1'], ENT_NOQUOTES), htmlspecialchars_decode($extra['BillAddress']['Addr2'], ENT_NOQUOTES), '', '', '', htmlspecialchars_decode($extra['BillAddress']['City'], ENT_NOQUOTES), $state, $province, htmlspecialchars_decode($extra['BillAddress']['PostalCode'], ENT_NOQUOTES), htmlspecialchars_decode($extra['BillAddress']['Country'], ENT_NOQUOTES), '');

    if (!empty($extra['ShipAddress']))
    {
        $ship_province = !empty($extra['ShipAddress']['Country']) ? htmlspecialchars_decode($extra['ShipAddress']['State'], ENT_NOQUOTES) : '';
        $ship_state = empty($extra['ShipAddress']['Country']) ? htmlspecialchars_decode($extra['ShipAddress']['State'], ENT_NOQUOTES) : '';

        $Estimate->setShipAddress(htmlspecialchars_decode($extra['ShipAddress']['Addr1'], ENT_NOQUOTES), htmlspecialchars_decode($extra['ShipAddress']['Addr2'], ENT_NOQUOTES), '', '', '', htmlspecialchars_decode($extra['ShipAddress']['City'], ENT_NOQUOTES), $ship_state, $ship_province, htmlspecialchars_decode($extra['ShipAddress']['PostalCode'], ENT_NOQUOTES), htmlspecialchars_decode($extra['ShipAddress']['Country'], ENT_NOQUOTES), '');
    }

    $Estimate->setIsToBeEmailed('true');

    if (!empty($extra['all_items']))
    {
        foreach($extra['all_items'] as $item_data)
        {
            $EstimateLineItem = new QuickBooks_QBXML_Object_Estimate_EstimateLine();
            $description = array();

            if (!isset($item_data['ItemListID']))
            {
                $itemListID = $wpdb->get_var("SELECT ListID FROM " . $wpdb->prefix . "quickbook_items WHERE Name = '" . htmlspecialchars_decode(addcslashes($item_data['name'], "'"), ENT_NOQUOTES) . "' ORDER BY NULL LIMIT 1");
                $item_data['ItemListID'] = $itemListID;
            }

            $EstimateLineItem->setItemListID($item_data['ItemListID']);

            $description = array();

            if (!empty($item_data['sales_desc']))
                $description[] = htmlspecialchars_decode($item_data['sales_desc'], ENT_NOQUOTES);

            if (!empty($item_data['purchase_desc']))
                $description[] = htmlspecialchars_decode($item_data['purchase_desc'], ENT_NOQUOTES);

            if (!empty($description))
                $EstimateLineItem->setDescription(implode(' ', $description));

            $EstimateLineItem->setQuantity($item_data['quantity']);

            if (!empty($item_data['noBid']))
            {
                $EstimateLineItem->setAmount(0);
                $EstimateLineItem->setOther1('NO BID');
            }
            $Estimate->addEstimateLine($EstimateLineItem);
        }
    }

    $qbxml = $Estimate->asQBXML(QUICKBOOKS_ADD_ESTIMATE);

    $xml = '<?xml version="1.0" encoding="utf-8"?>
        <?qbxml version="13.0"?>
        <QBXML>
            <QBXMLMsgsRq onError="stopOnError">
                ' . $qbxml . '
            </QBXMLMsgsRq>
        </QBXML>';

    file_put_contents(dirname(__FILE__) . '/xml.log', $xml . PHP_EOL . var_export($xml, true) . PHP_EOL . PHP_EOL , FILE_APPEND | LOCK_EX);

    return $xml;
}

function _quickbooks_estimate_add_response($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents)
{
    global $wpdb, $tables_response;

    $estimate = json_decode(json_encode(simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA)), true);

    if (!empty($tables_response[$action]) && !empty($estimate['QBXMLMsgsRs']) && !empty($estimate['QBXMLMsgsRs'][$tables_response[$action]['response_action']]) && !empty($estimate['QBXMLMsgsRs'][$tables_response[$action]['response_action']][$tables_response[$action]['response_subaction']]))
    {
        $estimate_data = $estimate['QBXMLMsgsRs'][$tables_response[$action]['response_action']][$tables_response[$action]['response_subaction']];
        $is_magic_quotes = get_magic_quotes_gpc();

        if (isset($estimate_data['EstimateLineRet']))
        {
            $estimate_lineitems = $estimate_data['EstimateLineRet'];
            unset($estimate_data['EstimateLineRet']);
        }

        $estimate_info = hunter_build_table_data_array($estimate_data, $tables_response[$action]['columns']);

        // Insert Customers...
        if (!empty($estimate_info['add']))
        {
            $add_columns = array(
                'names_and_values' => array(),
                'formats' => array()
            );

            foreach($tables_response[$action]['columns'] as $column_name => $format)
            {
                if (isset($estimate_info['add'][$column_name]))
                {
                    $add_columns['names_and_values'][$column_name] = htmlspecialchars_decode($estimate_info['add'][$column_name], ENT_NOQUOTES);
                    $add_columns['formats'][] = $format;
                }
            }

            // Add in the additional columns that are used for tracking, but not included in Quickbooks.
            if (!empty($extra['additional_columns']))
            {
                foreach($extra['additional_columns'] as $column_name => $column_data)
                {
                    $add_columns['names_and_values'][$column_name] = htmlspecialchars_decode($extra['additional_columns'][$column_name]['value'], ENT_NOQUOTES);
                    $add_columns['formats'][] = $extra['additional_columns'][$column_name]['format'];
                }
            }

            // Add Estimate into Database!
            $wpdb->insert( 
                $tables_response[$action]['table'],
                $add_columns['names_and_values'],
                $add_columns['formats']
            );

            $submit_time = str_replace('.', '_', $extra['submit_time']);

            $vendor_item_ids_transient = get_transient('vQBIDs_' . $extra['submission_id'] . '_' . $submit_time);

            if (!empty($vendor_item_ids_transient))
            {
                $vendor_item_ids = implode(',', $vendor_item_ids_transient);
                $wpdb->query("UPDATE " . $wpdb->prefix . "quickbook_vendor_items SET EstimateID = " . $wpdb->insert_id . " WHERE id IN(" . $vendor_item_ids . ")");

                delete_transient('vQBIDs_' . $extra['submission_id'] . '_' . $submit_time);
            }

            $estimate_info['lineitems'] = array(
                'EstimateTxnID' => isset($estimate_info['add']['TxnID']) ? $estimate_info['add']['TxnID'] : '',
                'Items' => !empty($estimate_lineitems) ? $estimate_lineitems : array()
            );

            if (isset($estimate_info['add']['TxnID'], $estimate_info['add']['EditSequence']))
                $table_data[str_replace('-', '_', $estimate_info['add']['TxnID'])] = $estimate_info['add']['EditSequence'];
        }

        if (!empty($tables_response[$action]['secondary_table']) && !empty($tables_response[$action]['secondary_columns']) && !empty($estimate_info['lineitems']))
        {
            $lineitems = array();
            $estimate_lineitems = isAssociativeArray($estimate_info['lineitems']['Items']) ? array($estimate_info['lineitems']['Items']) : $estimate_info['lineitems']['Items'];

            foreach($estimate_lineitems as $index => $information)
            {
                foreach($information as $key => $item_info)
                {
                    if ($key == 'Quantity')
                    {
                        $item_info = (float) $item_info;
                        if (empty($item_info) || $item_info == '0.00')
                            $item_info = 0;
                    }
                    // Desc is reserved in MYSQL, so change to Description
                    if ($key == 'Desc')
                        $key = 'Description';

                    if (is_array($item_info))
                    {
                        foreach($item_info as $subkey => $value)
                        {
                            if (!is_array($value) && isset($tables_response[$action]['secondary_columns'][$key . '_' . $subkey]))
                                $lineitems[$index][$key . '_' . $subkey] = $is_magic_quotes ? htmlspecialchars_decode(stripslashes($value), ENT_NOQUOTES) : htmlspecialchars_decode($value, ENT_NOQUOTES);
                        }
                    }
                    else if (isset($tables_response[$action]['secondary_columns'][$key]))
                        $lineitems[$index][$key] = $is_magic_quotes ? htmlspecialchars_decode(stripslashes($item_info), ENT_NOQUOTES) : htmlspecialchars_decode($item_info, ENT_NOQUOTES);
                }
            }

            if (!empty($lineitems))
            {
                foreach($lineitems as $lineitem)
                {
                    $secondary_columns = array(
                        'names_and_values' => array(),
                        'formats' => array()
                    );

                    foreach($tables_response[$action]['secondary_columns'] as $secondary_column_name => $secondary_format)
                    {
                        if (isset($lineitem[$secondary_column_name]))
                        {
                            $secondary_columns['names_and_values'][$secondary_column_name] = $lineitem[$secondary_column_name];
                            $secondary_columns['formats'][] = $secondary_format;
                        }
                    }

                    // Additional columns
                    if (!empty($extra['additional_secondary_columns']))
                    {
                        $founds = array_keys($extra['additional_secondary_columns']['items'], $lineitem['ItemRef_FullName']);

                        if (!empty($founds))
                        {
                            foreach($founds as $key)
                            {
                                foreach($extra['additional_secondary_columns']['columns'] as $db_column_name => $col_data)
                                {
                                    // start with empty array...
                                    $dsata = array();
                                    $dsata[$db_column_name] = $col_data['values'][$key];
                                    $secondary_columns['names_and_values'] = array_merge($seconday_columns['names_and_values'], $dsata);
                                    $secondary_columns['formats'][] = $col_data['format'];
                                }
                            }
                        }
                    }

                    $secondary_columns['names_and_values'] = array_merge(array('EstimateTxnID' => $estimate_info['lineitems']['EstimateTxnID']), $secondary_columns['names_and_values']);
                    $secondary_columns['formats'] = array_merge(array('%s'), $secondary_columns['formats']);

                    $wpdb->insert(
                        $tables_response[$action]['secondary_table'],
                        $secondary_columns['names_and_values'],
                        $secondary_columns['formats']
                    );
                }
            }
        }
    }
}

When I file_put_contents to see the results of $xml in the request function, I get the xml I posted up earlier, which there is nothing wrong with. But Quickbooks is returning the parse error and is happening in the adding of the Estimate only! The Items, Vendors, and Customer all get created properly in Quickbooks, only the adding of the Estimate is the problem here.


Solution

  • Anytime you see this:

    QuickBooks found an error when parsing the provided XML text stream.

    The first thing you should do is grab the qbXML from the logs, and run to the XML Validator tool included with the QuickBooks SDK.

    The XML Validator tool will tell you EXACTLY what is wrong if you run the qbXML through it.

    Look in the quickbooks_log SQL table and find the actual qbXML that was sent to the Web Connector. Or, turn the Web Connector to VERBOSE mode and grab the qbXML request from the Web Connector log. DO NOT use your own logging. It is not accurate. The framework inserts a requestID attribute that is used for tracking, so any logging you put in place will NOT be the actual request sent.

    Take the qbXML from the log, and put it through the XML Validator tool. It will tell you a line number an error message.

    If you run this through the XML Validator tool that is included with the QuickBooks SDK, you get:

    Line: 14
    LinePos: 23
    Src Text: <Province>PA</Province>
    Reason: Element content is invalid according to the DTD/Schema.
    Expecting: State, PostalCode, Country, Note.
    

    If you look at the QuickBooks OSR, you then notice that there is no <Province> tag shown in the OSR.

    The validator is telling you that the Province tag is invalid, and it's expecting that you specify one of these tags instead: State, PostalCode, Country, Note.