Editing QuickBooks for Windows inventory quantity on hand with QuickBooks PHP Dev Kit / QuickBooks Web Connector

392 views Asked by At

Summary

So I set up the example_mysql_mirror and got it all working but for some reason it always overwrites the changes in MySQL regardless of qbsql_modify_timestamp. I noticed the mirroring wiki page http://www.consolibyte.com/wiki/doku.php/quickbooks_integration_php_consolibyte_sqlmirror references a different field (qbsql_modify_datetime) but just chalked that up to internal changes that were never changed in the wiki. I am updating table "qb_iteminventory" and the key "QuantityOnHand" for any/all rows.

My ultimate objective is to be able to update inventory quantity for items. The mirror example was simply a first working example that it is not only possible but easily done (probably the same reason why it was created).

Upon further digging I have come to a few conclusions, one being that the QuickBooks PHP Dev Kit example_mysql_mirror.php is not complete. I did see the WARNING and I am using the latest GitHub code.

I feel it's a problem with time (internal qbsql_modify_timestamp processing) or a parent issue (qb_iteminventory a sub-item/child of a parent that needs to be updated as well, etc.), or even a missing field (I noticed QuantityOnHand but did not see QuantityAvailable in MySQL. Probably requiring using a different report for the QB inventory exchange (not sure if this is needed?).

CODE

example_mysql_mirror.php

// I always program in E_STRICT error mode with error reporting turned on... 
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
// Set the include path
require_once dirname(__FILE__) . '/../../QuickBooks.php';
// You should make sure this matches the time-zone QuickBooks is running in
if (function_exists('date_default_timezone_set'))
{
    date_default_timezone_set('America/Los_Angeles');
}
// The username and password the Web Connector will use to connect with
$username = '';
$password = '';
// Database connection string
//
// You *MUST* start with a fresh database! If the database you use has any 
//  quickbooks_* or qb_* related tables in it, then the schema *WILL NOT* build 
//  correctly! 
//  
//  Currently, only MySQL is supported/tested. 
$dsn = 'mysqli://:@localhost/spray_quickbooks';
// If the database has not been initialized, we need to initialize it (create 
//  schema and set up the username/password, etc.)
if (!QuickBooks_Utilities::initialized($dsn))
{
    header('Content-Type: text/plain');

    // It takes a really long time to build the schema... 
    set_time_limit(0);

    $driver_options = array(
        );

    $init_options = array(
        'quickbooks_sql_enabled' => true, 
        );

    QuickBooks_Utilities::initialize($dsn, $driver_options, $init_options);
    QuickBooks_Utilities::createUser($dsn, $username, $password);

    exit;
}
// What mode do we want to run the mirror in? 
//$mode = QuickBooks_WebConnector_Server_SQL::MODE_READONLY;        // Read from QuickBooks only (no data will be pushed back to QuickBooks)
//$mode = QuickBooks_WebConnector_Server_SQL::MODE_WRITEONLY;       // Write to QuickBooks only (no data will be copied into the SQL database)
$mode = QuickBooks_WebConnector_Server_SQL::MODE_READWRITE;     // Keep both QuickBooks and the database in sync, reading and writing changes back and forth)
// What should we do if a conflict is found? (a record has been changed by another user or process that we're trying to update)
$conflicts = QuickBooks_WebConnector_Server_SQL::CONFLICT_LOG;
// What should we do with records deleted from QuickBooks? 
//$delete = QuickBooks_WebConnector_Server_SQL::DELETE_REMOVE;      // Delete the record from the database too
$delete = QuickBooks_WebConnector_Server_SQL::DELETE_FLAG;      // Just flag it as deleted
// Hooks (optional stuff)
$hooks = array();
/*
// Hooks (optional stuff)
$hook_obj = new MyHookClass2('Keith Palmer');
$hooks = array(
    // Register a hook which occurs when we perform an INSERT into the SQL database for a record from QuickBooks
    // QuickBooks_SQL::HOOK_SQL_INSERT => 'my_function_name_for_inserts', 
    // QuickBooks_SQL::HOOK_SQL_INSERT => 'MyHookClass::myMethod',

    // Register a hook which occurs when we perform an UPDATE on the SQL database for a record from QuickBooks
    // QuickBooks_SQL::HOOK_SQL_UPDATE => 'my_function_name_for_updates',
    // Example of registering multiple hooks for one hook type 
    // QuickBooks_SQL::HOOK_PREHANDLE => array(
    //  'my_prehandle_function',
    //  array( $hook_obj, 'myMethod' ),
    //  ),

    // Example of using the hook factory to use a pre-defined hook
    // QuickBooks_SQL::HOOK_SQL_INSERT => QuickBooks_Hook_Factory::create(
    //  'Relay_POST',                               // Relay the hook data to a remote URL via a HTTP POST
    //  'http://localhost:8888/your_script.php'),

    QuickBooks_SQL::SQL_INSERT => array(
        QuickBooks_Hook_Factory::create(
            'Relay_POST', 
            'http://localhost:8888/your_script.php', 
            array( '_secret' => 'J03lsN3at@pplication' ) ), 
        ), 
    );
class MyHookClass
{
    static public function myMethod($requestID, $user, $hook, &$err, $hook_data, $callback_config)
    {
        // do something here...
        return true;
    }
}
function my_prehandle_function($requestID, $user, $hook, &$err, $hook_data, $callback_config)
{
    //print('here we are!');
    return true;
}
class MyHookClass2
{
    protected $_var;

    public function __construct($var)
    {
        $this->_var = $var;
    }

    public function myMethod($requestID, $user, $hook, &$err, $hook_data, $callback_config)
    {
        //print('variable equals: ' . $this->_var);
        return true;
    }
}
*/
// 
$soap_options = array();
// 
$handler_options = array(
    'deny_concurrent_logins' => false,
    'deny_reallyfast_logins' => false, 
    );
