1

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.

Solomon Closson
  • 6,111
  • 14
  • 73
  • 115
  • QuickBooks does not produce an error that simply says "Parse Error". Specifically EXACTLY WHAT ERROR are you seeing, and EXACTLY WHERE are you seeing it? Also, why have you not posted your code? Post your code. – Keith Palmer Jr. Dec 04 '16 at 19:17
  • @KeithPalmerJr. updated question with all code. I don't think you need to see the Adding of Items, Vendors, and/or Customers code inside of the `server.php` file in order to see the problem, since the problem is only with the Adding of Estimates. I believe the priorities on adding Items, Vendors, Customers, etc. before adding Estimates in Quickbooks are properly set, in order to have the ability to add the Estimate. The XML that gets returned, also confirms this, but there is still a parse error in adding estimates somehow. – Solomon Closson Dec 05 '16 at 00:45
  • There are a couple functions that have not been included, but you can pretty much guess what they do based on their names. But let me know if you need to see these functions also. – Solomon Closson Dec 05 '16 at 00:52
  • Wondering the problem is with the setting of the Estimate Address? Do the string in these have requirements? For example the Country and/or State? Does State need to be an abbreviation? What about the province? Maybe there is an error someplace else, but I'm thinking possibly the address is problematic perhaps? – Solomon Closson Dec 05 '16 at 01:07

1 Answers1

1

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.

Keith Palmer Jr.
  • 27,666
  • 16
  • 68
  • 105
  • Ohh, you are referring to the tables on the client machine for the web connector, ok, let me try again and see what I get back. Give me a few... – Solomon Closson Dec 05 '16 at 04:11
  • Ok, I posted up the Outgoing XML Request, qbXML that gets sent via the request for adding an Estimate, that was found in the quickbooks_log table. I passed it through an XML Validator and it passes validation, so what to do next here? – Solomon Closson Dec 05 '16 at 05:05
  • Looks like Province is being set where State should be set instead, not sure if this is causing the problem though.... – Solomon Closson Dec 05 '16 at 05:10
  • Do you need the Outgoing SOAP Response perhaps? – Solomon Closson Dec 05 '16 at 05:14
  • Could it be that the items are being created as `SalesAndPurchase` yet all items that have NO BID defined in my app, do not get sent a price when adding the item, and than maybe when the Estimate gets created, it gives error, even when I set the price to 0.00 on that item for the Estimate? – Solomon Closson Dec 05 '16 at 05:58
  • Did you actually run this through the XML Validator tool THAT IS INCLUDED WITH THE QUICKBOOKS SDK? Because, it definitely doesn't pass validation. Editing answer to show you more. – Keith Palmer Jr. Dec 05 '16 at 13:19
  • Ok, well I don't have the SDK installed and not able to install it. Now looking at the possible tags for Estimate and Province is not 1 of them, yet you have this set in `Estimate.php`, so this should be removed from your file that you include as it's not available with Estimates. Please fix your `setBillAddress` method in `Estimate.php` where is says: `$this->set('BillAddress Province', $province);` – Solomon Closson Dec 05 '16 at 14:52
  • You can't install software on your own computer? You're in for a world of hurt developing for QuickBooks without the SDK. Good luck! The Province code is available in SOME versions of QuickBooks, just not in YOUR version (or at least the version of qbXML you're targeting). YOU are responsible for making sure YOU are sending the correct data/tags. If you leave that null/blank, then it won't be included in the qbXML output. – Keith Palmer Jr. Dec 05 '16 at 15:11
  • There is not an SDK for MACOSX, only for Windows. Unless you know where I can download one at? – Solomon Closson Dec 05 '16 at 15:12
  • There is only a Windows SDK unfortunately. VMWare Fusion is your friend. – Keith Palmer Jr. Dec 05 '16 at 16:37