perf: filter by BC category before fetching instead of per-product lookups
When wbc_category_filter is set, sync now pages through BC's /items endpoint filtered server-side by itemCategoryCode (or-chained eq - BC's classic OData rejects the `in` operator) and matches each returned item to a WooCommerce product locally via wc_get_product_id_by_sku(), instead of querying BC once per WooCommerce product only to discard most of them. Confirmed against live data that BC's `in` filter isn't supported here but chained `eq ... or ...` is. Also reorders find_bc_item_by_sku() to try GTIN/number before Item Reference (ReferenciasArticulo). Empirically, on this catalog Reference_No is always identical to Item_No, so the reference lookup never matched anything the GTIN/number checks didn't already find - it was costing an extra BC round trip on every single product for no benefit. Kept as a last-resort fallback both directions (SKU->item and item->SKU) in case that doesn't hold for some future item. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,10 @@
|
|||||||
var run = response.data;
|
var run = response.data;
|
||||||
|
|
||||||
if (run.status === 'running') {
|
if (run.status === 'running') {
|
||||||
$status.text('Syncing... ' + (run.batches_done || 0) + ' / ' + (run.total_batches || 0) + ' batches (success: ' + (run.success || 0) + ', failed: ' + (run.failed || 0) + ', skipped: ' + (run.skipped || 0) + ')');
|
var progressText = (run.mode === 'category')
|
||||||
|
? (run.pages_done || 0) + ' page(s), ' + (run.total || 0) + ' item(s) checked'
|
||||||
|
: (run.batches_done || 0) + ' / ' + (run.total_batches || 0) + ' batches';
|
||||||
|
$status.text('Syncing... ' + progressText + ' (success: ' + (run.success || 0) + ', failed: ' + (run.failed || 0) + ', skipped: ' + (run.skipped || 0) + ')');
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
WBC_Admin.pollSyncStatus($btn, $status);
|
WBC_Admin.pollSyncStatus($btn, $status);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ class WBC_Batch_Sync {
|
|||||||
*/
|
*/
|
||||||
const BATCH_SIZE = 25;
|
const BATCH_SIZE = 25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of BC items fetched per page in category-filtered mode
|
||||||
|
*/
|
||||||
|
const CATEGORY_PAGE_SIZE = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option name storing the current/last run's progress
|
* Option name storing the current/last run's progress
|
||||||
*/
|
*/
|
||||||
@@ -38,28 +43,42 @@ class WBC_Batch_Sync {
|
|||||||
const LOCK_TRANSIENT = 'wbc_sync_lock';
|
const LOCK_TRANSIENT = 'wbc_sync_lock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action Scheduler hook processed per batch
|
* Action Scheduler hook processed per batch (WooCommerce-first mode)
|
||||||
*/
|
*/
|
||||||
const ACTION_HOOK = 'wbc_process_sync_batch';
|
const ACTION_HOOK = 'wbc_process_sync_batch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action Scheduler hook processed per page (category-filtered, BC-first mode)
|
||||||
|
*/
|
||||||
|
const CATEGORY_ACTION_HOOK = 'wbc_process_category_page';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action Scheduler group name
|
* Action Scheduler group name
|
||||||
*/
|
*/
|
||||||
const ACTION_GROUP = 'wbc_product_sync';
|
const ACTION_GROUP = 'wbc_product_sync';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the Action Scheduler batch handler
|
* Register the Action Scheduler batch handlers
|
||||||
*/
|
*/
|
||||||
public function register_hooks() {
|
public function register_hooks() {
|
||||||
add_action( self::ACTION_HOOK, array( $this, 'process_batch' ), 10, 2 );
|
add_action( self::ACTION_HOOK, array( $this, 'process_batch' ), 10, 2 );
|
||||||
|
add_action( self::CATEGORY_ACTION_HOOK, array( $this, 'process_category_page' ), 10, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kick off a full catalog sync in the background
|
* Kick off a full catalog sync in the background
|
||||||
*
|
*
|
||||||
* Gathers WooCommerce product IDs with a SKU (ID-only query - never
|
* If a category filter is configured, syncs BC-first: pages through
|
||||||
* loads full product objects for the whole catalog into memory), splits
|
* /items filtered server-side by itemCategoryCode and matches each item
|
||||||
* them into batches, and enqueues one background action per batch.
|
* to a WooCommerce product locally. This avoids querying BC once per
|
||||||
|
* WooCommerce product only to discard most of them - with a filter set,
|
||||||
|
* the vast majority of products aren't in an allowed category, so doing
|
||||||
|
* the filtering on BC's side first is far fewer API calls.
|
||||||
|
*
|
||||||
|
* Without a filter, falls back to the WooCommerce-first approach (ID-only
|
||||||
|
* query, batched, never loads the whole catalog as full objects), which
|
||||||
|
* doesn't have BC-side filtering available to narrow the scope.
|
||||||
|
*
|
||||||
* Returns immediately; actual work happens asynchronously.
|
* Returns immediately; actual work happens asynchronously.
|
||||||
*
|
*
|
||||||
* @return array Result with 'success' and 'message'.
|
* @return array Result with 'success' and 'message'.
|
||||||
@@ -99,6 +118,13 @@ class WBC_Batch_Sync {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$category_filter = get_option( 'wbc_category_filter', '' );
|
||||||
|
$categories = array_filter( array_map( 'trim', explode( ',', $category_filter ) ) );
|
||||||
|
|
||||||
|
if ( ! empty( $categories ) ) {
|
||||||
|
return self::queue_category_filtered_sync( array_values( $categories ) );
|
||||||
|
}
|
||||||
|
|
||||||
// ID-only query with a meta filter, so 60k+ catalogs don't get loaded
|
// ID-only query with a meta filter, so 60k+ catalogs don't get loaded
|
||||||
// as full WC_Product objects just to check for a SKU.
|
// as full WC_Product objects just to check for a SKU.
|
||||||
$product_ids = wc_get_products( array(
|
$product_ids = wc_get_products( array(
|
||||||
@@ -127,6 +153,7 @@ class WBC_Batch_Sync {
|
|||||||
|
|
||||||
update_option( self::RUN_OPTION, array(
|
update_option( self::RUN_OPTION, array(
|
||||||
'run_id' => $run_id,
|
'run_id' => $run_id,
|
||||||
|
'mode' => 'by_id',
|
||||||
'status' => 'running',
|
'status' => 'running',
|
||||||
'total' => count( $product_ids ),
|
'total' => count( $product_ids ),
|
||||||
'total_batches' => count( $batches ),
|
'total_batches' => count( $batches ),
|
||||||
@@ -166,6 +193,143 @@ class WBC_Batch_Sync {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kick off a BC-first, category-filtered sync
|
||||||
|
*
|
||||||
|
* Pages through BC's /items endpoint filtered server-side by
|
||||||
|
* itemCategoryCode (or-chained eq - BC's classic OData rejects the `in`
|
||||||
|
* operator here) and self-chains one background action per page, since
|
||||||
|
* the total item count isn't known upfront. Each page is matched against
|
||||||
|
* WooCommerce locally (no further BC calls in the common case).
|
||||||
|
*
|
||||||
|
* @param string[] $categories Allowed BC item category codes.
|
||||||
|
* @return array Result with 'success' and 'message'.
|
||||||
|
*/
|
||||||
|
private static function queue_category_filtered_sync( array $categories ) {
|
||||||
|
$run_id = uniqid( 'wbc_', true );
|
||||||
|
|
||||||
|
update_option( self::RUN_OPTION, array(
|
||||||
|
'run_id' => $run_id,
|
||||||
|
'mode' => 'category',
|
||||||
|
'status' => 'running',
|
||||||
|
'categories' => $categories,
|
||||||
|
'skip' => 0,
|
||||||
|
'pages_done' => 0,
|
||||||
|
'total' => 0,
|
||||||
|
'success' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'started_at' => current_time( 'mysql' ),
|
||||||
|
'finished_at' => '',
|
||||||
|
), false );
|
||||||
|
|
||||||
|
set_transient( self::LOCK_TRANSIENT, $run_id, 4 * HOUR_IN_SECONDS );
|
||||||
|
|
||||||
|
as_enqueue_async_action( self::CATEGORY_ACTION_HOOK, array( $run_id, 0 ), self::ACTION_GROUP );
|
||||||
|
|
||||||
|
WBC_Logger::info( 'BatchSync', 'Queued category-filtered background sync', array(
|
||||||
|
'run_id' => $run_id,
|
||||||
|
'categories' => $categories,
|
||||||
|
) );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => true,
|
||||||
|
'started' => true,
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: %s: comma-separated category codes */
|
||||||
|
__( 'Sync started in the background, filtered to categories: %s.', 'woo-business-central' ),
|
||||||
|
implode( ', ', $categories )
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process one page of BC items filtered by category (called by Action Scheduler)
|
||||||
|
*
|
||||||
|
* Self-chains: enqueues the next page if this one came back full, or marks
|
||||||
|
* the run complete otherwise. Positional args, not a combined array - see
|
||||||
|
* the note on process_batch().
|
||||||
|
*
|
||||||
|
* @param string $run_id Run ID this page belongs to.
|
||||||
|
* @param int $skip OData $skip offset for this page.
|
||||||
|
*/
|
||||||
|
public function process_category_page( $run_id, $skip = 0 ) {
|
||||||
|
$run = get_option( self::RUN_OPTION, array() );
|
||||||
|
|
||||||
|
if ( empty( $run ) || ( $run['run_id'] ?? '' ) !== $run_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$categories = $run['categories'] ?? array();
|
||||||
|
|
||||||
|
if ( empty( $categories ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filter_parts = array_map( function ( $code ) {
|
||||||
|
return "itemCategoryCode eq '" . WBC_API_Client::escape_odata_string( $code ) . "'";
|
||||||
|
}, $categories );
|
||||||
|
|
||||||
|
$result = WBC_API_Client::get( '/items', array(
|
||||||
|
'$filter' => '(' . implode( ' or ', $filter_parts ) . ')',
|
||||||
|
'$select' => WBC_Product_Sync::SELECT_FIELDS,
|
||||||
|
'$top' => self::CATEGORY_PAGE_SIZE,
|
||||||
|
'$skip' => $skip,
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
WBC_Logger::error( 'BatchSync', 'Failed to fetch category page from BC, stopping run', array(
|
||||||
|
'run_id' => $run_id,
|
||||||
|
'skip' => $skip,
|
||||||
|
'error' => $result->get_error_message(),
|
||||||
|
) );
|
||||||
|
|
||||||
|
$run['status'] = 'failed';
|
||||||
|
$run['finished_at'] = current_time( 'mysql' );
|
||||||
|
update_option( self::RUN_OPTION, $run, false );
|
||||||
|
delete_transient( self::LOCK_TRANSIENT );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = isset( $result['value'] ) ? $result['value'] : array();
|
||||||
|
|
||||||
|
$sync = new WBC_Product_Sync();
|
||||||
|
foreach ( $items as $item ) {
|
||||||
|
$sync->process_bc_item( $item );
|
||||||
|
usleep( 50000 ); // 50ms, matches the rate limiting used elsewhere
|
||||||
|
}
|
||||||
|
$page_results = $sync->get_results();
|
||||||
|
|
||||||
|
// Best-effort counters - see note on process_batch().
|
||||||
|
$run = get_option( self::RUN_OPTION, $run );
|
||||||
|
if ( ( $run['run_id'] ?? '' ) !== $run_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$run['pages_done'] = (int) ( $run['pages_done'] ?? 0 ) + 1;
|
||||||
|
$run['total'] = (int) ( $run['total'] ?? 0 ) + count( $items );
|
||||||
|
$run['success'] = (int) ( $run['success'] ?? 0 ) + $page_results['success'];
|
||||||
|
$run['failed'] = (int) ( $run['failed'] ?? 0 ) + $page_results['failed'];
|
||||||
|
$run['skipped'] = (int) ( $run['skipped'] ?? 0 ) + $page_results['skipped'];
|
||||||
|
|
||||||
|
$has_more_pages = count( $items ) === self::CATEGORY_PAGE_SIZE;
|
||||||
|
|
||||||
|
if ( $has_more_pages ) {
|
||||||
|
$next_skip = $skip + self::CATEGORY_PAGE_SIZE;
|
||||||
|
$run['skip'] = $next_skip;
|
||||||
|
update_option( self::RUN_OPTION, $run, false );
|
||||||
|
as_enqueue_async_action( self::CATEGORY_ACTION_HOOK, array( $run_id, $next_skip ), self::ACTION_GROUP );
|
||||||
|
} else {
|
||||||
|
$run['status'] = 'completed';
|
||||||
|
$run['finished_at'] = current_time( 'mysql' );
|
||||||
|
update_option( self::RUN_OPTION, $run, false );
|
||||||
|
WBC_Cron::update_last_sync_time();
|
||||||
|
delete_transient( self::LOCK_TRANSIENT );
|
||||||
|
|
||||||
|
WBC_Logger::info( 'BatchSync', 'Category-filtered background sync completed', $run );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a single batch (called by Action Scheduler)
|
* Process a single batch (called by Action Scheduler)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -143,7 +143,13 @@ class WBC_Product_Sync {
|
|||||||
/**
|
/**
|
||||||
* Find a BC item by WooCommerce SKU
|
* Find a BC item by WooCommerce SKU
|
||||||
*
|
*
|
||||||
* Tries, in order: Item Reference (ReferenciasArticulo), GTIN, then item number.
|
* Tries, in order: GTIN, item number, then Item Reference (ReferenciasArticulo).
|
||||||
|
* Item Reference is last on purpose: on this catalog's real data, Reference_No
|
||||||
|
* is always identical to Item_No for every sampled row, so it never matches
|
||||||
|
* anything the GTIN/number checks didn't already find - trying it first (as
|
||||||
|
* originally implemented) cost an extra BC round trip on every single product
|
||||||
|
* for zero benefit. Kept as a fallback in case that assumption doesn't hold
|
||||||
|
* for some future item.
|
||||||
*
|
*
|
||||||
* @param string $sku WooCommerce product SKU.
|
* @param string $sku WooCommerce product SKU.
|
||||||
* @return array|false|WP_Error BC item data, false if not found, or WP_Error.
|
* @return array|false|WP_Error BC item data, false if not found, or WP_Error.
|
||||||
@@ -151,30 +157,6 @@ class WBC_Product_Sync {
|
|||||||
private function find_bc_item_by_sku( $sku ) {
|
private function find_bc_item_by_sku( $sku ) {
|
||||||
$escaped_sku = WBC_API_Client::escape_odata_string( $sku );
|
$escaped_sku = WBC_API_Client::escape_odata_string( $sku );
|
||||||
|
|
||||||
// Try Item Reference first - lets merchants use a customer/vendor/bar-code
|
|
||||||
// reference number as the WooCommerce SKU instead of the raw item number/GTIN.
|
|
||||||
$item_number = $this->find_item_number_by_reference( $sku );
|
|
||||||
|
|
||||||
if ( is_wp_error( $item_number ) ) {
|
|
||||||
return $item_number;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $item_number ) {
|
|
||||||
$result = WBC_API_Client::get( '/items', array(
|
|
||||||
'$filter' => "number eq '" . WBC_API_Client::escape_odata_string( $item_number ) . "'",
|
|
||||||
'$select' => self::SELECT_FIELDS,
|
|
||||||
'$top' => 1,
|
|
||||||
) );
|
|
||||||
|
|
||||||
if ( is_wp_error( $result ) ) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $result['value'] ) ) {
|
|
||||||
return $result['value'][0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try by GTIN (since SKU = EAN = GTIN in this setup)
|
// Try by GTIN (since SKU = EAN = GTIN in this setup)
|
||||||
$result = WBC_API_Client::get( '/items', array(
|
$result = WBC_API_Client::get( '/items', array(
|
||||||
'$filter' => "gtin eq '" . $escaped_sku . "'",
|
'$filter' => "gtin eq '" . $escaped_sku . "'",
|
||||||
@@ -205,6 +187,29 @@ class WBC_Product_Sync {
|
|||||||
return $result['value'][0];
|
return $result['value'][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last resort: Item Reference
|
||||||
|
$item_number = $this->find_item_number_by_reference( $sku );
|
||||||
|
|
||||||
|
if ( is_wp_error( $item_number ) ) {
|
||||||
|
return $item_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $item_number ) {
|
||||||
|
$result = WBC_API_Client::get( '/items', array(
|
||||||
|
'$filter' => "number eq '" . WBC_API_Client::escape_odata_string( $item_number ) . "'",
|
||||||
|
'$select' => self::SELECT_FIELDS,
|
||||||
|
'$top' => 1,
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $result['value'] ) ) {
|
||||||
|
return $result['value'][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +270,118 @@ class WBC_Product_Sync {
|
|||||||
return in_array( $item_category, $allowed, true );
|
return in_array( $item_category, $allowed, true );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single BC item - find the matching WooCommerce product and sync
|
||||||
|
*
|
||||||
|
* Used by the category-filtered (BC-first) sync path: instead of iterating
|
||||||
|
* every WooCommerce product and asking BC "do you have this SKU?", we
|
||||||
|
* already fetched this item from BC (filtered server-side by category), so
|
||||||
|
* we just need to find the corresponding WooCommerce product locally - no
|
||||||
|
* further BC round trip needed in the common case.
|
||||||
|
*
|
||||||
|
* @param array $item BC item data (id, number, gtin, displayName, unitPrice, inventory, itemCategoryCode).
|
||||||
|
*/
|
||||||
|
public function process_bc_item( $item ) {
|
||||||
|
$this->results['total']++;
|
||||||
|
|
||||||
|
$product = $this->find_wc_product_for_item( $item );
|
||||||
|
|
||||||
|
if ( ! $product ) {
|
||||||
|
$this->results['skipped']++;
|
||||||
|
WBC_Logger::debug( 'ProductSync', 'No matching WooCommerce product for BC item', array(
|
||||||
|
'item_number' => $item['number'] ?? '',
|
||||||
|
'gtin' => $item['gtin'] ?? '',
|
||||||
|
) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If location code is configured, get location-specific stock
|
||||||
|
$location_code = get_option( 'wbc_location_code', '' );
|
||||||
|
if ( ! empty( $location_code ) && get_option( 'wbc_enable_stock_sync', 'yes' ) === 'yes' ) {
|
||||||
|
$location_stock = $this->get_location_stock( $item, $location_code );
|
||||||
|
if ( $location_stock !== false ) {
|
||||||
|
$item['inventory'] = $location_stock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $this->update_product( $product, $item );
|
||||||
|
|
||||||
|
if ( $updated ) {
|
||||||
|
$this->results['success']++;
|
||||||
|
} else {
|
||||||
|
$this->results['failed']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the WooCommerce product matching a BC item, via a local SKU lookup
|
||||||
|
* (no BC round trip) - tries GTIN, then item number, then Item Reference
|
||||||
|
* as a last resort.
|
||||||
|
*
|
||||||
|
* @param array $item BC item data.
|
||||||
|
* @return WC_Product|false
|
||||||
|
*/
|
||||||
|
private function find_wc_product_for_item( $item ) {
|
||||||
|
$gtin = $item['gtin'] ?? '';
|
||||||
|
$number = $item['number'] ?? '';
|
||||||
|
|
||||||
|
if ( ! empty( $gtin ) ) {
|
||||||
|
$product_id = wc_get_product_id_by_sku( $gtin );
|
||||||
|
if ( $product_id ) {
|
||||||
|
return wc_get_product( $product_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $number ) ) {
|
||||||
|
$product_id = wc_get_product_id_by_sku( $number );
|
||||||
|
if ( $product_id ) {
|
||||||
|
return wc_get_product( $product_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $number ) ) {
|
||||||
|
$reference_no = $this->find_reference_no_by_item_number( $number );
|
||||||
|
|
||||||
|
if ( $reference_no ) {
|
||||||
|
$product_id = wc_get_product_id_by_sku( $reference_no );
|
||||||
|
if ( $product_id ) {
|
||||||
|
return wc_get_product( $product_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a reference/barcode number for a BC item number via the custom
|
||||||
|
* ReferenciasArticulo OData endpoint (reverse direction of find_item_number_by_reference).
|
||||||
|
*
|
||||||
|
* @param string $item_number BC item number.
|
||||||
|
* @return string|false Reference number, or false if none found/on error.
|
||||||
|
*/
|
||||||
|
private function find_reference_no_by_item_number( $item_number ) {
|
||||||
|
$escaped = WBC_API_Client::escape_odata_string( $item_number );
|
||||||
|
|
||||||
|
$result = WBC_API_Client::odata_get( 'ReferenciasArticulo', array(
|
||||||
|
'$filter' => "Item_No eq '" . $escaped . "'",
|
||||||
|
'$select' => 'Item_No,Reference_No,Reference_Type',
|
||||||
|
'$top' => 1,
|
||||||
|
) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = isset( $result['value'] ) ? $result['value'] : array();
|
||||||
|
|
||||||
|
if ( empty( $entries ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries[0]['Reference_No'] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get stock quantity for a specific BC location using ItemByLocation endpoint
|
* Get stock quantity for a specific BC location using ItemByLocation endpoint
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user