// 
$driver_options = array();
$ops = array(
    QUICKBOOKS_OBJECT_SALESTAXITEM, 
    QUICKBOOKS_OBJECT_SALESTAXCODE, 
    QUICKBOOKS_OBJECT_CUSTOMER, 
    QUICKBOOKS_OBJECT_VENDOR, 

    QUICKBOOKS_OBJECT_TEMPLATE, 

    QUICKBOOKS_OBJECT_CUSTOMERTYPE, 
    QUICKBOOKS_OBJECT_VENDORTYPE, 
    QUICKBOOKS_OBJECT_ESTIMATE, 
    QUICKBOOKS_OBJECT_INVOICE, 
    QUICKBOOKS_OBJECT_CLASS, 

    QUICKBOOKS_OBJECT_INVOICE, 

    QUICKBOOKS_OBJECT_INVENTORYITEM,

    /* Not quite sure why these are not being used
    QUICKBOOKS_OBJECT_NONINVENTORYITEM, 
    QUICKBOOKS_OBJECT_SERVICEITEM, 
    QUICKBOOKS_OBJECT_SHIPMETHOD, 
    QUICKBOOKS_OBJECT_PAYMENTMETHOD, 
    QUICKBOOKS_OBJECT_TERMS, 
    QUICKBOOKS_OBJECT_PRICELEVEL, 
    QUICKBOOKS_OBJECT_ITEM,
    */ 

    QUICKBOOKS_OBJECT_PAYMENTMETHOD, 

    QUICKBOOKS_OBJECT_COMPANY, 
    QUICKBOOKS_OBJECT_HOST, 
    QUICKBOOKS_OBJECT_PREFERENCES,
    );
$ops_misc = array(      // For fetching inventory levels, deleted transactions, etc. 
    QUICKBOOKS_DERIVE_INVENTORYLEVELS,
    QUICKBOOKS_QUERY_DELETEDLISTS,
    QUICKBOOKS_QUERY_DELETEDTRANSACTIONS,
    // 'nothing', 
    );
