/** * * * */ protected static function _queryResponse($type, $List, $requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents, $callback_config = array()) { $type = strtolower($type); $Driver = QuickBooks_Driver_Singleton::getInstance(); $objects = array(); // For each one of the objects we got back in the qbXML response... foreach ($List->children() as $Node) { // If this object is a base-level object, we're going to keep track of it's TxnID or // ListID so that we can use it to tie child elements back to this base-level // element (about 20 or 30 lines below this is that code) // Child records get deleted, and then re-created with the same // qbsql_id values so that we don't muck up people's associated // records. This keeps track of deleted records so we can re-create // the records with the same qbsql_id values. $deleted = array(); // Convert the XML nodes to objects, based on the XML to SQL schema definitions in Schema.php $objects = array(); QuickBooks_Callbacks_SQL_Callbacks::_transformToSQLObjects('', $Node, $objects); //print_r($objects); //exit; // For each object we created from the XML nodes... // (might have created more than one, e.g. an Invoice, plus 10 InvoiceLines, plus a Invoice_DataExt, etc.) /* if (count($objects) > 1) { print_r($objects); exit; } */ // This keeps track of whether or not we're ignoring this entire batch of UPDATES/INSERTS $ignore_this_and_its_children = false; foreach ($objects as $key => $object) { $Object =& $object; if ($ignore_this_and_its_children) { // If we're supposed to ignore this object and it's children, then just continue continue; } $table = $Object->table(); $path = $Object->path(); $map = array(); QuickBooks_SQL_Schema::mapPrimaryKey($path, QUICKBOOKS_SQL_SCHEMA_MAP_TO_SQL, $map); // Special hack for preferences if ($Object->table() == 'preferences') { $map = array('qb_preferences', 'qbsql_external_id'); } //print_r($Object); //print_r($path); //print_r($map); //exit; // if ($table and count($map) and $map[0] and $map[1]) { $addMapTest = array(); $addMapTestOthers = array(); QuickBooks_SQL_Schema::mapToSchema(trim(QuickBooks_Utilities::actionToXMLElement($action)), QUICKBOOKS_SQL_SCHEMA_MAP_TO_SQL, $addMapTest, $addMapTestOthers); if (!isset($extra['IsAddResponse']) and !isset($extra['is_add_response']) or !(count($addMapTest) and $addMapTest[0]) or $map[0] != $addMapTest[0]) { // GARRETT'S bug Fix -- Arrays with primary keys consisting of multiple fields weren't updating properly // due to failure to check for arrays. $multipart = array(); if (is_array($map[1])) { foreach ($map[1] as $table_name) { $multipart[$table_name] = $object->get($table_name); } } else { $multipart[$map[1]] = $object->get($map[1]); } } else { $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID] = $ID; } $hooks = array(); if (isset($callback_config['hooks'])) { $hooks = $callback_config['hooks']; } if ($tmp = $Driver->get(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $multipart)) { $actually_do_update = false; $actually_do_updaterelatives = false; $actually_do_deletechildren = false; if (isset($tmp[Quickbooks_Utilities::keyForAction($action)])) { // I have no idea what this does or what this is for.... // > EDIT: This keeps track of what the old TxnID or ListID is, so that we can use it to update relative tables $extra['AddResponse_OldKey'] = $tmp[Quickbooks_Utilities::keyForAction($action)]; $extra['temporary_TxnID_or_ListID_or_LineID'] = $tmp[Quickbooks_Utilities::keyForAction($action)]; } if (empty($extra['AddResponse_OldKey']) and Quickbooks_Utilities::keyForAction($action) == 'TxnID' and isset($tmp['TxnLineID'])) { //$extra['AddResponse_OldKey'] = $tmp->get("TxnLineID"); $extra['AddResponse_OldKey'] = $tmp['TxnLineID']; $extra['temporary_TxnID_or_ListID_or_LineID'] = $tmp['TxnLineID']; } // Make sure a conflict mode has been selected if (empty($callback_config['conflicts'])) { $callback_config['conflicts'] = null; } if (empty($callback_config['mode'])) { $callback_config['mode'] = QuickBooks_WebConnector_Server_SQL::MODE_READONLY; } if (isset($extra['is_query_response']) or isset($extra['is_import_response']) or isset($extra['is_mod_response']) or isset($extra['is_add_response'])) { // @TODO There should probably be some conflict handling code below to handle conflicts $actually_do_update = true; $actually_do_deletechildren = true; $actually_do_updaterelatives = true; } //$Driver->log('Diagnostics for incoming: is_query[' . !empty($extra['is_query_response']) . '], is_import[' . !empty($extra['is_import_response']) . '], is_mod[' . !empty($extra['is_mod_response']) . '], is_add[' . !empty($extra['is_add_response']) . '], conflict mode: ' . $callback_config['conflicts'] . '', null, QUICKBOOKS_LOG_DEVELOP); // Conflict handling code // @todo I think this should only apply to query and improt, right? I mean, if it's a mod or add, then // *of course* it was modified after resynced, thats how we knew to send it back to QuickBooks... if ($tmp[QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY] > $tmp[QUICKBOOKS_DRIVER_SQL_FIELD_RESYNC] and $callback_config['mode'] != QuickBooks_WebConnector_Server_SQL::MODE_READONLY) { // CONFLICT resolution code switch ($callback_config['conflicts']) { case QuickBooks_WebConnector_Server_SQL::CONFLICT_NEWER: $msg = 'Conflict mode: (newer) ' . $callback_config['conflicts'] . ' is not supported right now.'; trigger_error($msg); die($msg); case QuickBooks_WebConnector_Server_SQL::CONFLICT_QUICKBOOKS: // QuickBooks is master, so remove all existing child records of this record, then apply the QuickBooks version update $actually_do_deletechildren = true; $actually_do_update = true; //QuickBooks_Callbacks_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); //$Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array( $multipart )); break; case QuickBooks_WebConnector_Server_SQL::CONFLICT_CALLBACK: $msg = 'Conflict mode: (callback) ' . $callback_config['conflicts'] . ' is not supported right now.'; trigger_error($msg); die($msg); break; case QuickBooks_WebConnector_Server_SQL::CONFLICT_SQL: // The SQL table is the master table, but we have an out-of-date EditSequence value // In this case, what we want to do is update our record to the latest EditSequence value, // and then re-queue the object so that it gets updated the next time the sync runs to // the values from the SQL record $tmp_editsequence_update = new QuickBooks_SQL_Object($table, null); $tmp_editsequence_update->set('EditSequence', $object->get('EditSequence')); // *Just* update the EditSequence value $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmp_editsequence_update, array($multipart), false); // Re-queue it so the conflict gets resolved $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToMod($action), $tmp[QUICKBOOKS_DRIVER_SQL_FIELD_ID], true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); break; case QuickBooks_WebConnector_Server_SQL::CONFLICT_LOG: default: if (isset($extra['IsModResponse']) or isset($extra['is_mod_response']) or isset($extra['is_add_response'])) { // If it's actually a mod response, then this isn't actually a conflict, it's just the mod response happening normally $actually_do_update = true; $actually_do_deletechildren = true; $actually_do_updaterelatives = true; } else { // Log it...? $Driver->log('Conflict occured at: ' . $table, null, QUICKBOOKS_LOG_NORMAL); } break; } } //print_r($object); //print_r($tmp); // If the EditSequence has not changed since the last time this record was updated, // then we can just skip this update because everything should already be up to // date. // // This works around a very important issue as a result of Mod requests. When a Mod // request is issued and succeeds, it updates the record. Then, on the next Query // request, the record will be re-imported because the DateModified timestamp was // updated as a result of the Mod request. However, if the record is modified // by the end-user in between that Mod request and Import, the changes the user // made will be overwritten/a conflict will occur *even though the Query response // was only due to a Mod request that we sent ourselves* and the record in // QuickBooks never actually changed between the Mod and the Query. if (empty($extra['is_query_response']) and isset($tmp['EditSequence']) and $tmp['EditSequence'] == $object->get('EditSequence')) { $actually_do_update = false; $actually_do_deletechildren = false; $actually_do_updaterelatives = false; //$Driver->log('Ignoring UPDATE: ' . $table . ': ' . print_r($object, true) . ' due to EditSequence equality.', null, QUICKBOOKS_LOG_DEVELOP); // Make sure we ignore the children too (invoice lines, data exts, etc.) $ignore_this_and_its_children = true; } if ($callback_config['mode'] == QuickBooks_WebConnector_Server_SQL::MODE_WRITEONLY) { // In WRITE-ONLY mode, we only write changes to QuickBooks, but never read them back // (but should we update the EditSequence still?) $actually_do_update = false; $actually_do_deletechildren = false; $actually_do_updaterelatives = false; } //$deleted = array(); if ($actually_do_deletechildren) { QuickBooks_Callbacks_SQL_Callbacks::_deleteChildren($table, $user, $action, $ID, $object, $extra, $deleted); //$Driver->log('Immediately after deleting: ' . print_r($deleted, true)); } if ($actually_do_updaterelatives) { QuickBooks_Callbacks_SQL_Callbacks::_updateRelatives($table, $user, $action, $ID, $object, $extra); } if ($actually_do_update) { // This handles setting certain special fields (SortOrder, booleans, etc.) QuickBooks_Callbacks_SQL_Callbacks::_massageUpdateRecord($table, $object); //print('applying updates, and with these deletes: '); //print_r($deleted); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY, date('Y-m-d H:i:s')); //$Driver->log('Applying UPDATE: ' . $table . ': ' . print_r($object, true) . ', where: ' . print_r($multipart, true), null, QUICKBOOKS_LOG_DEVELOP); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array($multipart)); $qbsql_id = null; if (!empty($multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID])) { $qbsql_id = $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID]; } // Call any hooks that occur when a record is updated $hook_data = array('hook' => QuickBooks_SQL::HOOK_SQL_UPDATE, 'user' => $user, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $qbsql_id, 'where' => array($multipart)); $err = null; QuickBooks_Callbacks_SQL_Callbacks::_callHooks($hooks, QuickBooks_SQL::HOOK_SQL_UPDATE, $requestID, $user, $err, $hook_data, $callback_config); } else { //$Driver->log('Skipping UPDATE: ' . $table . ': ' . print_r($object, true) . ', where: ' . print_r($multipart, true), null, QUICKBOOKS_LOG_DEVELOP); } if ($actually_do_update and isset($extra['is_add_response'])) { // It's an add response, call the hooks $qbsql_id = null; if (!empty($multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID])) { $qbsql_id = $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID]; } // Call any hooks that occur when a record is updated $hook_data = array('hook' => QuickBooks_SQL::HOOK_QUICKBOOKS_INSERT, 'user' => $user, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $qbsql_id, 'where' => array($multipart)); $err = null; QuickBooks_Callbacks_SQL_Callbacks::_callHooks($hooks, QuickBooks_SQL::HOOK_QUICKBOOKS_INSERT, $requestID, $user, $err, $hook_data, $callback_config); } else { if ($actually_do_update and isset($extra['is_mod_response'])) { // It's an add response, call the hooks $qbsql_id = null; if (!empty($multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID])) { $qbsql_id = $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID]; } // Call any hooks that occur when a record is updated $hook_data = array('hook' => QuickBooks_SQL::HOOK_QUICKBOOKS_UPDATE, 'user' => $user, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $qbsql_id, 'where' => array($multipart)); $err = null; QuickBooks_Callbacks_SQL_Callbacks::_callHooks($hooks, QuickBooks_SQL::HOOK_QUICKBOOKS_UPDATE, $requestID, $user, $err, $hook_data, $callback_config); } } } else { // The record *DOES NOT* exist in the current table, so just INSERT it if ($callback_config['mode'] != QuickBooks_WebConnector_Server_SQL::MODE_WRITEONLY) { // This handles setting certain special fields (booleans, SortOrder, etc.) QuickBooks_Callbacks_SQL_Callbacks::_massageInsertRecord($table, $object); //$Driver->log('DELETED: ' . print_r($deleted, true) . ', table: [' . $table . ']'); // This makes sure that re-inserted child records are re-inserted with the // same qbsql_id values if (isset($deleted[$table][QUICKBOOKS_TXNLINEID][$object->get(QUICKBOOKS_TXNLINEID)][0])) { $tmp = $deleted[$table][QUICKBOOKS_TXNLINEID][$object->get(QUICKBOOKS_TXNLINEID)]; unset($deleted[$table][QUICKBOOKS_TXNLINEID][$object->get(QUICKBOOKS_TXNLINEID)]); // Can't use this anymore after it's been used for an INSERT $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_ID, $tmp[0]); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_USERNAME_ID, $tmp[1]); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_EXTERNAL_ID, $tmp[2]); } else { if (isset($deleted[$table][QUICKBOOKS_TXNLINEID]) and count($deleted[$table][QUICKBOOKS_TXNLINEID]) > 0) { // We deleted some child from this table, and what we deleted *should* // have been sent to QuickBooks and received from QuickBooks in the // same order... so we should be able to just fetch the next deleted // thing, and re-use that qbsql_id value reset($deleted[$table][QUICKBOOKS_TXNLINEID]); $tmp = array_shift($deleted[$table][QUICKBOOKS_TXNLINEID]); // Remove it from the list so it can't be used anymore $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_ID, $tmp[0]); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_USERNAME_ID, $tmp[1]); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_EXTERNAL_ID, $tmp[2]); } } if ('' == $object->get(QUICKBOOKS_DRIVER_SQL_FIELD_USERNAME_ID)) { $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_USERNAME_ID, null); } if ('' == $object->get(QUICKBOOKS_DRIVER_SQL_FIELD_EXTERNAL_ID)) { $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_EXTERNAL_ID, null); } //print_r($object); //$Driver->log('Applying INSERT: ' . $table . ': ' . print_r($object, true), null, QUICKBOOKS_LOG_DEVELOP); $object->set(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY, date('Y-m-d H:i:s')); $Driver->insert(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object); $last = $Driver->last(); // Call any hooks that occur when a record is inserted $hook_data = array('hook' => QuickBooks_SQL::HOOK_SQL_INSERT, 'user' => $user, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $last); $err = null; QuickBooks_Callbacks_SQL_Callbacks::_callHooks($hooks, QuickBooks_SQL::HOOK_SQL_INSERT, $requestID, $user, $err, $hook_data, $callback_config); } else { //$Driver->log('Skipping INSERT: ' . $table . ': ' . print_r($object, true), null, QUICKBOOKS_LOG_DEVELOP); } } // Triggered actions // Receive Payment => reload any linked invoices // Invoice => reload the customer // Purchase Order => reload the vendor QuickBooks_Callbacks_SQL_Callbacks::_triggerActions($user, $table, $Object, $action); } } } // Find out if we need to iterate further to get more results $matches = array(); //$iterator_count = ereg('iteratorRemainingCount="([0-9]*)" iteratorID="([^"]*)"', $xml, $matches); $matched_iteratorID = QuickBooks_XML::extractTagAttribute('iteratorID', $xml); $matched_iteratorRemainingCount = QuickBooks_XML::extractTagAttribute('iteratorRemainingCount', $xml); // If an iterator was used and there's results remaining if ($matched_iteratorID and $matched_iteratorRemainingCount > 0) { $extra = array('iteratorID' => $matched_iteratorID); // Set the iteratorID to be used /* // What is this code trying to do...? This doesn't look right... if ( (int) $matches[1] < QUICKBOOKS_SERVER_SQL_ITERATOR_MAXRETURNED) { $extra['maxReturned'] = (int) $matches[1]; } */ // queueEnqueue($user, $action, $ident, $replace = true, $priority = 0, $extra = null, $qbxml = null) $Driver->queueEnqueue($user, $action, null, true, QUICKBOOKS_SERVER_SQL_ITERATOR_PRIORITY, $extra); // Queue up another go! } else { // We're done with this iterator! // When the current iterator started... $module = __CLASS__; $type = null; $opts = null; // configRead($user, $module, $key, &$type, &$opts) $curr_sync_datetime = $Driver->configRead($user, $module, QuickBooks_Callbacks_SQL_Callbacks::_keySyncCurr($action), $type, $opts); // last sync started... //print('WRITING: [' . $curr_sync_datetime . '] from /' . $module . '/ {' . QuickBooks_Callbacks_SQL_Callbacks::_keySyncCurr($action) . '}'); // Start of the iteration, update the previous timestamp to NOW $Driver->configWrite($user, $module, QuickBooks_Callbacks_SQL_Callbacks::_keySyncPrev($action), $curr_sync_datetime, null); } }
/** * @TODO Change this to return false by default, and only catch the specific errors we're concerned with. * */ public static function catchall($requestID, $user, $action, $ident, $extra, &$err, $xml, $errnum, $errmsg, $config) { $Driver = QuickBooks_Driver_Singleton::getInstance(); /* $Parser = new QuickBooks_XML($xml); $errnumTemp = 0; $errmsgTemp = ''; $Doc = $Parser->parse($errnumTemp, $errmsgTemp); $Root = $Doc->getRoot(); $emailStr = var_export($Root->children(), true); $List = $Root->getChildAt('QBXML QBXMLMsgsRs '.QuickBooks_Utilities::actionToResponse($action)); $Node = current($List->children()); */ $map = array(); $others = array(); QuickBooks_SQL_Schema::mapToSchema(trim(QuickBooks_Utilities::actionToXMLElement($action)), QUICKBOOKS_SQL_SCHEMA_MAP_TO_SQL, $map, $others); $sqlObject = new QuickBooks_SQL_Object($map[0], trim(QuickBooks_Utilities::actionToXMLElement($action))); $table = $sqlObject->table(); switch ($errnum) { case 1: // These errors occur when we search for something and it doesn't exist // These errors occur when we search for something and it doesn't exist case 500: // i.e. we query for invoices modified since xyz, but there are none that have been modified since then // This isn't really an error, just ignore it return true; case 1000: // An internal error occured // @todo Hopefully at some point we'll have a better idea of how to handle this error... return true; case 3200: // Ignore EditSequence errors (the record will be picked up and a conflict reported next time it runs... maybe?) // @todo Think about this one more return true; case 3250: // This feature is not enabled or not available in this version of QuickBooks. // Do nothing (this can be safely ignored) return true; case 3100: // Name of List Element is already in use. $multipart = array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_NUMBER, $errnum); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, $errmsg); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $sqlObject, array($multipart)); break; case 3260: // Insufficient permission level to perform this action. // There's nothing we can do about this, if they don't grant the user permission, just skip it return true; case 3200: // The provided edit sequence is out-of-date. if (!($tmp = $Driver->get(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident)))) { return true; } switch ($config['conflicts']) { case QUICKBOOKS_SERVER_SQL_CONFLICT_LOG: $multipart = array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_NUMBER, $errnum); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, $errmsg); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $sqlObject, array($multipart)); break; case QUICKBOOKS_SERVER_SQL_CONFLICT_NEWER: $Parser = new QuickBooks_XML_Parser($xml); $errnumTemp = 0; $errmsgTemp = ''; $Doc = $Parser->parse($errnumTemp, $errmsgTemp); $Root = $Doc->getRoot(); $List = $Root->getChildAt('QBXML QBXMLMsgsRs ' . QuickBooks_Utilities::actionToResponse($action)); $TimeModified = $Root->getChildDataAt('QBXML QBXMLMsgsRs ' . QuickBooks_Utilities::actionToResponse($action) . ' ' . QuickBooks_Utilities::actionToXMLElement($action) . ' TimeModified'); $EditSequence = $Root->getChildDataAt('QBXML QBXMLMsgsRs ' . QuickBooks_Utilities::actionToResponse($action) . ' ' . QuickBooks_Utilities::actionToXMLElement($action) . ' EditSequence'); $multipart = array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident); if (QuickBooks_Utilities::compareQBTimeToSQLTime($TimeModified, $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) >= 0 && $config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { //@TODO: Make this get only a single item, not the whole table $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToQuery($action), __FILE__, true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); } else { if (QuickBooks_Utilities::compareQBTimeToSQLTime($TimeModified, $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) < 0) { //Updates the EditSequence without marking the row as resynced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set("EditSequence", $EditSequence); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array($multipart)); $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToMod($action), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_ID), true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); } else { //Trash it, set synced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, "Read/Write Mode is WRITEONLY, and Conflict Mode is NEWER, and Quickbooks has Newer data, so no Update Occured."); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array($multipart)); } } break; case QUICKBOOKS_SERVER_SQL_CONFLICT_QUICKBOOKS: if ($config['mode'] == QUICKBOOKS_SERVER_SQL_MODE_READWRITE) { //@TODO: Make this get only a single item, not the whole table $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToQuery($action), null, true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); $multipart = array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_NUMBER, $errnum); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, $errmsg); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $sqlObject, array($multipart)); //Use what's on quickbooks, and trash whatever is here. } else { $multipart = array(QUICKBOOKS_DRIVER_SQL_FIELD_ID => $ident); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_NUMBER, $errnum); $sqlObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, $errmsg); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $sqlObject, array($multipart)); // @TODO: Raise Notification that the conflicts level requires writing to SQL table, but Mode disallows this } break; case QUICKBOOKS_SERVER_SQL_CONFLICT_SQL: // Updates the EditSequence without marking the row as resynced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set("EditSequence", $EditSequence); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array($multipart)); $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToMod($action), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_ID), true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); break; case QUICKBOOKS_SERVER_SQL_CONFLICT_CALLBACK: break; default: break; } break; default: if (strstr($xml, 'statusSeverity="Info"') === false) { // } break; } // Please don't change this, it stops us from knowing what's actually // going wrong. If an error occurs, we should either catch it if it's // recoverable, or treated as a fatal error so we know about it and // can address it later. //return false; return true; }
/** * * * */ protected static function _QueryResponse($type, $List, $requestID, $user, $action, $ID, $extra, &$err, $last_action_time, $last_actionident_time, $xml, $idents, $callback_config = array()) { $type = strtolower($type); $Driver = QuickBooks_Driver_Singleton::getInstance(); $objects = array(); // For each one of the objects we got back in the qbXML response... foreach ($List->children() as $Node) { // If this object is a base-level object, we're going to keep track of it's TxnID or // ListID so that we can use it to tie child elements back to this base-level // element (about 20 or 30 lines below this is that code) // Convert the XML nodes to objects, based on the XML to SQL schema definitions in Schema.php $objects = array(); QuickBooks_Server_SQL_Callbacks::_transformToSQLObjects('', $Node, $objects); // For each object we created from the XML nodes... // (might have created more than one, e.g. an Invoice, plus 10 InvoiceLines, plus a Invoice_DataExt, etc.) /* if (count($objects) > 1) { print_r($objects); exit; } */ foreach ($objects as $key => $object) { $table = $object->table(); $path = $object->path(); $map = array(); QuickBooks_SQL_Schema::mapPrimaryKey($path, QUICKBOOKS_SQL_SCHEMA_MAP_TO_SQL, $map); // This is a list of "Boolean" fields in QuickBooks // QuickBooks sends us boolean value as the strings "true" and // "false", so we need to convert them to 1 and 0 so that we // can store this in an INT field in the database (not all // databases support a BOOLEAN type) $qb_to_sql_booleans = array('EmployeePayrollInfo_IsUsingTimeDataToCreatePaychecks', 'EmployeePayrollInfo_ClearEarnings', 'EmployeePayrollInfo_SickHours_IsResettingHoursEachNewYear', 'EmployeePayrollInfo_VacationHours_IsResettingHoursEachNewYear', 'IsAdjustment', 'IsActive', 'IsBillable', 'IsBilled', 'IsClosed', 'IsFinanceCharge', 'IsFullyInvoiced', 'IsFullyReceived', 'IsManuallyClosed', 'IsPaid', 'IsPending', 'IsPrintItemsInGroup', 'IsToBeEmailed', 'IsToBePrinted', 'IsSampleCompany', 'IsTaxable', 'IsVendorEligibleFor1099'); // Cast QuickBooks booleans (strings, "true" and "false") to database booleans (tinyint 1 and 0) foreach ($qb_to_sql_booleans as $qb_field_boolean) { $qb_bool = $object->get($qb_field_boolean, false); if ($qb_bool !== false) { if ($qb_bool == 'true') { $object->set($qb_field_boolean, 1); } else { $object->set($qb_field_boolean, 0); } } } // if (count($map) and $map[0] and $map[1]) { $addMapTest = array(); $addMapTestOthers = array(); QuickBooks_SQL_Schema::mapToSchema(trim(QuickBooks_Utilities::actionToXMLElement($action)), QUICKBOOKS_SQL_SCHEMA_MAP_TO_SQL, $addMapTest, $addMapTestOthers); if (!isset($extra['IsAddResponse']) or !(count($addMapTest) and $addMapTest[0]) or $map[0] != $addMapTest[0]) { //echo "<br /> ".$path."<br />"; //GARRETT'S bug Fix -- Arrays with primary keys consisting of multiple fields weren't updating properly //due to failure to check for arrays. $multipart = array(); if (is_array($map[1])) { foreach ($map[1] as $table_name) { $multipart[$table_name] = $object->get($table_name); } } else { $multipart[$map[1]] = $object->get($map[1]); } } else { $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID] = $ID; } $hooks = array(); if (isset($callback_config['hooks'])) { $hooks = $callback_config['hooks']; } if ($tmp = $Driver->get(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $multipart)) { $actually_do_update = false; $actually_do_updaterelatives = false; $actually_do_deletechildren = false; $extra['AddResponse_OldKey'] = $tmp->get(Quickbooks_Utilities::keyForAction($action)); if ($extra['AddResponse_OldKey'] == null and Quickbooks_Utilities::keyForAction($action) == "TxnID") { $extra['AddResponse_OldKey'] = $tmp->get("TxnLineID"); } if (isset($extra['IsAddResponse']) and stripos($action, 'dataext') === false) { // DATAEXT custom field handling // Handling data extensions here... $tmpSQLObject = new QuickBooks_SQL_Object("dataext", null); $tempMulti = array(); if (Quickbooks_Utilities::keyForAction($action) == "ListID") { $tmpSQLObject->set('Entity_ListID', $object->get("ListID")); $tempMulti["Entity_ListID"] = $extra['AddResponse_OldKey']; $wField = "Entity_ListID"; $wValue = $object->get("ListID"); } else { $tmpSQLObject->set('Txn_TxnID', $object->get("TxnID")); $tempMulti["Txn_TxnID"] = $extra['AddResponse_OldKey']; $wField = "Txn_TxnID"; $wValue = $object->get("TxnID"); if ($object->get("TxnID") == null) { $tmpSQLObject->set('Txn_TxnID', $object->get("TxnLineID")); $wValue = $object->get("TxnLineID"); } } $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . "dataext", $tmpSQLObject, array($tempMulti)); //mail("*****@*****.**", "DataExtStuffData", "Type: ".$table."\n\n\nTable: ".QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . "dataext\n\n\n".var_export($tmpSQLObject, true)."\n\n\n".var_export($tempMulti, true)); $sql = "\n\t\t\t\t\t\t\t\t\tSELECT \n\t\t\t\t\t\t\t\t\t\t" . QUICKBOOKS_DRIVER_SQL_FIELD_ID . "\n\t\t\t\t\t\t\t\t\tFROM \n\t\t\t\t\t\t\t\t\t\t" . QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . "dataext" . " \n\t\t\t\t\t\t\t\t\tWHERE \n\t\t\t\t\t\t\t\t\t\t " . $wField . " = '" . $Driver->escape($wValue) . "'"; $errnum = 0; $errmsg = ''; $tRes = $Driver->query($sql, $errnum, $errmsg); while ($tArr = $Driver->fetch($tRes)) { $Driver->queueEnqueue($user, QUICKBOOKS_ADD_DATAEXT, $tArr[QUICKBOOKS_DRIVER_SQL_FIELD_ID], true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, array()); } } else { if (isset($extra['IsModResponse']) and stripos($action, "dataext") === false) { // *MORE* DATAEXT custom field stuff if (Quickbooks_Utilities::keyForAction($action) == "ListID") { $wField = "Entity_ListID"; $wValue = $object->get("ListID"); } else { $wField = "Txn_TxnID"; $wValue = $object->get("TxnID"); if ($object->get("TxnID") == null) { $wValue = $object->get("TxnLineID"); } } $sql = "\n\t\t\t\t\t\t\t\t\tSELECT \n\t\t\t\t\t\t\t\t\t\t" . QUICKBOOKS_DRIVER_SQL_FIELD_ID . "\n\t\t\t\t\t\t\t\t\tFROM \n\t\t\t\t\t\t\t\t\t\t" . QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . "dataext" . " \n\t\t\t\t\t\t\t\t\tWHERE \n\t\t\t\t\t\t\t\t\t\t " . $wField . " = '" . $Driver->escape($wValue) . "'"; $errnum = 0; $errmsg = ''; $tRes = $Driver->query($sql, $errnum, $errmsg); while ($tArr = $Driver->fetch($tRes)) { $Driver->queueEnqueue($user, QUICKBOOKS_MOD_DATAEXT, $tArr[QUICKBOOKS_DRIVER_SQL_FIELD_ID], true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, array()); } } } // Make sure a conflict mode has been selected if (empty($callback_config['conflicts'])) { $callback_config['conflicts'] = null; } if (!isset($extra['IsAddResponse']) and $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY) > $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_RESYNC) and $callback_config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_READONLY) { // CONFLICT resolution code switch ($callback_config['conflicts']) { case QUICKBOOKS_SERVER_SQL_CONFLICT_NEWER: $msg = 'Conflict mode: ' . $callback_config['conflicts'] . ' is not supported right now.'; trigger_error($msg); die($msg); /* This code needs to be looked over and fixed: - Needs to use the $actually_do_update stuff - Needs to not depend on those deprecated QBTimeToSQLTime functions which are MySQL dependent - Needs to be documented if (QuickBooks_Utilities::compareQBTimeToSQLTime($object->get('TimeModified'), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) >= 0 and $callback_config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { //mail("*****@*****.**", "April 10TM", "QB Was Newer or Equal\n\n"."QB: ".$object->get('TimeModified')."\n\nSQL: ".$tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)); QuickBooks_Server_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array( $multipart )); //$actually_do_update = true; //$actually_do_deletechildren = true; } else if (QuickBooks_Utilities::compareQBTimeToSQLTime($object->get('TimeModified'), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) < 0) { //mail("*****@*****.**", "April 10TM", "SQL Was Newer\n\n"."QB: ".$object->get('TimeModified')."\n\nSQL: ".$tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)); //Updates the EditSequence without marking the row as resynced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set('EditSequence', $object->get('EditSequence')); //mail("*****@*****.**", "April10 ES", $object->name()); $tempmap = ''; $tempothers = array(); QuickBooks_SQL_Schema::mapToSchema($table . ".*", QUICKBOOKS_SQL_SCHEMA_MAP_TO_XML, $tempmap, $tempothers); $tempmap = str_replace(" *", "", $tempmap); $tempmap = str_replace("Ret", "Mod", $tempmap); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array( $multipart ), false); $Driver->queueEnqueue($user, $tempmap, $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_ID), true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); } else { //Trash it, set synced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, "Read/Write Mode is WRITEONLY, and Conflict Mode is NEWER, and Quickbooks has Newer data, so no Update Occured."); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array( $multipart )); } break;*/ /* This code needs to be looked over and fixed: - Needs to use the $actually_do_update stuff - Needs to not depend on those deprecated QBTimeToSQLTime functions which are MySQL dependent - Needs to be documented if (QuickBooks_Utilities::compareQBTimeToSQLTime($object->get('TimeModified'), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) >= 0 and $callback_config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { //mail("*****@*****.**", "April 10TM", "QB Was Newer or Equal\n\n"."QB: ".$object->get('TimeModified')."\n\nSQL: ".$tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)); QuickBooks_Server_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array( $multipart )); //$actually_do_update = true; //$actually_do_deletechildren = true; } else if (QuickBooks_Utilities::compareQBTimeToSQLTime($object->get('TimeModified'), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)) < 0) { //mail("*****@*****.**", "April 10TM", "SQL Was Newer\n\n"."QB: ".$object->get('TimeModified')."\n\nSQL: ".$tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_MODIFY)); //Updates the EditSequence without marking the row as resynced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set('EditSequence', $object->get('EditSequence')); //mail("*****@*****.**", "April10 ES", $object->name()); $tempmap = ''; $tempothers = array(); QuickBooks_SQL_Schema::mapToSchema($table . ".*", QUICKBOOKS_SQL_SCHEMA_MAP_TO_XML, $tempmap, $tempothers); $tempmap = str_replace(" *", "", $tempmap); $tempmap = str_replace("Ret", "Mod", $tempmap); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array( $multipart ), false); $Driver->queueEnqueue($user, $tempmap, $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_ID), true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); } else { //Trash it, set synced. $tmpSQLObject = new QuickBooks_SQL_Object($table, null); $tmpSQLObject->set(QUICKBOOKS_DRIVER_SQL_FIELD_ERROR_MESSAGE, "Read/Write Mode is WRITEONLY, and Conflict Mode is NEWER, and Quickbooks has Newer data, so no Update Occured."); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmpSQLObject, array( $multipart )); } break;*/ case QUICKBOOKS_SERVER_SQL_CONFLICT_QUICKBOOKS: // QuickBooks is master, so remove all existing child records of this record, then apply the QuickBooks version update $actually_do_deletechildren = true; $actually_do_update = true; //QuickBooks_Server_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); //$Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array( $multipart )); break; case QUICKBOOKS_SERVER_SQL_CONFLICT_CALLBACK: $msg = 'Conflict mode: ' . $callback_config['conflicts'] . ' is not supported right now.'; trigger_error($msg); die($msg); break; case QUICKBOOKS_SERVER_SQL_CONFLICT_SQL: // The SQL table is the master table, but we have an out-of-date EditSequence value // In this case, what we want to do is update our record to the latest EditSequence value, // and then re-queue the object so that it gets updated the next time the sync runs to // the values from the SQL record $tmp_editsequence_update = new QuickBooks_SQL_Object($table, null); $tmp_editsequence_update->set('EditSequence', $object->get('EditSequence')); // *Just* update the EditSequence value $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $tmp_editsequence_update, array($multipart), false); // Re-queue it so the conflict gets resolved $Driver->queueEnqueue($user, QuickBooks_Utilities::convertActionToMod($action), $tmp->get(QUICKBOOKS_DRIVER_SQL_FIELD_ID), true, QUICKBOOKS_SERVER_SQL_CONFLICT_QUEUE_PRIORITY, $extra); break; case QUICKBOOKS_SERVER_SQL_CONFLICT_LOG: default: if (isset($extra['IsModResponse'])) { // If it's actually a mod response, then this isn't actually a conflict, it's just the mod response happening normally $actually_do_update = true; $actually_do_deletechildren = true; $actually_do_updaterelatives = true; } else { // Log it...? $Driver->log('Conflict occured at: ' . QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, null, QUICKBOOKS_LOG_NORMAL); } break; } } else { if (isset($extra['IsAddResponse'])) { // If this is set, it means we just added something // to QuickBooks, and now we need to record the // ListID and EditSequence provided to us by QuickBooks $actually_do_update = true; $actually_do_updaterelatives = true; $actually_do_deletechildren = true; } } /*else if (!isset($callback_config['conflicts']) or $callback_config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { // No conflicts have occurred, let's update the record in the SQL table QuickBooks_Server_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); QuickBooks_Server_SQL_Callbacks::_UpdateRelatives($table, $user, $action, $ID, $object, $extra); $Driver->log('Applying UPDATE: ' . $table . ': ' . print_r($object) . ', where: ' . print_r($multipart), null, QUICKBOOKS_LOG_DEVELOP); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array( $multipart )); }*/ if ($callback_config['mode'] == QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { // In WRITE-ONLY mode, we only write changes to QuickBooks, but never read them back // (but should we update the EditSequence still?) $actually_do_update = false; $actually_do_deletechildren = false; $actually_do_updaterelatives = false; } if ($actually_do_deletechildren) { QuickBooks_Server_SQL_Callbacks::_DeleteChildren($table, $user, $action, $ID, $object, $extra); } if ($actually_do_updaterelatives) { QuickBooks_Server_SQL_Callbacks::_UpdateRelatives($table, $user, $action, $ID, $object, $extra); } if ($actually_do_update) { $Driver->log('Applying UPDATE: ' . $table . ': ' . print_r($object, true) . ', where: ' . print_r($multipart, true), null, QUICKBOOKS_LOG_DEVELOP); $Driver->update(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object, array($multipart)); $qbsql_id = null; if (!empty($multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID])) { $qbsql_id = $multipart[QUICKBOOKS_DRIVER_SQL_FIELD_ID]; } // Call any hooks that occur when a record is updated $hook_data = array('hook' => QUICKBOOKS_SQL_HOOK_SQL_UPDATE, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $qbsql_id, 'where' => array($multipart)); $err = null; Quickbooks_Server_SQL_Callbacks::_callHooks($hooks, QUICKBOOKS_SQL_HOOK_SQL_UPDATE, $requestID, $user, $err, $hook_data, $callback_config); } } else { // The record *DOES NOT exist in the current table, so just INSERT it if ($callback_config['mode'] != QUICKBOOKS_SERVER_SQL_MODE_WRITEONLY) { $Driver->log('Applying INSERT: ' . $table . ': ' . print_r($object, true), null, QUICKBOOKS_LOG_DEVELOP); $Driver->insert(QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, $object); $last = $Driver->last(); // Call any hooks that occur when a record is inserted $hook_data = array('hook' => QUICKBOOKS_SQL_HOOK_SQL_INSERT, 'table' => QUICKBOOKS_DRIVER_SQL_PREFIX_SQL . $table, 'object' => $object, 'data' => $object->asArray(), 'qbsql_id' => $last); $err = null; Quickbooks_Server_SQL_Callbacks::_callHooks($hooks, QUICKBOOKS_SQL_HOOK_SQL_INSERT, $requestID, $user, $err, $hook_data, $callback_config); } } } } //exit; } // Find out if we need to iterate further to get more results $matches = array(); $iterator_count = ereg('iteratorRemainingCount="([0-9]*)" iteratorID="([^"]*)"', $xml, $matches); // If an iterator was used and there's results remaining if ($iterator_count !== false and isset($matches[1]) and (int) $matches[1] > 0) { $extra = array('iteratorID' => $matches[2]); // Set the iteratorID to be used if ((int) $matches[1] < QUICKBOOKS_SERVER_SQL_ITERATOR_MAXRETURNED) { $extra['maxReturned'] = (int) $matches[1]; } // queueEnqueue($user, $action, $ident, $replace = true, $priority = 0, $extra = null, $qbxml = null) $Driver->queueEnqueue($user, $action, null, true, QUICKBOOKS_SERVER_SQL_ITERATOR_PRIORITY, $extra); // Queue up another go! } else { // We're done with this iterator! // When the current iterator started... $module = __CLASS__; $type = null; $opts = null; // configRead($user, $module, $key, &$type, &$opts) $curr_sync_datetime = $Driver->configRead($user, $module, QuickBooks_Server_SQL_Callbacks::_keySyncCurr($action), $type, $opts); // last sync started... // Start of the iteration, update the previous timestamp to NOW $Driver->configWrite($user, $module, QuickBooks_Server_SQL_Callbacks::_keySyncPrev($action), $curr_sync_datetime, null); } }