ArcWOC/admin/services/class-hesabix-v2-stock-push-service.php
2026-08-02 16:11:07 +03:30

315 lines
8.9 KiB
PHP

<?php
/**
* ارسال عدد موجودی ووکامرس → حسابیکس از طریق حواله تعدیل انبار.
*
* فقط وقتی منبع حقیقت «ووکامرس» و گزینه push فعال باشد.
*
* @package Hesabix_V2
* @subpackage Hesabix_V2/admin/services
*/
if (!defined('WPINC')) {
die;
}
class Hesabix_V2_Stock_Push_Service
{
const APPLYING_FLAG = 'hesabix_v2_stock_pull_applying';
const META_LAST_PUSHED_QTY = '_hesabix_v2_last_pushed_stock_qty';
const ORIGIN_KEY = 'arcwoc_origin';
const ORIGIN_VALUE = 'wc_stock_push';
/**
* ثبت هوک‌های تغییر موجودی ووکامرس.
*/
public static function register_hooks()
{
add_action('woocommerce_product_set_stock', array(__CLASS__, 'on_product_set_stock'), 40, 1);
add_action('woocommerce_variation_set_stock', array(__CLASS__, 'on_product_set_stock'), 40, 1);
}
/**
* جلوگیری از حلقه وقتی کشش موجودی از حسابیکس در حال نوشتن روی WC است.
*
* @return bool
*/
public static function is_stock_pull_applying()
{
return !empty($GLOBALS[ self::APPLYING_FLAG ]);
}
/**
* @param bool $on
*/
public static function set_stock_pull_applying($on)
{
if ($on) {
$GLOBALS[ self::APPLYING_FLAG ] = true;
} else {
unset($GLOBALS[ self::APPLYING_FLAG ]);
}
}
/**
* @param WC_Product $product
*/
public static function on_product_set_stock($product)
{
if (!$product || !is_a($product, 'WC_Product')) {
return;
}
if (self::is_stock_pull_applying()) {
return;
}
if (!get_option('hesabix_v2_enabled')) {
return;
}
if (!Hesabix_V2_Inventory_Policy::should_push_wc_qty_to_hesabix()) {
return;
}
if ($product->is_type('variable') || $product->is_virtual()) {
return;
}
if (!$product->managing_stock()) {
return;
}
$wc_id = (int) $product->get_id();
if ($wc_id < 1) {
return;
}
// جلوگیری از دوباره‌کاری در همان درخواست.
static $done = array();
if (isset($done[ $wc_id ])) {
return;
}
$done[ $wc_id ] = true;
try {
self::push_product_quantity($product);
} catch (Exception $e) {
Hesabix_V2_Log_Service::error(
'WC→Hesabix stock push failed',
array(
'entity_type' => 'stock_push',
'entity_id' => $wc_id,
'error' => $e->getMessage(),
)
);
}
}
/**
* هم‌ترازی عدد موجودی یک محصول با حسابیکس (تعدیل انبار).
*
* @param WC_Product $product
* @return array{success:bool, message:string, diff?:float, hesabix_id?:int}
*/
public static function push_product_quantity($product)
{
$wc_id = (int) $product->get_id();
$wc_qty = (float) $product->get_stock_quantity();
if ($wc_qty < 0) {
$wc_qty = 0.0;
}
$db = new Hesabix_V2_DB_Service();
$parent_id = $product->is_type('variation') ? (int) $product->get_parent_id() : 0;
$mapping = $parent_id > 0
? $db->get_mapping('product', $wc_id, $parent_id)
: $db->get_mapping('product', $wc_id, null);
if (!$mapping || empty($mapping['hesabix_id'])) {
return array(
'success' => false,
'message' => __('محصول به حسابیکس متصل نیست.', 'hesabix-v2'),
);
}
$hesabix_id = (int) $mapping['hesabix_id'];
$warehouse_id = absint(get_option('hesabix_v2_default_warehouse_id', 0));
if ($warehouse_id < 1) {
return array(
'success' => false,
'message' => __('انبار پیش‌فرض برای ارسال موجودی مشخص نیست.', 'hesabix-v2'),
);
}
$last_pushed = $product->get_meta(self::META_LAST_PUSHED_QTY, true);
if ($last_pushed !== '' && $last_pushed !== null && abs((float) $last_pushed - $wc_qty) < 0.00001) {
return array(
'success' => true,
'message' => __('موجودی قبلاً هم‌تراز است.', 'hesabix-v2'),
'diff' => 0.0,
'hesabix_id' => $hesabix_id,
);
}
$api = new Hesabix_V2_Api();
$hx_qty = self::fetch_hesabix_quantity($api, $hesabix_id, $warehouse_id);
if (is_wp_error($hx_qty)) {
return array(
'success' => false,
'message' => $hx_qty->get_error_message(),
'hesabix_id' => $hesabix_id,
);
}
$diff = $wc_qty - (float) $hx_qty;
if (abs($diff) < 0.00001) {
$product->update_meta_data(self::META_LAST_PUSHED_QTY, $wc_qty);
$product->save_meta_data();
return array(
'success' => true,
'message' => __('موجودی حسابیکس با ووکامرس برابر است.', 'hesabix-v2'),
'diff' => 0.0,
'hesabix_id' => $hesabix_id,
);
}
$movement = $diff > 0 ? 'in' : 'out';
$qty = abs($diff);
$payload = array(
'doc_type' => 'adjustment',
'document_date' => function_exists('current_time') ? current_time('Y-m-d') : gmdate('Y-m-d'),
'warehouse_id_to' => $warehouse_id,
'warehouse_id_from' => $warehouse_id,
'description' => sprintf(
/* translators: 1: WC product id, 2: qty */
__('تعدیل موجودی از ووکامرس (محصول #%1$d → %2$s)', 'hesabix-v2'),
$wc_id,
wc_format_decimal($wc_qty, 4)
),
'extra_info' => array(
self::ORIGIN_KEY => self::ORIGIN_VALUE,
'wc_product_id' => $wc_id,
'wc_target_qty' => $wc_qty,
'hesabix_prev_qty' => (float) $hx_qty,
),
'lines' => array(
array(
'product_id' => $hesabix_id,
'quantity' => $qty,
'movement' => $movement,
'warehouse_id' => $warehouse_id,
),
),
);
$create = $api->create_warehouse_document($payload);
if (empty($create['success'])) {
$msg = isset($create['message']) ? (string) $create['message'] : __('ایجاد حواله تعدیل ناموفق بود.', 'hesabix-v2');
Hesabix_V2_Log_Service::error(
'WC→Hesabix stock push: create warehouse doc failed',
array(
'entity_type' => 'stock_push',
'entity_id' => $wc_id,
'hesabix_id' => $hesabix_id,
'request' => array('decoded' => $payload),
'response' => array('decoded' => $create),
)
);
return array('success' => false, 'message' => $msg, 'hesabix_id' => $hesabix_id, 'diff' => $diff);
}
$doc = isset($create['data']) && is_array($create['data']) ? $create['data'] : array();
$wh_id = isset($doc['id']) ? (int) $doc['id'] : 0;
if ($wh_id < 1) {
return array(
'success' => false,
'message' => __('شناسه حواله تعدیل دریافت نشد.', 'hesabix-v2'),
'hesabix_id' => $hesabix_id,
'diff' => $diff,
);
}
$post = $api->post_warehouse_document($wh_id);
if (empty($post['success'])) {
$msg = isset($post['message']) ? (string) $post['message'] : __('قطعی‌سازی حواله تعدیل ناموفق بود.', 'hesabix-v2');
Hesabix_V2_Log_Service::error(
'WC→Hesabix stock push: post warehouse doc failed',
array(
'entity_type' => 'stock_push',
'entity_id' => $wc_id,
'hesabix_id' => $hesabix_id,
'warehouse_doc_id' => $wh_id,
'response' => array('decoded' => $post),
)
);
return array('success' => false, 'message' => $msg, 'hesabix_id' => $hesabix_id, 'diff' => $diff);
}
$product->update_meta_data(self::META_LAST_PUSHED_QTY, $wc_qty);
$product->save_meta_data();
Hesabix_V2_Log_Service::info(
'WC→Hesabix stock push completed',
array(
'entity_type' => 'stock_push',
'entity_id' => $wc_id,
'hesabix_id' => $hesabix_id,
'diff' => $diff,
'movement' => $movement,
'warehouse_doc_id' => $wh_id,
'wc_qty' => $wc_qty,
'hesabix_prev_qty' => (float) $hx_qty,
)
);
return array(
'success' => true,
/* translators: 1: diff, 2: warehouse doc id */
'message' => sprintf(__('موجودی حسابیکس تعدیل شد (اختلاف %1$s، حواله #%2$d).', 'hesabix-v2'), wc_format_decimal($diff, 4), $wh_id),
'diff' => $diff,
'hesabix_id' => $hesabix_id,
'warehouse_doc_id' => $wh_id,
);
}
/**
* @param Hesabix_V2_Api $api
* @param int $hesabix_product_id
* @param int $warehouse_id
* @return float|WP_Error
*/
private static function fetch_hesabix_quantity($api, $hesabix_product_id, $warehouse_id)
{
$res = $api->inventory_stock_report(
array(
'product_ids' => array((int) $hesabix_product_id),
'warehouse_ids' => array((int) $warehouse_id),
'track_inventory' => true,
'include_zero' => true,
'skip' => 0,
'take' => 50,
),
60
);
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();
$total = 0.0;
$found = false;
foreach ($items as $it) {
if (!is_array($it)) {
continue;
}
if ((int) ($it['product_id'] ?? 0) !== (int) $hesabix_product_id) {
continue;
}
$found = true;
$total += isset($it['quantity']) ? (float) $it['quantity'] : 0.0;
}
return $found ? $total : 0.0;
}
}