// 
$sql_options = array(
    'only_import' => $ops,
    'only_add' => $ops, 
    'only_modify' => $ops, 
    'only_misc' => $ops_misc, 
    );
// 
$callback_options = array();
// $dsn_or_conn, $how_often, $mode, $conflicts, $users = null, 
//  $map = array(), $onerror = array(), $hooks = array(), $log_level, $soap = QUICKBOOKS_SOAPSERVER_BUILTIN, $wsdl = QUICKBOOKS_WSDL, $soap_options = array(), $handler_options = array(), $driver_options = array()
$Server = new QuickBooks_WebConnector_Server_SQL(
    $dsn, 
    '1 minute', 
    $mode, 
    $conflicts, 
    $delete,
    $username, 
    array(), 
    array(), 
    $hooks, 
    QUICKBOOKS_LOG_DEVELOP, 
    QUICKBOOKS_SOAPSERVER_BUILTIN, 
    QUICKBOOKS_WSDL,
    $soap_options, 
    $handler_options, 
    $driver_options,
    $sql_options, 
    $callback_options);
$Server->handle(true, true);

save.php

<?php
if (count($_POST) == 0) {
    dErr("There is nothing to save. Try again later.");
}

// Compile list of rows to update
$updates = array();
foreach ($_POST as $key => $value) {
    array_push($updates, array(explode('_', $key)[1], $value));
}
unset($key);
unset($value);

// TODO: Database updating
// IMPORTANT: Add validation and other selective functionality before adding saving
$qb = new mysqli('127.0.0.1', '', '', 'spray_quickbooks');

// Oh no! A connect_errno exists so the connection attempt failed!
if ($qb->connect_errno) {
    dErr("Error: Failed to make a MySQL connection, here is why: <br />Errno: " . $qb->connect_errno . "<br />Error: " . $qb->connect_error);
}

$qbe = new mysqli('127.0.0.1', '', '', 'spray_qb_extras');

// Oh no! A connect_errno exists so the connection attempt failed!
if ($qbe->connect_errno) {
    dErr("Error: Failed to make a MySQL connection, here is why: <br />Errno: " . $qbe->connect_errno . "<br />Error: " . $qbe->connect_error);
}

foreach ($updates as $update) {
    // Perform an SQL query
    $sql = "UPDATE qb_iteminventory SET QuantityOnHand='" . $update[1] . "' WHERE qbsql_id='" . $update[0] . "'";
    if (!$qb_result = $qb->query($sql)) {
        dErr("Error: Our query failed to execute and here is why: <br />Query: " . $sql . "<br />Errno: " . $qb->errno . "<br />Error: " . $qb->error);
    }
}
unset($updates);
unset($update);

// Redirect back to where they came from
echo "<meta http-equiv=\"refresh\" content=\"0;url=".$_SERVER['HTTP_REFERER']."\"/>";

// Extra Functions
function dErr($msg) {
    echo "<center><b>Sorry, we have encountered an error.</b><br /><br />";
    echo $msg;
    echo "</center>";
    exit;
}
?>

Updated save.php (Inventory Adjustments)

<?php
if (count($_POST) == 0) {
    dErr("There is nothing to save. Please try again later.");
}

// Compile list of rows to update
$updates = array();
foreach ($_POST as $key => $value) {
    array_push($updates, array(explode('_', $key)[1], $value));
}
/*unset($key);
unset($value);*/

// IMPORTANT: Add validation and other selective functionality before adding saving
//            and stop saving unchanged items just because we can
$qb = new mysqli('127.0.0.1', '', '', 'spray_quickbooks');

// Oh no! A connect_errno exists so the connection attempt failed!
if ($qb->connect_errno) {
    dErr("Error: Failed to make a MySQL connection, here is why: <br />Errno: " . $qb->connect_errno . "<br />Error: " . $qb->connect_error);
}

