Show Hesabix invoice profit on ArcWOC orders list and order meta box, refresh after sync, and surface WooCommerce marketplace license upsell (hidden when active). Also includes 4.8.2 bank/cash take≤100 fixes.
683 lines
19 KiB
PHP
Executable file
683 lines
19 KiB
PHP
Executable file
<?php
|
||
/**
|
||
* کشش موجودی عددی حسابیکس → ووکامرس (چند انبار، جمع روی انبارهای انتخابشده).
|
||
*
|
||
* @package Hesabix_V2
|
||
* @subpackage Hesabix_V2/admin/services
|
||
*/
|
||
|
||
if (!defined('WPINC')) {
|
||
die;
|
||
}
|
||
|
||
class Hesabix_V2_Stock_Pull_Service
|
||
{
|
||
const CRON_HOOK = 'hesabix_v2_pull_stock_cron';
|
||
|
||
const LOCK_TRANSIENT = 'hesabix_v2_stock_pull_running';
|
||
|
||
const OPTION_KEY = 'hesabix_v2_stock_pull';
|
||
|
||
const LAST_RESULT_KEY = 'hesabix_v2_stock_pull_last';
|
||
|
||
const SCHEDULE_KEY = 'hesabix_v2_pull_stock_ivl';
|
||
|
||
/** @var int تعداد کالا در هر درخواست گزارش (محدودیت ~۵۰۰ سطر = کالا×انبار) */
|
||
const PRODUCT_BATCH = 40;
|
||
|
||
/**
|
||
* گزینههای ذخیرهشده با پیشفرض امن.
|
||
*
|
||
* @return array<string, mixed>
|
||
*/
|
||
public static function get_options()
|
||
{
|
||
$defaults = array(
|
||
'enabled' => false,
|
||
'warehouse_scope' => 'default',
|
||
'warehouse_ids' => array(),
|
||
'cron_minutes' => 15,
|
||
'force_manage_stock' => true,
|
||
// وقتی فعال است، موجودی با سفارش در ووکامرس کم نمیشود (مرجع: حسابیکس / کشش موجودی).
|
||
'disable_wc_stock_reduction' => false,
|
||
// اگر حسابیکس ۰ گزارش کند ولی WC > 0 باشد، موجودی فروشگاه را صفر نکن (مگر force_zero).
|
||
'skip_zero_overwrite' => true,
|
||
);
|
||
|
||
$raw = get_option(self::OPTION_KEY, array());
|
||
if (!is_array($raw)) {
|
||
$raw = array();
|
||
}
|
||
|
||
$o = wp_parse_args($raw, $defaults);
|
||
|
||
$o['enabled'] = !empty($o['enabled']);
|
||
$scope = isset($o['warehouse_scope']) ? (string) $o['warehouse_scope'] : 'default';
|
||
if (!in_array($scope, array('default', 'selected', 'all'), true)) {
|
||
$scope = 'default';
|
||
}
|
||
$o['warehouse_scope'] = $scope;
|
||
|
||
$ids = isset($o['warehouse_ids']) && is_array($o['warehouse_ids']) ? $o['warehouse_ids'] : array();
|
||
$clean_ids = array();
|
||
foreach ($ids as $wid) {
|
||
$i = absint($wid);
|
||
if ($i > 0) {
|
||
$clean_ids[] = $i;
|
||
}
|
||
}
|
||
$o['warehouse_ids'] = array_values(array_unique($clean_ids));
|
||
|
||
$o['cron_minutes'] = max(5, min(180, absint($o['cron_minutes'])));
|
||
$o['force_manage_stock'] = !empty($o['force_manage_stock']);
|
||
$o['disable_wc_stock_reduction'] = !empty($o['disable_wc_stock_reduction']);
|
||
$o['skip_zero_overwrite'] = !isset($o['skip_zero_overwrite']) || !empty($o['skip_zero_overwrite']);
|
||
|
||
return $o;
|
||
}
|
||
|
||
/**
|
||
* ثبت بازهٔ cron و اکشن زمانبندی.
|
||
*/
|
||
public static function register_hooks()
|
||
{
|
||
add_filter('cron_schedules', array(__CLASS__, 'filter_cron_schedules'));
|
||
add_action(self::CRON_HOOK, array(__CLASS__, 'run_scheduled_pull'));
|
||
add_action('init', array(__CLASS__, 'maybe_ensure_cron_scheduled'), 30);
|
||
}
|
||
|
||
/**
|
||
* اگر کشش زمانبندیشده فعال است ولی رویداد cron حذف شده، دوباره زمانبندی کن.
|
||
*/
|
||
public static function maybe_ensure_cron_scheduled()
|
||
{
|
||
if (defined('WP_INSTALLING') && WP_INSTALLING) {
|
||
return;
|
||
}
|
||
if (!get_option('hesabix_v2_enabled')) {
|
||
return;
|
||
}
|
||
$o = self::get_options();
|
||
if (empty($o['enabled'])) {
|
||
return;
|
||
}
|
||
if (wp_next_scheduled(self::CRON_HOOK)) {
|
||
return;
|
||
}
|
||
wp_schedule_event(time() + 120, self::SCHEDULE_KEY, self::CRON_HOOK);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $schedules
|
||
* @return array<string, mixed>
|
||
*/
|
||
public static function filter_cron_schedules($schedules)
|
||
{
|
||
if (!is_array($schedules)) {
|
||
$schedules = array();
|
||
}
|
||
$opts = self::get_options();
|
||
$m = isset($opts['cron_minutes']) ? (int) $opts['cron_minutes'] : 15;
|
||
|
||
$schedules[self::SCHEDULE_KEY] = array(
|
||
'interval' => max(300, min(64800, $m * 60)),
|
||
/* translators: %d: interval in minutes */
|
||
'display' => sprintf(__('موجودی حسابیکس → ووک (%d دقیقه)', 'hesabix-v2'), $m),
|
||
);
|
||
|
||
return $schedules;
|
||
}
|
||
|
||
/**
|
||
* حذف و در صورت فعال بودن زمانبندی مجدد.
|
||
*/
|
||
public static function reschedule_cron()
|
||
{
|
||
wp_clear_scheduled_hook(self::CRON_HOOK);
|
||
|
||
if (!get_option('hesabix_v2_enabled')) {
|
||
return;
|
||
}
|
||
|
||
$o = self::get_options();
|
||
if (empty($o['enabled'])) {
|
||
return;
|
||
}
|
||
|
||
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||
wp_schedule_event(time() + 90, self::SCHEDULE_KEY, self::CRON_HOOK);
|
||
}
|
||
}
|
||
|
||
public static function run_scheduled_pull()
|
||
{
|
||
$res = self::execute_pull(array('source' => 'cron'));
|
||
|
||
if (get_option('hesabix_v2_debug_mode')) {
|
||
Hesabix_V2_Log_Service::debug(
|
||
'Scheduled stock pull',
|
||
array(
|
||
'entity_type' => 'stock_pull',
|
||
'request' => array('decoded' => $res),
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* اجرای اصلی کشش موجودی.
|
||
*
|
||
* @param array{
|
||
* source?:string,
|
||
* product_ids?:int[],
|
||
* force_zero?:bool,
|
||
* ignore_source_policy?:bool
|
||
* } $ctx
|
||
* @return array{success:bool, message:string, updated?:int, skipped?:int, errors?:int, guarded_zero?:int, execution_time?:float}
|
||
*/
|
||
public static function execute_pull($ctx = array())
|
||
{
|
||
$start = microtime(true);
|
||
$source = isset($ctx['source']) ? (string) $ctx['source'] : 'manual';
|
||
// ajax و hesabix_push مثل manual عمل میکنند تا «اجرا هماکنون» و پوش از حسابیکس بدون Cron ممکن باشد.
|
||
$is_manual_like = in_array($source, array('manual', 'ajax', 'hesabix_push'), true);
|
||
|
||
if (!class_exists('WooCommerce')) {
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('ووکامرس فعال نیست.', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
|
||
if (!get_option('hesabix_v2_enabled')) {
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('افزونه حسابیکس فعال نشده است.', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
|
||
$opts = self::get_options();
|
||
if (empty($opts['enabled']) && !$is_manual_like) {
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('کشش موجودی غیرفعال است.', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
|
||
if ($source === 'hesabix_push' && !Hesabix_V2_Inventory_Policy::accept_remote_stock_push()) {
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('پوش موجودی از حسابیکس در سیاست فروشگاه پذیرفته نمیشود (منبع حقیقت یا گزینهٔ مربوطه).', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
|
||
if (
|
||
empty($ctx['ignore_source_policy'])
|
||
&& !Hesabix_V2_Inventory_Policy::should_apply_hesabix_qty_to_wc()
|
||
&& $source !== 'manual'
|
||
&& $source !== 'ajax'
|
||
) {
|
||
// Cron و push خودکار فقط وقتی منبع حقیقت حسابیکس است؛ دستی از ادمین مجاز میماند.
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('منبع حقیقت موجودی حسابیکس نیست؛ کشش خودکار اعمال نشد.', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
|
||
if (get_transient(self::LOCK_TRANSIENT)) {
|
||
return array(
|
||
'success' => false,
|
||
'message' => __('یک عملیات کشش موجودی در حال اجراست؛ لحظاتی بعد دوباره تلاش کنید.', 'hesabix-v2'),
|
||
'execution_time' => microtime(true) - $start,
|
||
);
|
||
}
|
||
set_transient(self::LOCK_TRANSIENT, 1, 15 * MINUTE_IN_SECONDS);
|
||
|
||
$force_zero = !empty($ctx['force_zero']);
|
||
$filter_hids = array();
|
||
if (!empty($ctx['product_ids']) && is_array($ctx['product_ids'])) {
|
||
foreach ($ctx['product_ids'] as $pid) {
|
||
$i = absint($pid);
|
||
if ($i > 0) {
|
||
$filter_hids[] = $i;
|
||
}
|
||
}
|
||
$filter_hids = array_values(array_unique($filter_hids));
|
||
}
|
||
|
||
try {
|
||
$wh_filter = self::resolve_warehouse_ids_for_api($opts);
|
||
if (is_wp_error($wh_filter)) {
|
||
return self::finish_result(
|
||
array(
|
||
'success' => false,
|
||
'message' => $wh_filter->get_error_message(),
|
||
'execution_time' => microtime(true) - $start,
|
||
),
|
||
$source
|
||
);
|
||
}
|
||
|
||
$db = new Hesabix_V2_DB_Service();
|
||
$mappings = $db->get_all_product_mappings();
|
||
if (empty($mappings)) {
|
||
return self::finish_result(
|
||
array(
|
||
'success' => true,
|
||
'message' => __('نگاشت محصولی برای بهروزرسانی موجودی وجود ندارد.', 'hesabix-v2'),
|
||
'updated' => 0,
|
||
'skipped' => 0,
|
||
'errors' => 0,
|
||
'guarded_zero' => 0,
|
||
'execution_time' => microtime(true) - $start,
|
||
),
|
||
$source
|
||
);
|
||
}
|
||
|
||
if (!empty($filter_hids)) {
|
||
$mappings = array_values(
|
||
array_filter(
|
||
$mappings,
|
||
function ($row) use ($filter_hids) {
|
||
$hid = isset($row['hesabix_id']) ? (int) $row['hesabix_id'] : 0;
|
||
return $hid > 0 && in_array($hid, $filter_hids, true);
|
||
}
|
||
)
|
||
);
|
||
if (empty($mappings)) {
|
||
return self::finish_result(
|
||
array(
|
||
'success' => true,
|
||
'message' => __('هیچ نگاشتی برای شناسههای درخواستی یافت نشد.', 'hesabix-v2'),
|
||
'updated' => 0,
|
||
'skipped' => 0,
|
||
'errors' => 0,
|
||
'guarded_zero' => 0,
|
||
'execution_time' => microtime(true) - $start,
|
||
),
|
||
$source
|
||
);
|
||
}
|
||
}
|
||
|
||
$hesabix_ids = array();
|
||
foreach ($mappings as $row) {
|
||
$hid = isset($row['hesabix_id']) ? (int) $row['hesabix_id'] : 0;
|
||
if ($hid > 0) {
|
||
$hesabix_ids[] = $hid;
|
||
}
|
||
}
|
||
$hesabix_ids = array_values(array_unique($hesabix_ids));
|
||
|
||
$api = new Hesabix_V2_Api();
|
||
$totals = self::fetch_quantities_for_product_ids($api, $hesabix_ids, $wh_filter);
|
||
|
||
if (is_wp_error($totals)) {
|
||
return self::finish_result(
|
||
array(
|
||
'success' => false,
|
||
'message' => $totals->get_error_message(),
|
||
'execution_time' => microtime(true) - $start,
|
||
),
|
||
$source
|
||
);
|
||
}
|
||
|
||
$updated = 0;
|
||
$skipped = 0;
|
||
$errors = 0;
|
||
$guarded_zero = 0;
|
||
|
||
$sync_pull = Hesabix_V2_Invoice_Helper::normalize_sync_settings(get_option('hesabix_v2_sync_settings', array()));
|
||
$pinv = isset($sync_pull['track_inventory_policy']) ? sanitize_key((string) $sync_pull['track_inventory_policy']) : 'wc';
|
||
$policy_auto_manage_stock = !empty($sync_pull['sync_product_stock'])
|
||
&& in_array($pinv, array('physical_always', 'always_on'), true);
|
||
|
||
Hesabix_V2_Stock_Push_Service::set_stock_pull_applying(true);
|
||
|
||
try {
|
||
foreach ($mappings as $row) {
|
||
$hid = isset($row['hesabix_id']) ? (int) $row['hesabix_id'] : 0;
|
||
$wc_id = isset($row['wc_id']) ? (int) $row['wc_id'] : 0;
|
||
|
||
if ($hid < 1 || $wc_id < 1) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
if (!array_key_exists($hid, $totals)) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$qty = (float) $totals[ $hid ];
|
||
|
||
$product = wc_get_product($wc_id);
|
||
if (!$product) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
if ($product->is_virtual()) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
if ($product->is_type('variable')) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
if (!apply_filters('hesabix_v2_stock_pull_apply_to_product', true, $product, $row, $qty, $opts)) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$current_qty = $product->managing_stock() ? (float) $product->get_stock_quantity() : null;
|
||
|
||
if (
|
||
!$force_zero
|
||
&& !empty($opts['skip_zero_overwrite'])
|
||
&& abs($qty) < 0.00001
|
||
&& $current_qty !== null
|
||
&& $current_qty > 0.00001
|
||
) {
|
||
$guarded_zero++;
|
||
Hesabix_V2_Log_Service::warning(
|
||
'Stock pull skipped zero overwrite (WC has positive qty)',
|
||
array(
|
||
'entity_type' => 'stock_pull',
|
||
'entity_id' => $wc_id,
|
||
'hesabix_id' => $hid,
|
||
'wc_qty' => $current_qty,
|
||
'hesabix_qty' => $qty,
|
||
)
|
||
);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
if (!empty($opts['force_manage_stock']) || $policy_auto_manage_stock) {
|
||
$product->set_manage_stock(true);
|
||
} elseif (!$product->managing_stock()) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$product->set_stock_quantity($qty);
|
||
$product->save();
|
||
|
||
$updated++;
|
||
} catch (Exception $e) {
|
||
$errors++;
|
||
Hesabix_V2_Log_Service::error(
|
||
'Stock pull: failed to update WooCommerce product',
|
||
array(
|
||
'entity_type' => 'stock_pull',
|
||
'entity_id' => $wc_id,
|
||
'hesabix_id' => $hid,
|
||
'error' => $e->getMessage(),
|
||
)
|
||
);
|
||
}
|
||
}
|
||
} finally {
|
||
Hesabix_V2_Stock_Push_Service::set_stock_pull_applying(false);
|
||
}
|
||
|
||
$elapsed = microtime(true) - $start;
|
||
|
||
Hesabix_V2_Log_Service::info(
|
||
'Stock pull from Hesabix completed',
|
||
array(
|
||
'entity_type' => 'stock_pull',
|
||
'source' => $source,
|
||
'updated' => $updated,
|
||
'skipped' => $skipped,
|
||
'errors' => $errors,
|
||
'guarded_zero' => $guarded_zero,
|
||
'execution_time' => $elapsed,
|
||
'warehouse_scope' => $opts['warehouse_scope'],
|
||
)
|
||
);
|
||
|
||
return self::finish_result(
|
||
array(
|
||
'success' => true,
|
||
/* translators: 1: updated, 2: skipped, 3: errors, 4: guarded zeros */
|
||
'message' => sprintf(
|
||
__('موجودی بهروز شد: %1$d مورد، رد شد %2$d، خطا %3$d، محافظ صفر %4$d', 'hesabix-v2'),
|
||
$updated,
|
||
$skipped,
|
||
$errors,
|
||
$guarded_zero
|
||
),
|
||
'updated' => $updated,
|
||
'skipped' => $skipped,
|
||
'errors' => $errors,
|
||
'guarded_zero' => $guarded_zero,
|
||
'execution_time' => $elapsed,
|
||
),
|
||
$source
|
||
);
|
||
} finally {
|
||
delete_transient(self::LOCK_TRANSIENT);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* نمونه اختلاف موجودی WC و حسابیکس برای عیبیابی.
|
||
*
|
||
* @param int $limit
|
||
* @return array{success:bool, message?:string, items?:array, checked?:int}
|
||
*/
|
||
public static function detect_conflicts($limit = 25)
|
||
{
|
||
$limit = max(5, min(100, absint($limit)));
|
||
if (!class_exists('WooCommerce') || !get_option('hesabix_v2_enabled')) {
|
||
return array('success' => false, 'message' => __('افزونه یا ووکامرس آماده نیست.', 'hesabix-v2'));
|
||
}
|
||
|
||
$opts = self::get_options();
|
||
$wh_filter = self::resolve_warehouse_ids_for_api($opts);
|
||
if (is_wp_error($wh_filter)) {
|
||
return array('success' => false, 'message' => $wh_filter->get_error_message());
|
||
}
|
||
|
||
$db = new Hesabix_V2_DB_Service();
|
||
$mappings = $db->get_all_product_mappings();
|
||
$hesabix_ids = array();
|
||
foreach ($mappings as $row) {
|
||
$hid = isset($row['hesabix_id']) ? (int) $row['hesabix_id'] : 0;
|
||
if ($hid > 0) {
|
||
$hesabix_ids[] = $hid;
|
||
}
|
||
}
|
||
$hesabix_ids = array_values(array_unique($hesabix_ids));
|
||
if (empty($hesabix_ids)) {
|
||
return array('success' => true, 'items' => array(), 'checked' => 0);
|
||
}
|
||
|
||
$api = new Hesabix_V2_Api();
|
||
$totals = self::fetch_quantities_for_product_ids($api, $hesabix_ids, $wh_filter);
|
||
if (is_wp_error($totals)) {
|
||
return array('success' => false, 'message' => $totals->get_error_message());
|
||
}
|
||
|
||
$items = array();
|
||
$checked = 0;
|
||
foreach ($mappings as $row) {
|
||
if (count($items) >= $limit) {
|
||
break;
|
||
}
|
||
$hid = isset($row['hesabix_id']) ? (int) $row['hesabix_id'] : 0;
|
||
$wc_id = isset($row['wc_id']) ? (int) $row['wc_id'] : 0;
|
||
if ($hid < 1 || $wc_id < 1 || !array_key_exists($hid, $totals)) {
|
||
continue;
|
||
}
|
||
$product = wc_get_product($wc_id);
|
||
if (!$product || $product->is_type('variable') || $product->is_virtual() || !$product->managing_stock()) {
|
||
continue;
|
||
}
|
||
$checked++;
|
||
$wc_qty = (float) $product->get_stock_quantity();
|
||
$hx_qty = (float) $totals[ $hid ];
|
||
if (abs($wc_qty - $hx_qty) < 0.00001) {
|
||
continue;
|
||
}
|
||
$items[] = array(
|
||
'wc_id' => $wc_id,
|
||
'hesabix_id' => $hid,
|
||
'name' => $product->get_name(),
|
||
'wc_qty' => $wc_qty,
|
||
'hesabix_qty' => $hx_qty,
|
||
'diff' => $wc_qty - $hx_qty,
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'success' => true,
|
||
'items' => $items,
|
||
'checked' => $checked,
|
||
'conflict_count' => count($items),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $result
|
||
* @param string $source
|
||
* @return array<string, mixed>
|
||
*/
|
||
private static function finish_result(array $result, $source)
|
||
{
|
||
$store = array(
|
||
'at' => gmdate('c'),
|
||
'source' => (string) $source,
|
||
'success' => !empty($result['success']),
|
||
'message' => isset($result['message']) ? (string) $result['message'] : '',
|
||
'updated' => isset($result['updated']) ? (int) $result['updated'] : null,
|
||
'skipped' => isset($result['skipped']) ? (int) $result['skipped'] : null,
|
||
'errors' => isset($result['errors']) ? (int) $result['errors'] : null,
|
||
'guarded_zero' => isset($result['guarded_zero']) ? (int) $result['guarded_zero'] : null,
|
||
);
|
||
update_option(self::LAST_RESULT_KEY, $store, false);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $opts
|
||
* @return array<int>|null|WP_Error
|
||
*/
|
||
private static function resolve_warehouse_ids_for_api($opts)
|
||
{
|
||
$scope = isset($opts['warehouse_scope']) ? $opts['warehouse_scope'] : 'default';
|
||
|
||
if ($scope === 'all') {
|
||
return null;
|
||
}
|
||
|
||
if ($scope === 'selected') {
|
||
if (empty($opts['warehouse_ids']) || !is_array($opts['warehouse_ids'])) {
|
||
return new WP_Error(
|
||
'hesabix_stock_wh',
|
||
__('برای حالت «انبارهای انتخابی» حداقل یک انبار را مشخص کنید.', 'hesabix-v2')
|
||
);
|
||
}
|
||
|
||
return array_map('intval', $opts['warehouse_ids']);
|
||
}
|
||
|
||
$def = get_option('hesabix_v2_default_warehouse_id', '');
|
||
$wid = ($def !== '' && $def !== null) ? absint($def) : 0;
|
||
if ($wid < 1) {
|
||
return new WP_Error(
|
||
'hesabix_stock_wh',
|
||
__('انبار پیشفرض در تنظیمات فاکتور مشخص نیست.', 'hesabix-v2')
|
||
);
|
||
}
|
||
|
||
return array($wid);
|
||
}
|
||
|
||
/**
|
||
* @param Hesabix_V2_Api $api
|
||
* @param array<int> $hesabix_product_ids
|
||
* @param array<int>|null $warehouse_ids null = همه انبارها
|
||
* @return array<int, float>|WP_Error
|
||
*/
|
||
private static function fetch_quantities_for_product_ids($api, $hesabix_product_ids, $warehouse_ids)
|
||
{
|
||
$totals = array();
|
||
$batches = array_chunk($hesabix_product_ids, self::PRODUCT_BATCH);
|
||
|
||
foreach ($batches as $batch) {
|
||
if (empty($batch)) {
|
||
continue;
|
||
}
|
||
|
||
$skip = 0;
|
||
$take = 500;
|
||
$guard = 0;
|
||
|
||
do {
|
||
$body = array(
|
||
'product_ids' => array_map('intval', $batch),
|
||
'track_inventory' => true,
|
||
'include_zero' => true,
|
||
'skip' => $skip,
|
||
'take' => $take,
|
||
);
|
||
|
||
if ($warehouse_ids !== null) {
|
||
$body['warehouse_ids'] = array_map('intval', $warehouse_ids);
|
||
}
|
||
|
||
$body = apply_filters('hesabix_v2_inventory_stock_report_body', $body, $batch, $warehouse_ids);
|
||
|
||
$res = $api->inventory_stock_report($body, 90);
|
||
|
||
if (empty($res['success'])) {
|
||
$msg = isset($res['message']) ? (string) $res['message'] : __('خطا در گزارش موجودی حسابیکس', 'hesabix-v2');
|
||
return new WP_Error('hesabix_stock_api', $msg);
|
||
}
|
||
|
||
$data = isset($res['data']) && is_array($res['data']) ? $res['data'] : array();
|
||
$items = isset($data['items']) && is_array($data['items']) ? $data['items'] : array();
|
||
|
||
foreach ($items as $it) {
|
||
if (!is_array($it)) {
|
||
continue;
|
||
}
|
||
$pid = isset($it['product_id']) ? (int) $it['product_id'] : 0;
|
||
if ($pid < 1) {
|
||
continue;
|
||
}
|
||
$q = isset($it['quantity']) ? (float) $it['quantity'] : 0.0;
|
||
if (!isset($totals[ $pid ])) {
|
||
$totals[ $pid ] = 0.0;
|
||
}
|
||
$totals[ $pid ] += $q;
|
||
}
|
||
|
||
$n = count($items);
|
||
$has_next = false;
|
||
if (!empty($data['pagination']) && is_array($data['pagination'])) {
|
||
$has_next = !empty($data['pagination']['has_next']);
|
||
} else {
|
||
$has_next = ($n >= $take);
|
||
}
|
||
|
||
$skip += $take;
|
||
$guard++;
|
||
} while ($has_next && $guard < 80);
|
||
}
|
||
|
||
return $totals;
|
||
}
|
||
}
|