// IMPORTANT: ONLY UPDATE CHANGED ROWS. WE DONT WANT INVENTORY ADJUSTMENTS FOR UNCHANGED ITEMS!
foreach ($updates as $update) {
    // Update QuantityOnHand still so our web interface can easily see the new quantity before QB sync
    $sql = "UPDATE qb_iteminventory SET QuantityOnHand='" . $update[1] . "' WHERE qbsql_id='" . $update[0] . "'";
    if (!$qb_result = $qb->query($sql)) {
        dErr("Error: Our query failed to execute and here is why: <br />Query: " . $sql . "<br />Errno: " . $qb->errno . "<br />Error: " . $qb->error);
    }

    // Get a newly updated item so we can extract Item's FullName
     $sql = "SELECT * FROM qb_iteminventory WHERE qbsql_id='" . $update[0] . "'";
    if (!$qb_result = $qb->query($sql)) {
        dErr("Error: Our query failed to execute and here is why: <br />Query: " . $sql . "<br />Errno: " . $qbe->errno . "<br />Error: " . $qbe->error);
    }

    $row = mysqli_fetch_assoc($qb_result);
    //print_r($row);

    // Generate unique TxnID
    // Apparently QuickBooks will overwrite it with the permanent TxnID when it syncs
    $tID = rand(1000, 9999);

    // Insert new Item Adjustment
    $sql = "INSERT INTO `qb_inventoryadjustment` ( `TxnID`, `TimeCreated`, `TimeModified`,  `Account_FullName`, `TxnDate`, `RefNumber`,  `Memo`, `qbsql_discov_datetime`, `qbsql_resync_datetime`, `qbsql_modify_timestamp` ) VALUES ( 'TxnID-" . $tID . "', now(), now(),  'Inventory Adjustments', CURDATE(), '" . $tID . "', NULL, NULL, NULL, now() )";
    if (!$qb_result = $qb->query($sql)) {
        dErr("Error: Our query failed to execute and here is why: <br />Query: " . $sql . "<br />Errno: " . $qb->errno . "<br />Error: " . $qb->error);
    }

    // Insert new Item Adjustment Line
    $sql = "INSERT INTO `qb_inventoryadjustment_inventoryadjustmentline` ( `InventoryAdjustment_TxnID`, `SortOrder`, `TxnLineID`, `Item_FullName`, `QuantityAdjustment_NewQuantity` ) VALUES ( 'TxnID-" . $tID . "', '0', 'TxnLID-" . $tID . "', '" . $row['FullName'] . "', " . $update[1] . ");";
    if (!$qb_result = $qb->query($sql)) {
        dErr("Error: Our query failed to execute and here is why: <br />Query: " . $sql . "<br />Errno: " . $qb->errno . "<br />Error: " . $qb->error);
    }
}

// TODO: Research whether this is really required, and to what extent
// INFO: Not sure why I feel like this is important
/*$qb_result->free();
$qb->close();
unset($updates);
unset($update);
unset($sql);*/

// Redirect back to where they came from
//echo "<meta http-equiv=\"refresh\" content=\"0;url=".$_SERVER['HTTP_REFERER']."\"/>";

// Extra Functions
function dErr($msg) {
    echo "<center><b>Sorry, we have encountered an error.</b><br /><br />";
    echo $msg;
    echo "</center>";
    exit;
}
?>
1

There are 1 answers

9
Keith Palmer Jr. On BEST ANSWER

SQL Mirror for Inventory Adjustments:

First, the usual disclaimer - the SQL mirror stuff is beta, so don't expect 100% functionality (as indicated in the release notes). With that said...

QuickBooks does not let you update quantities directly by editing the inventory item. If you go into the QuickBooks UI you will see this same behavior there as well.

So something like this will not work:

$sql = "UPDATE qb_iteminventory SET QuantityOnHand='" . $update[1] . "' WHERE qbsql_id='" . $update[0] . "'";

The above query would be telling QuickBooks to just replace the given quantity on hand, which really doesn't work in the "accounting world" because inventory changes have tax implications (businesses pay tax on the amount of inventory they carry), revenue implications (a change in inventory generally means you either bought more items from a manufacturer, or sold an item to a customer), and physical implications (the quantity or something changed, which means a physical product went to a customer, etc.) that accountants/businesses need to keep very close track of and have audit logs showing how and why the quantity changed when it did.

Instead, the way you change inventory in QuickBooks is via separate transactions. For example:

  • an Invoice would reduce quantity on hand (you sold something)
  • a Item Receipt would increase quantity on hand (you bought something from a manufacturer or vendor)
  • if just need to make manual adjustments (e.g. someone in the warehouse broke a product by mistake, or you lost inventory due to theft, etc.), then you use a Inventory Adjustment

The QuickBooks SDK (and thus the mirror code) follows this convention -- if you want to change quantities, you need to create a transaction.

You probably want to create an Inventory Adjustment (the qb_inventoryadjustment SQL table). Something like this:

INSERT INTO `qb_inventoryadjustment` ( `TxnID`, `TimeCreated`, `TimeModified`,  `Account_FullName`, `TxnDate`, `RefNumber`,  `Memo`, `qbsql_discov_datetime`, `qbsql_resync_datetime`, `qbsql_modify_timestamp` )
VALUES ( 'TxnID-1234', now(), now(),  'Inventory Adjustments', '2016-12-23', '1234', 'Test adjustment', NULL, NULL, now() );

INSERT INTO `qb_inventoryadjustment_inventoryadjustmentline` ( `InventoryAdjustment_TxnID`, `SortOrder`, `TxnLineID`, `NewQuantity` )
VALUES ( 'TxnID-1234', '0', 'ABCD-1234', '10' );

If you look in the QuickBooks UI, you'll see that Inventory Adjustment transactions have both base detail (reference number, date, etc.) and line-item level detail (items and quantities), so you have to make sure you supply both (the qb_inventoryadjustment table for the base detail and the qb_inventoryadjustment_inventoryadjustmentline table for the lines)

Make sure in your $ops array you have Inventory Adjustments enabled:

$ops = array(
   ... 
   QUICKBOOKS_OBJECT_INVENTORYADJUSTMENT
   ...
   );

The QUICKBOOKS_OBJECT_INVENTORYITEM only syncs the actual product itself, and will not sync actual quantity changes of the product.

If you find the SQL Mirror doesn't work for you for whatever reason...:

The SQL mirror stuff is an experiment, and it doesn't always work properly. Boo. :-(

However, there's a great alternative if you don't mind writing a little code (and hey, you're on StackOverflow, so you don't!).

Instead, follow the quick-start linked to by the GitHub project:

Basically, you'll end up pointing the Web Connector at something like this:

// Require the framework
require_once '../../QuickBooks.php';

// A username and password you'll use in: 
//  a) Your .QWC file
//  b) The Web Connector
//  c) The QuickBooks framework
$user = 'quickbooks';
$pass = 'password';

// Map QuickBooks actions to handler functions
$map = array(
    // ...
    );

// This is entirely optional, use it to trigger actions when an error is returned by QuickBooks
$errmap = array();

// An array of callback hooks
$hooks = array();

// Logging level
$log_level = QUICKBOOKS_LOG_DEVELOP;        // Use this level until you're sure everything works!!!

// * MAKE SURE YOU CHANGE THE DATABASE CONNECTION STRING BELOW TO A VALID MYSQL USERNAME/PASSWORD/HOSTNAME *
$dsn = 'mysql://root:root@localhost/quickbooks_server';

if (!QuickBooks_Utilities::initialized($dsn))
{
    // Initialize creates the neccessary database schema for queueing up requests and logging
    QuickBooks_Utilities::initialize($dsn);

    // This creates a username and password which is used by the Web Connector to authenticate
    QuickBooks_Utilities::createUser($dsn, $user, $pass);
}

// Create a new server and tell it to handle the requests
$Server = new QuickBooks_WebConnector_Server($dsn, $map, $errmap, $hooks, $log_level);
$response = $Server->handle(true, true);

Which you can then adapt to send InventoryAdjustmentAdd transactions pretty easily by:

  • Adding QUICKBOOKS_ADD_INVENTORYADJUSTMENT => array( '_quickbooks_inventoryadjustment_add_request', '_quickbooks_inventoryadjustment_add_response' ) to $map
  • Copying in the below functions and tweaking to your needs:

-

/**
 * Generate a qbXML response to add a particular customer to QuickBooks
 * 
 * @return string                           A valid qbXML request
 */
function _quickbooks_inventoryadjustment_add_request($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $version, $locale, $callback_config)
{
    $Driver = QuickBooks_Driver_Singleton::getInstance();

    $errnum = null;
    $errmsg = null;
    $data = $Driver->fetch($Driver->query("SELECT * FROM qb_inventoryadjustment WHERE qbsql_id = %d", $errnum, $errmsg, 0, 1, array( $ID )));

    $res_lines = $Driver->query("SELECT * FROM qb_inventoryadjustment_inventoryadjustmentline WHERE InventoryAdjustment_TxnID = '%s' ORDER BY SortOrder ASC", $errnum, $errmsg, null, null, array( $data['TxnID'] ));

    foreach ($data as $key => $value)
    {
        //$data[$key] = QuickBooks_Cast::cast(QUICKBOOKS_OBJECT_CUSTOMER, str_replace('_', ' ', $key), $value);
    }

    $str_action = 'InventoryAdjustmentAdd';
    $TxnID = '';
    $EditSequence = '';
    if ($action == 'InventoryAdjustmentMod')
    {
        $str_action = 'InventoryAdjustmentMod';
        $TxnID = '<TxnID>' . $data['TxnID'] . '</TxnID>';
        $EditSequence = '<EditSequence>' . $data['EditSequence'] . '</EditSequence>';
    }

    $xml = '<?xml version="1.0" encoding="utf-8"?>
        <?qbxml version="' . $version . '"?>
        <QBXML>
            <QBXMLMsgsRq onError="stopOnError">
                <' . $str_action . 'Rq requestID="' . $requestID . '">
                    <' . $str_action . '>
                        ' . $TxnID . '
                        ' . $EditSequence . '

                        <AccountRef>
                            <FullName>' . $data['Account_FullName'] . '</FullName>
                        </AccountRef>

                        <TxnDate>' . $data['TxnDate'] . '</TxnDate>
                        <!--<RefNumber>' . $data['RefNumber'] . '</RefNumber>-->

                        <Memo>' . $data['Memo'] . '</Memo>
                        ';

    while ($line = $Driver->fetch($res_lines))
    {
        $xml .= '
                        <InventoryAdjustmentLineAdd>
                            <ItemRef>';

        if ($line['Item_ListID'])
        {
            $xml .= '
                                <ListID>' . $line['Item_ListID'] . '</ListID>';
        }
        else
        {
            $xml .= '
                                <FullName>' . $line['Item_FullName'] . '</FullName>';
        }

        $xml .= '
                            </ItemRef>

                            <QuantityAdjustment>';

        if ($line['QuantityDifference'])
        {
            $xml .= '
                                <QuantityDifference>' . $line['QuantityDifference'] . '</QuantityDifference>';
        }
        else
        {
            $xml .= '
                                <NewQuantity>' . $line['NewQuantity'] . '</NewQuantity>';
        }

        $xml .= '
                            </QuantityAdjustment>
                        </InventoryAdjustmentLineAdd>
                ';
    }

    $xml .= '

                    </' . $str_action . '>
                </' . $str_action . 'Rq>
            </QBXMLMsgsRq>
        </QBXML>';

    return $xml;
}

/**
 * Receive a response from QuickBooks 
 */
function _quickbooks_inventoryadjustment_add_response($requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents, $callback_config)
{   
    $Driver = QuickBooks_Driver_Singleton::getInstance();

    $datetime = date('Y-m-d H:i:s');

    $errnum = null;
    $errmsg = null;
    $data = $Driver->fetch($Driver->query("SELECT * FROM qb_inventoryadjustment WHERE qbsql_id = %d", $errnum, $errmsg, 0, 1, array( $ID )));

    if ($data)
    {
        // Get the existing lines 
        $res_lines = $Driver->query("SELECT * FROM qb_inventoryadjustment_inventoryadjustmentline WHERE InventoryAdjustment_TxnID = '%s' ORDER BY qbsql_id ASC ", $errnum, $errmsg, null, null, array( $data['TxnID'] ));

        // Update ListID/EditSequence
        $errnum = null;
        $errmsg = null;
        $Driver->query("
            UPDATE 
                qb_inventoryadjustment
            SET 
                TxnID = '%s', 
                EditSequence = '%s', 
                TimeCreated = '%s', 
                TimeModified = '%s', 
                RefNumber = '%s', 
                qbsql_discov_datetime = '%s', 
                qbsql_resync_datetime = '%s', 
                qbsql_modify_timestamp = '%s'
            WHERE 
                qbsql_id = %d ", $errnum, $errmsg, 0, 1, 
            array( 
                $idents['TxnID'], 
                $idents['EditSequence'], 
                date('Y-m-d H:i:s'), 
                date('Y-m-d H:i:s'), 
                $idents['RefNumber'],
                $datetime, 
                $datetime, 
                $datetime,
                $ID ));

        // Parse the XML we got back 
        // Import all of the records
        $errnum = 0;
        $errmsg = '';
        $Parser = new QuickBooks_XML_Parser($xml);
        if ($Doc = $Parser->parse($errnum, $errmsg))
        {
            $Root = $Doc->getRoot();
            $List = $Root->getChildAt('QBXML/QBXMLMsgsRs/InventoryAdjustmentAddRs');

            $TxnLineIDs = array();

            foreach ($List->children() as $InventoryAdjustment)
            {
                // Process the line items
                foreach ($InventoryAdjustment->children() as $Child)
                {
                    if ($Child->name() == 'InventoryAdjustmentLineRet')
                    {
                        // Store the TxnLineID
                        $TxnLineIDs[] = $Child->getChildDataAt('InventoryAdjustmentLineRet TxnLineID');
                    }
                }
            }

            reset($TxnLineIDs);
            while ($line = $Driver->fetch($res_lines))
            {
                $TxnLineID = current($TxnLineIDs);
                next($TxnLineIDs);

                // Update each line item with the TxnID and the TxnLineID 
                $Driver->query("
                    UPDATE 
                        qb_inventoryadjustment_inventoryadjustmentline
                    SET 
                        InventoryAdjustment_TxnID = '%s', 
                        TxnLineID = '%s', 
                        qbsql_discov_datetime = '%s', 
                        qbsql_resync_datetime = '%s', 
                        qbsql_modify_timestamp = '%s'
                    WHERE 
                        qbsql_id = %d ", $errnum, $errmsg, null, null,  
                    array( 
                        $idents['TxnID'], 
                        $TxnLineID, 
                        $datetime, 
                        $datetime, 
                        $datetime,
                        $line['qbsql_id'] ));
            }
        }

    }
}

Those functions ^^^ do essentially the same thing the SQL mirror attempts to do, but they are a little more controllable and tweakable than the SQL mirror stuff is since you have complete control over the qbXML there.

Remember if you go this route, you need to queue up the request whenever you want to trigger it to send to QuickBooks:

$Queue->enqueue(QUICKBOOKS_ADD_INVENTORYADJUSTMENT, $ID);

Finally:

I would highly recommend you familiarize yourself as much as possible with the QuickBooks UI. The SDK requests VERY closely match the UI, so it's very beneficial to know how to do things in the UI.