get_results("SELECT ID, post_parent FROM {$wpdb->posts} WHERE ID IN ({$in})") as $row) {
$parents[(int) $row->ID] = ((int) $row->post_parent > 0) ? (int) $row->post_parent : (int) $row->ID;
}
foreach ($ids as $id) { if (!isset($parents[$id])) { $parents[$id] = $id; } }
$lookup_ids = array_values(array_unique(array_merge($ids, array_values($parents))));
$pmap = $parents;
foreach ($lookup_ids as $lid) { if (!isset($pmap[$lid])) { $pmap[$lid] = $lid; } }
$found = array();
$pending = function () use (&$found, $lookup_ids, $pmap) {
$rest = array();
foreach ($lookup_ids as $lid) {
if (!isset($found[$lid]) && !isset($found[$pmap[$lid]])) { $rest[] = $lid; }
}
return $rest;
};
$rin = implode(',', $lookup_ids);
foreach ((array) $wpdb->get_results(
"SELECT tr.object_id, tt.term_id
FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'supplier' AND tr.object_id IN ({$rin})"
) as $row) {
$oid = (int) $row->object_id;
if (!isset($found[$oid])) { $found[$oid] = (int) $row->term_id; }
}
$rest = $pending();
$vitals = $wpdb->prefix . 'aieo_core_product_vitals';
if ($rest && function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($vitals)) {
$vin = implode(',', $rest);
foreach ((array) $wpdb->get_results("SELECT product_id, supplier_id FROM {$vitals} WHERE product_id IN ({$vin}) AND supplier_id > 0") as $row) {
$found[(int) $row->product_id] = (int) $row->supplier_id;
}
}
$rest = $pending();
$map = $wpdb->prefix . 'aieo_dmm_supplier_sku_map';
$pml = $wpdb->prefix . 'wc_product_meta_lookup';
if ($rest && function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($map) && aieo_dmm_table_exists($pml)) {
$rin = implode(',', $rest);
foreach ((array) $wpdb->get_results(
"SELECT pml.product_id, m.supplier_term_id
FROM {$pml} pml
JOIN {$map} m ON m.sku = pml.sku
WHERE pml.product_id IN ({$rin}) AND m.supplier_term_id IS NOT NULL"
) as $row) {
$pid = (int) $row->product_id;
if (!isset($found[$pid])) { $found[$pid] = (int) $row->supplier_term_id; }
}
}
$rest = $pending();
if ($rest && class_exists('AIEO_DMM_Brand_Extras_Data') && taxonomy_exists('product_brand')) {
$rin = implode(',', $rest);
$brand = array();
foreach ((array) $wpdb->get_results(
"SELECT tr.object_id, tt.term_id
FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'product_brand' AND tr.object_id IN ({$rin})"
) as $row) {
$brand[(int) $row->object_id][] = (int) $row->term_id;
}
$brand_supplier = array();
foreach ($brand as $oid => $bids) {
foreach ($bids as $bid) {
if (!array_key_exists($bid, $brand_supplier)) {
$extras = AIEO_DMM_Brand_Extras_Data::get('product_brand', $bid);
$brand_supplier[$bid] = (int) ($extras['supplier_term_id'] ?? 0);
}
if ($brand_supplier[$bid] > 0 && !isset($found[$oid])) {
$found[$oid] = $brand_supplier[$bid];
break;
}
}
}
}
foreach ($ids as $id) {
self::$supplier_memo[$id] = (int) ($found[$id] ?? $found[$parents[$id]] ?? 0);
}
}
public static function resolve_supplier($product_id) {
$memo = &self::$supplier_memo;
$product_id = (int) $product_id;
if (isset($memo[$product_id])) { return $memo[$product_id]; }
$product = function_exists('wc_get_product') ? wc_get_product($product_id) : null;
$parent = $product ? ((int) $product->get_parent_id() ?: $product_id) : $product_id;
$terms = wp_get_object_terms($parent, 'supplier', array('fields' => 'ids'));
if (is_array($terms) && $terms) {
return $memo[$product_id] = (int) $terms[0];
}
global $wpdb;
$vitals = $wpdb->prefix . 'aieo_core_product_vitals';
if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($vitals)) {
$vit = $wpdb->get_var($wpdb->prepare(
"SELECT supplier_id FROM {$vitals} WHERE product_id = %d LIMIT 1",
$product_id
));
if (null !== $vit && (int) $vit > 0) {
return $memo[$product_id] = (int) $vit;
}
}
$sku = $product ? (string) $product->get_sku() : '';
if ($sku !== '') {
global $wpdb;
$map = $wpdb->prefix . 'aieo_dmm_supplier_sku_map';
if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($map)) {
$sid = (int) $wpdb->get_var($wpdb->prepare(
"SELECT supplier_term_id FROM {$map} WHERE sku = %s AND supplier_term_id IS NOT NULL LIMIT 1",
$sku
));
if ($sid > 0) { return $memo[$product_id] = $sid; }
}
}
if (class_exists('AIEO_DMM_Brand_Extras_Data') && taxonomy_exists('product_brand')) {
$brands = wp_get_object_terms($parent, 'product_brand', array('fields' => 'ids'));
foreach ((is_array($brands) ? $brands : array()) as $bid) {
$extras = AIEO_DMM_Brand_Extras_Data::get('product_brand', (int) $bid);
if (!empty($extras['supplier_term_id'])) {
return $memo[$product_id] = (int) $extras['supplier_term_id'];
}
}
}
return $memo[$product_id] = 0;
}
private static function supplier_product_ids($supplier_id) {
global $wpdb;
$supplier_id = (int) $supplier_id;
$ids = array();
$vitals = $wpdb->prefix . 'aieo_core_product_vitals';
if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($vitals)) {
$ids = array_merge($ids, (array) $wpdb->get_col($wpdb->prepare(
"SELECT product_id FROM {$vitals} WHERE supplier_id = %d",
$supplier_id
)));
}
$ids = array_merge($ids, (array) $wpdb->get_col($wpdb->prepare(
"SELECT tr.object_id
FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'supplier' AND tt.term_id = %d",
$supplier_id
)));
$map = $wpdb->prefix . 'aieo_dmm_supplier_sku_map';
$pml = $wpdb->prefix . 'wc_product_meta_lookup';
if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($map) && aieo_dmm_table_exists($pml)) {
$ids = array_merge($ids, (array) $wpdb->get_col($wpdb->prepare(
"SELECT pml.product_id
FROM {$map} m
JOIN {$pml} pml ON pml.sku = m.sku
WHERE m.supplier_term_id = %d",
$supplier_id
)));
}
if (taxonomy_exists('product_brand')) {
$brand_ids = array();
$bx = $wpdb->prefix . 'aieo_dmm_brand_extras';
if (function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($bx)) {
$brand_ids = array_map('intval', (array) $wpdb->get_col($wpdb->prepare(
"SELECT term_id FROM {$bx} WHERE taxonomy = 'product_brand' AND supplier_term_id = %d",
$supplier_id
)));
} elseif (class_exists('AIEO_DMM_Brand_Extras_Data')) {
$brands = get_terms(array('taxonomy' => 'product_brand', 'hide_empty' => false, 'fields' => 'ids'));
foreach ((is_wp_error($brands) ? array() : (array) $brands) as $bid) {
$extras = AIEO_DMM_Brand_Extras_Data::get('product_brand', (int) $bid);
if ((int) ($extras['supplier_term_id'] ?? 0) === $supplier_id) {
$brand_ids[] = (int) $bid;
}
}
}
if ($brand_ids) {
$in = implode(',', array_map('intval', $brand_ids));
$ids = array_merge($ids, (array) $wpdb->get_col(
"SELECT tr.object_id
FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'product_brand' AND tt.term_id IN ({$in})"
));
}
}
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
if (empty($ids)) { return array(); }
$in = implode(',', $ids);
$parent = array();
foreach ((array) $wpdb->get_results("SELECT ID, post_parent FROM {$wpdb->posts} WHERE ID IN ({$in})") as $row) {
$parent[(int) $row->ID] = (int) $row->post_parent;
}
$out = array();
foreach ($ids as $id) {
$p = (int) ($parent[$id] ?? 0);
$out[$p > 0 ? $p : $id] = true;
}
return array_keys($out);
}
private static function items_via_lookup($supplier_id, $mode, $from, $to) {
global $wpdb;
$lookup = $wpdb->prefix . 'wc_order_product_lookup';
if (!function_exists('aieo_dmm_table_exists') || !aieo_dmm_table_exists($lookup)) {
return null;
}
$hpos = class_exists('\Automattic\WooCommerce\Utilities\OrderUtil')
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ($hpos) {
$orders_table = $wpdb->prefix . 'wc_orders';
if (!aieo_dmm_table_exists($orders_table)) { return null; }
$join = "JOIN {$orders_table} o ON o.id = lu.order_id";
$status = 'o.status';
} else {
$join = "JOIN {$wpdb->posts} o ON o.ID = lu.order_id";
$status = 'o.post_status';
}
$statuses = array_map(function ($s) { return 'wc-' . $s; }, self::statuses_for($mode));
$status_in = "'" . implode("','", array_map('esc_sql', $statuses)) . "'";
$where_date = '';
if ('period' === $mode) {
$from = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $from) ? $from : wp_date('Y-m-d', time() - 30 * DAY_IN_SECONDS);
$to = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $to) ? $to : wp_date('Y-m-d');
$where_date = $wpdb->prepare(
' AND lu.date_created BETWEEN %s AND %s ',
$from . ' 00:00:00',
$to . ' 23:59:59'
);
}
$orders_scanned = (int) $wpdb->get_var(
"SELECT COUNT(DISTINCT lu.order_id) FROM {$lookup} lu {$join}
WHERE {$status} IN ({$status_in}) {$where_date}"
);
$parents = self::supplier_product_ids($supplier_id);
self::warm_supplier_memo($parents);
$parents = array_values(array_filter($parents, function ($pid) use ($supplier_id) {
return self::resolve_supplier($pid) === (int) $supplier_id;
}));
if (empty($parents)) {
return array('rows' => array(), 'orders_scanned' => $orders_scanned, 'capped' => false);
}
$parents_in = implode(',', array_map('intval', $parents));
$rows = $wpdb->get_results(
"SELECT COALESCE(NULLIF(lu.variation_id, 0), lu.product_id) AS pid,
SUM(lu.product_qty) AS qty,
COUNT(DISTINCT lu.order_id) AS order_count,
MAX(lu.date_created) AS last_order,
GROUP_CONCAT(DISTINCT lu.order_id ORDER BY lu.order_id DESC SEPARATOR ',') AS order_ids
FROM {$lookup} lu
{$join}
WHERE {$status} IN ({$status_in})
AND lu.product_id IN ({$parents_in})
{$where_date}
GROUP BY pid
ORDER BY qty DESC",
ARRAY_A
);
$labels = self::product_labels(wp_list_pluck((array) $rows, 'pid'));
$out = array();
foreach ((array) $rows as $r) {
$pid = (int) $r['pid'];
$ids = array_slice(array_filter(explode(',', (string) $r['order_ids'])), 0, 12);
$out[] = array(
'product_id' => $pid,
'sku' => (string) ($labels[$pid]['sku'] ?? ''),
'name' => (string) ($labels[$pid]['name'] ?? ''),
'qty' => (int) $r['qty'],
'order_count' => (int) $r['order_count'],
'orders' => implode(', ', array_map(function ($id) { return '#' . (int) $id; }, $ids)),
'last_order' => substr((string) $r['last_order'], 0, 10),
);
}
return array('rows' => $out, 'orders_scanned' => $orders_scanned, 'capped' => false);
}
private static function product_labels(array $ids) {
global $wpdb;
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
if (empty($ids)) { return array(); }
$in = implode(',', $ids);
$pml = $wpdb->prefix . 'wc_product_meta_lookup';
$has_pml = function_exists('aieo_dmm_table_exists') && aieo_dmm_table_exists($pml);
$sku_select = $has_pml ? 'pml.sku' : "''";
$sku_join = $has_pml ? "LEFT JOIN {$pml} pml ON pml.product_id = p.ID" : '';
$rows = $wpdb->get_results(
"SELECT p.ID, p.post_title, p.post_parent, pp.post_title AS parent_title, {$sku_select} AS sku
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->posts} pp ON pp.ID = p.post_parent
{$sku_join}
WHERE p.ID IN ({$in})",
ARRAY_A
);
$out = array();
foreach ((array) $rows as $r) {
$name = ((int) $r['post_parent'] > 0 && '' !== (string) $r['parent_title'])
? (string) $r['parent_title']
: (string) $r['post_title'];
$out[(int) $r['ID']] = array('sku' => (string) $r['sku'], 'name' => $name);
}
return $out;
}
private static function window_sql($mode, $from, $to) {
global $wpdb;
$lookup = $wpdb->prefix . 'wc_order_product_lookup';
if (!function_exists('aieo_dmm_table_exists') || !aieo_dmm_table_exists($lookup)) {
return null;
}
$hpos = class_exists('\Automattic\WooCommerce\Utilities\OrderUtil')
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
if ($hpos) {
$orders_table = $wpdb->prefix . 'wc_orders';
if (!aieo_dmm_table_exists($orders_table)) { return null; }
$join = "JOIN {$orders_table} o ON o.id = lu.order_id";
$status = 'o.status';
} else {
$join = "JOIN {$wpdb->posts} o ON o.ID = lu.order_id";
$status = 'o.post_status';
}
$statuses = array_map(function ($s) { return 'wc-' . $s; }, self::statuses_for($mode));
$status_in = "'" . implode("','", array_map('esc_sql', $statuses)) . "'";
$where_date = '';
if ('period' === $mode) {
$from = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $from) ? $from : wp_date('Y-m-d', time() - 30 * DAY_IN_SECONDS);
$to = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $to) ? $to : wp_date('Y-m-d');
$where_date = $wpdb->prepare(
' AND lu.date_created BETWEEN %s AND %s ',
$from . ' 00:00:00',
$to . ' 23:59:59'
);
}
return array(
'lookup' => $lookup,
'join' => $join,
'where' => "{$status} IN ({$status_in})",
'where_date' => $where_date,
);
}
public static function suppliers_in_window($mode = 'pending', $from = '', $to = '') {
global $wpdb;
$w = self::window_sql($mode, $from, $to);
if (null === $w) {
$terms = taxonomy_exists('supplier')
? get_terms(array('taxonomy' => 'supplier', 'hide_empty' => false, 'number' => 500, 'orderby' => 'name'))
: array();
$out = array();
foreach ((is_wp_error($terms) ? array() : (array) $terms) as $t) {
$out[] = array('id' => (int) $t->term_id, 'name' => $t->name, 'lines' => null, 'qty' => null);
}
return array('suppliers' => $out, 'orders_scanned' => 0, 'exact' => false);
}
$orders_scanned = (int) $wpdb->get_var(
"SELECT COUNT(DISTINCT lu.order_id) FROM {$w['lookup']} lu {$w['join']}
WHERE {$w['where']} {$w['where_date']}"
);
$rows = $wpdb->get_results(
"SELECT COALESCE(NULLIF(lu.variation_id, 0), lu.product_id) AS pid,
lu.product_id AS parent_id,
SUM(lu.product_qty) AS qty,
COUNT(DISTINCT lu.order_id) AS order_count
FROM {$w['lookup']} lu
{$w['join']}
WHERE {$w['where']} {$w['where_date']}
GROUP BY pid, parent_id",
ARRAY_A
);
if (empty($rows)) {
return array('suppliers' => array(), 'orders_scanned' => $orders_scanned, 'exact' => true);
}
$ids = array();
foreach ($rows as $r) { $ids[] = (int) $r['pid']; $ids[] = (int) $r['parent_id']; }
self::warm_supplier_memo($ids);
$agg = array();
foreach ($rows as $r) {
$sid = self::resolve_supplier((int) $r['parent_id']);
if ($sid <= 0) { continue; }
if (!isset($agg[$sid])) { $agg[$sid] = array('lines' => 0, 'qty' => 0); }
$agg[$sid]['lines']++;
$agg[$sid]['qty'] += (int) $r['qty'];
}
$out = array();
foreach ($agg as $sid => $a) {
$term = get_term((int) $sid, 'supplier');
if (!$term || is_wp_error($term)) { continue; }
$out[] = array(
'id' => (int) $sid,
'name' => $term->name,
'lines' => (int) $a['lines'],
'qty' => (int) $a['qty'],
);
}
usort($out, function ($a, $b) {
return ($b['qty'] === $a['qty']) ? strcoll($a['name'], $b['name']) : ($b['qty'] - $a['qty']);
});
return array('suppliers' => $out, 'orders_scanned' => $orders_scanned, 'exact' => true);
}
public static function items_for_suppliers(array $supplier_ids, $mode = 'pending', $from = '', $to = '') {
$supplier_ids = array_values(array_unique(array_filter(array_map('intval', $supplier_ids))));
if (empty($supplier_ids)) {
return array('rows' => array(), 'orders_scanned' => 0, 'capped' => false);
}
if (1 === count($supplier_ids)) {
return self::items_for_supplier($supplier_ids[0], $mode, $from, $to);
}
$merged = array();
$scanned = 0;
$capped = false;
foreach ($supplier_ids as $sid) {
$one = self::items_for_supplier($sid, $mode, $from, $to);
$scanned = max($scanned, (int) $one['orders_scanned']);
$capped = $capped || !empty($one['capped']);
$term = get_term($sid, 'supplier');
$name = ($term && !is_wp_error($term)) ? $term->name : '';
foreach ($one['rows'] as $row) {
$key = (int) $row['product_id'];
if (!isset($merged[$key])) {
$row['supplier_id'] = $sid;
$row['supplier_name'] = $name;
$merged[$key] = $row;
}
}
}
$rows = array_values($merged);
usort($rows, function ($a, $b) { return $b['qty'] - $a['qty']; });
return array('rows' => $rows, 'orders_scanned' => $scanned, 'capped' => $capped);
}
public static function items_for_supplier($supplier_id, $mode = 'pending', $from = '', $to = '') {
$fast = self::items_via_lookup((int) $supplier_id, $mode, $from, $to);
if (null !== $fast) {
return $fast;
}
return self::items_by_walking_orders($supplier_id, $mode, $from, $to);
}
private static function items_by_walking_orders($supplier_id, $mode = 'pending', $from = '', $to = '') {
$supplier_id = (int) $supplier_id;
$args = array(
'status' => self::statuses_for($mode),
'limit' => self::MAX_ORDERS + 1,
'orderby' => 'date',
'order' => 'DESC',
'type' => 'shop_order',
);
if ('period' === $mode) {
$from = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $from) ? $from : wp_date('Y-m-d', time() - 30 * DAY_IN_SECONDS);
$to = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $to) ? $to : wp_date('Y-m-d');
try {
$tz = wp_timezone();
$from_ts = (new \DateTime($from . ' 00:00:00', $tz))->getTimestamp();
$to_ts = (new \DateTime($to . ' 23:59:59', $tz))->getTimestamp();
$args['date_created'] = $from_ts . '...' . $to_ts;
} catch (\Throwable $e) {
$args['date_created'] = $from . '...' . $to;
}
}
$orders = function_exists('wc_get_orders') ? wc_get_orders($args) : array();
$capped = count($orders) > self::MAX_ORDERS;
if ($capped) { $orders = array_slice($orders, 0, self::MAX_ORDERS); }
$rows = array();
foreach ($orders as $order) {
foreach ($order->get_items('line_item') as $item) {
$pid = (int) ($item->get_variation_id() ?: $item->get_product_id());
if ($pid <= 0) { continue; }
$owner = (int) $item->get_product_id();
if (self::resolve_supplier($owner > 0 ? $owner : $pid) !== $supplier_id) { continue; }
if (!isset($rows[$pid])) {
$product = wc_get_product($pid);
$rows[$pid] = array(
'product_id' => $pid,
'sku' => $product ? (string) $product->get_sku() : '',
'name' => $product ? (string) $product->get_name() : (string) $item->get_name(),
'qty' => 0,
'orders' => array(),
'last_order' => '',
);
}
$rows[$pid]['qty'] += (int) $item->get_quantity();
$rows[$pid]['orders'][$order->get_id()] = '#' . $order->get_order_number();
$created = $order->get_date_created();
$when = $created ? $created->date_i18n('Y-m-d') : '';
if ($when > $rows[$pid]['last_order']) { $rows[$pid]['last_order'] = $when; }
}
}
foreach ($rows as &$row) {
$row['order_count'] = count($row['orders']);
$row['orders'] = implode(', ', array_slice(array_values($row['orders']), 0, 12));
}
unset($row);
usort($rows, function ($a, $b) { return $b['qty'] - $a['qty']; });
return array(
'rows' => array_values($rows),
'orders_scanned' => count($orders),
'capped' => $capped,
);
}
private static function view_params() {
$src = array_merge($_GET, $_POST);
$ids = array();
if (isset($src['supplier_ids'])) {
$raw = is_array($src['supplier_ids'])
? $src['supplier_ids']
: explode(',', (string) $src['supplier_ids']);
$ids = array_map('intval', (array) $raw);
}
if (isset($src['supplier_id'])) {
$ids[] = (int) $src['supplier_id'];
}
$ids = array_values(array_unique(array_filter($ids)));
return array(
$ids,
(isset($src['mode']) && 'period' === $src['mode']) ? 'period' : 'pending',
sanitize_text_field((string) ($src['from'] ?? '')),
sanitize_text_field((string) ($src['to'] ?? '')),
);
}
private static function valid_supplier_ids(array $ids) {
$out = array();
foreach ($ids as $id) {
if ((int) $id > 0 && term_exists((int) $id, 'supplier')) { $out[] = (int) $id; }
}
return $out;
}
public function ajax_items() {
AIEO_DMM::verify_ajax_request();
list($supplier_ids, $mode, $from, $to) = self::view_params();
$supplier_ids = self::valid_supplier_ids($supplier_ids);
if (empty($supplier_ids)) {
wp_send_json_error(array('message' => __('Pick a supplier first.', 'ai-eshop-optimizer')));
}
$data = self::items_for_suppliers($supplier_ids, $mode, $from, $to);
$data['multi'] = count($supplier_ids) > 1;
$data['table'] = self::items_table((array) ($data['rows'] ?? array()), $data['multi']);
wp_send_json_success($data);
}
private static function items_table(array $items, $multi) {
if (!class_exists('AIEO_Table')) {
return array('html' => '', 'icons' => '');
}
$columns = array(
array('key' => 'sku', 'label' => __('SKU', 'ai-eshop-optimizer'), 'type' => 'code', 'sort_type' => 'text'),
array('key' => 'name', 'label' => __('Product', 'ai-eshop-optimizer'), 'sort_type' => 'text'),
);
if ($multi) {
$columns[] = array('key' => 'supplier', 'label' => __('Supplier', 'ai-eshop-optimizer'), 'sort_type' => 'text');
}
$columns[] = array('key' => 'qty', 'label' => __('Quantity', 'ai-eshop-optimizer'), 'type' => 'num', 'sort_type' => 'number');
$columns[] = array('key' => 'orders', 'label' => __('Orders', 'ai-eshop-optimizer'), 'type' => 'num', 'sort_type' => 'number');
$columns[] = array('key' => 'last', 'label' => __('Last order', 'ai-eshop-optimizer'), 'sort_type' => 'text');
$rows = array();
foreach ($items as $i => $r) {
$sku = (string) ($r['sku'] ?? '');
$name = (string) ($r['name'] ?? '');
$rows[] = array(
'_id' => (string) ($i + 1),
'sku' => array('value' => $sku),
'name' => $name,
'supplier' => (string) ($r['supplier_name'] ?? ''),
'qty' => array('html' => '' . esc_html(number_format_i18n((int) ($r['qty'] ?? 0))) . ''),
'orders' => array('html' => esc_html((string) ($r['order_count'] ?? '')) . ' ' . esc_html((string) ($r['orders'] ?? '')) . ''),
'last' => (string) ($r['last_order'] ?? ''),
'_s' => array(
'sku' => $sku,
'name' => $name,
'supplier' => (string) ($r['supplier_name'] ?? ''),
'qty' => (int) ($r['qty'] ?? 0),
'orders' => (int) ($r['order_count'] ?? 0),
'last' => (string) ($r['last_order'] ?? ''),
),
'_text' => $sku . ' ' . $name,
);
}
return AIEO_Table::local_answer('aieo-so-table', $columns, $rows, array(
'prefix' => 'so_items_',
'size' => 25,
'caption' => __('Items to order, by supplier', 'ai-eshop-optimizer'),
'empty' => esc_html__('No items for this supplier in the chosen orders.', 'ai-eshop-optimizer'),
'search' => array('label' => __('Search', 'ai-eshop-optimizer')),
));
}
public function ajax_suppliers() {
AIEO_DMM::verify_ajax_request();
list(, $mode, $from, $to) = self::view_params();
wp_send_json_success(self::suppliers_in_window($mode, $from, $to));
}
public function ajax_picker_search() {
AIEO_DMM::verify_ajax_request();
list(, $mode, $from, $to) = self::view_params();
$q = isset($_GET['q']) ? sanitize_text_field(wp_unslash($_GET['q'])) : '';
$window = self::suppliers_in_window($mode, $from, $to);
$results = array();
foreach ($window['suppliers'] as $s) {
if ('' !== $q && false === mb_stripos($s['name'], $q)) {
continue;
}
$label = $s['name'];
if (null !== $s['qty']) {
$label .= sprintf(
' — ' . __('%s pieces', 'ai-eshop-optimizer'),
number_format_i18n((int) $s['qty'])
);
}
$results[] = array(
'id' => (int) $s['id'],
'text' => $label,
'name' => $s['name'],
'type' => 'simple',
'status' => 'publish',
'n_variations' => 0,
);
}
wp_send_json(array('results' => $results));
}
public function handle_export() {
if (!current_user_can(AIEO_CAPABILITY)) { wp_die(esc_html__('Insufficient permissions.', 'ai-eshop-optimizer')); }
check_admin_referer('aieo_supplier_items_export');
if (!class_exists('\Rooster\Admin\Export') && function_exists('rooster_admin_builders')) {
rooster_admin_builders();
}
if (!class_exists('\Rooster\Admin\Export')) { wp_die(esc_html__('XLSX writer unavailable.', 'ai-eshop-optimizer')); }
list($supplier_ids, $mode, $from, $to) = self::view_params();
$supplier_ids = self::valid_supplier_ids($supplier_ids);
if (empty($supplier_ids)) { wp_die(esc_html__('That supplier no longer exists.', 'ai-eshop-optimizer')); }
$names = array();
foreach ($supplier_ids as $sid) {
$t = get_term($sid, 'supplier');
if ($t && !is_wp_error($t)) { $names[] = $t->name; }
}
$title = implode(' + ', $names);
$multi = count($supplier_ids) > 1;
$data = self::items_for_suppliers($supplier_ids, $mode, $from, $to);
$whole = array('format' => array('decimals' => 0));
$columns = array(
'sku' => array('label' => __('SKU', 'ai-eshop-optimizer'), 'type' => 'text'),
'name' => __('Product', 'ai-eshop-optimizer'),
);
if ($multi) { $columns['supplier_name'] = __('Supplier', 'ai-eshop-optimizer'); }
$columns += array(
'qty' => array('label' => __('Quantity', 'ai-eshop-optimizer')) + $whole,
'order_count' => array('label' => __('Orders', 'ai-eshop-optimizer')) + $whole,
'orders' => array('label' => __('Order numbers', 'ai-eshop-optimizer'), 'type' => 'text'),
'last_order' => __('Last order', 'ai-eshop-optimizer'),
);
$slug = $multi ? (sanitize_title($names[0]) . '-plus-' . (count($names) - 1)) : sanitize_title($title);
\Rooster\Admin\Export::xlsx('supplier-items-' . $slug . '-' . $mode . '.xlsx', array(array('name' => $title, 'columns' => $columns, 'rows' => $data['rows'])));
exit;
}
public static function email_supplier_items($supplier_id, $mode = 'pending', $from = '', $to = '') {
$term = get_term((int) $supplier_id, 'supplier');
if (!$term || is_wp_error($term)) {
return new WP_Error('aieo_supplier_missing', __('That supplier no longer exists.', 'ai-eshop-optimizer'));
}
$address = '';
if (class_exists('AIEO_DMM_Supplier_Extras_Data')) {
$extras = AIEO_DMM_Supplier_Extras_Data::get('supplier', (int) $supplier_id);
$address = (string) ($extras['email'] ?? '');
if ('' === $address) { $address = (string) ($extras['corporate_email'] ?? ''); }
}
if ('' === $address || !is_email($address)) {
return new WP_Error('aieo_supplier_no_email', __('This supplier has no order-taking email on record. Add one on the Suppliers page first.', 'ai-eshop-optimizer'));
}
$data = self::items_for_supplier((int) $supplier_id, $mode, $from, $to);
if (empty($data['rows'])) {
return new WP_Error('aieo_supplier_no_items', __('Nothing to order: the chosen view holds no items for this supplier.', 'ai-eshop-optimizer'));
}
$shop = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
$lines = array();
$lines[] = sprintf(__('Dear %s,', 'ai-eshop-optimizer'), $term->name);
$lines[] = '';
$lines[] = __('We would like to order the following items:', 'ai-eshop-optimizer');
$lines[] = '';
foreach ($data['rows'] as $r) {
$lines[] = sprintf(
'- %1$s × %2$d — %3$s',
('' !== $r['sku']) ? $r['sku'] : __('(no SKU)', 'ai-eshop-optimizer'),
(int) $r['qty'],
$r['name']
);
}
$lines[] = '';
$lines[] = __('Please confirm availability and expected delivery.', 'ai-eshop-optimizer');
$lines[] = '';
$lines[] = __('Kind regards,', 'ai-eshop-optimizer');
$lines[] = $shop;
$subject = sprintf(__('Restock order from %s', 'ai-eshop-optimizer'), $shop);
$sent = wp_mail($address, $subject, implode("\n", $lines));
if (!$sent) {
return new WP_Error('aieo_supplier_mail_failed', __('The email could not be sent — check the site\'s outgoing mail.', 'ai-eshop-optimizer'));
}
return array('sent_to' => $address, 'items' => count($data['rows']));
}
public function ajax_email() {
AIEO_DMM::verify_ajax_request();
list($supplier_ids, $mode, $from, $to) = self::view_params();
$supplier_ids = self::valid_supplier_ids($supplier_ids);
if (empty($supplier_ids)) {
wp_send_json_error(array('message' => __('Pick a supplier first.', 'ai-eshop-optimizer')));
}
$sent = array();
$items = 0;
$errors = array();
foreach ($supplier_ids as $sid) {
$result = self::email_supplier_items($sid, $mode, $from, $to);
if (is_wp_error($result)) {
$term = get_term($sid, 'supplier');
$name = ($term && !is_wp_error($term)) ? $term->name : ('#' . $sid);
$errors[] = $name . ': ' . $result->get_error_message();
continue;
}
$sent[] = $result['sent_to'];
$items += (int) $result['items'];
}
if (empty($sent)) {
wp_send_json_error(array('message' => implode(' · ', $errors)));
}
$message = sprintf(
__('Sent %1$s item lines to %2$s.', 'ai-eshop-optimizer'),
number_format_i18n($items),
implode(', ', $sent)
);
if ($errors) {
$message .= ' — ' . implode(' · ', $errors);
}
wp_send_json_success(array('message' => $message));
}
public function report_bulk($result, $ctx, $def) {
if (false !== $result) { return $result; }
if ('supplier_preorders_queue' !== (string) $ctx['report']) { return $result; }
$map = array(
'approve' => 'approve',
'approved' => 'approve',
'mark_ordered' => 'mark_ordered',
'ordered' => 'mark_ordered',
'reject' => 'reject',
);
if (!isset($map[(string) $ctx['action']])) { return $result; }
$action = $map[(string) $ctx['action']];
if (!class_exists('AIEO_DMM_Branch_Scanner')) {
return __('The pre-order status flow is unavailable.', 'ai-eshop-optimizer');
}
global $wpdb;
$table = $wpdb->prefix . 'aieo_dmm_supplier_preorders';
$ids = array_values(array_unique(array_filter(array_map('intval', (array) $ctx['ids']))));
if (empty($ids)) {
return array('updated' => 0, 'skipped' => 0);
}
$in = implode(',', $ids);
$rows = $wpdb->get_results(
"SELECT id, status, fulfilled, supplier_term_id FROM {$table} WHERE id IN ({$in})",
ARRAY_A
);
$rows = is_array($rows) ? $rows : array();
$user_id = (int) get_current_user_id();
$day = wp_date('Ymd');
$updated = 0;
$skipped = count($ids) - count($rows);
$batches = array();
foreach ($rows as $r) {
$id = (int) $r['id'];
$st = (string) $r['status'];
$ok = false;
if ('approve' === $action) {
if ('requested' === $st) {
$ok = !is_wp_error(AIEO_DMM_Branch_Scanner::apply_preorder_status(
$id, 'approved', null, $user_id, array('source' => 'queue', 'note' => 'bulk approve')
));
}
} elseif ('reject' === $action) {
if (0 === (int) $r['fulfilled']) {
$ok = !is_wp_error(AIEO_DMM_Branch_Scanner::apply_preorder_status(
$id, 'rejected', null, $user_id, array('source' => 'queue', 'note' => 'bulk reject')
));
}
} elseif (in_array($st, array('requested', 'approved'), true)) {
$batch = 'PO-' . $day . '-' . (int) $r['supplier_term_id'];
$ok = !is_wp_error(AIEO_DMM_Branch_Scanner::apply_preorder_status(
$id, 'ordered', null, $user_id, array('source' => 'queue', 'batch_ref' => $batch)
));
if ($ok) { $batches[$batch] = true; }
}
$ok ? $updated++ : $skipped++;
}
$message = '';
if ('mark_ordered' === $action && !empty($batches)) {
$message = sprintf(
__('Batches stamped: %s. Export or email each from the Ordered batches card.', 'ai-eshop-optimizer'),
implode(', ', array_keys($batches))
);
}
return array('updated' => $updated, 'skipped' => $skipped, 'message' => $message);
}
private static function batch_lines($batch_ref) {
global $wpdb;
$table = $wpdb->prefix . 'aieo_dmm_supplier_preorders';
$pv = $wpdb->prefix . 'aieo_core_product_vitals';
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT p.product_id,
MIN( COALESCE( NULLIF( v.product_name, '' ), NULLIF( v.name, '' ), CONCAT( '#', p.product_id ) ) ) AS name,
MIN( v.sku ) AS sku,
SUM( p.requested_qty ) AS qty,
MIN( p.supplier_term_id ) AS supplier_term_id
FROM {$table} p
LEFT JOIN {$pv} v ON v.product_id = p.product_id
WHERE p.batch_ref = %s
GROUP BY p.product_id
ORDER BY qty DESC",
$batch_ref
), ARRAY_A);
$rows = is_array($rows) ? $rows : array();
$sid = 0;
foreach ($rows as $r) {
if ((int) $r['supplier_term_id'] > 0) { $sid = (int) $r['supplier_term_id']; break; }
}
return array('rows' => $rows, 'supplier_term_id' => $sid);
}
public function handle_batch_export() {
if (!current_user_can(AIEO_CAPABILITY)) { wp_die(esc_html__('Insufficient permissions.', 'ai-eshop-optimizer')); }
check_admin_referer('aieo_preorder_batch_export');
$batch = isset($_GET['batch']) ? sanitize_text_field(wp_unslash($_GET['batch'])) : '';
if ('' === $batch) { wp_die(esc_html__('No batch named.', 'ai-eshop-optimizer')); }
if (!class_exists('\Rooster\Admin\Export') && function_exists('rooster_admin_builders')) {
rooster_admin_builders();
}
if (!class_exists('\Rooster\Admin\Export')) { wp_die(esc_html__('XLSX writer unavailable.', 'ai-eshop-optimizer')); }
$data = self::batch_lines($batch);
if (empty($data['rows'])) { wp_die(esc_html__('That batch holds no lines.', 'ai-eshop-optimizer')); }
$columns = array(
'sku' => array('label' => __('SKU', 'ai-eshop-optimizer'), 'type' => 'text'),
'name' => __('Product', 'ai-eshop-optimizer'),
'qty' => array('label' => __('Quantity', 'ai-eshop-optimizer'), 'format' => array('decimals' => 0)),
'batch' => __('Batch', 'ai-eshop-optimizer'),
);
$lines = array();
foreach ($data['rows'] as $r) {
$lines[] = array('sku' => (string) $r['sku'], 'name' => (string) $r['name'], 'qty' => (int) $r['qty'], 'batch' => $batch);
}
\Rooster\Admin\Export::xlsx('preorder-batch-' . sanitize_title($batch) . '.xlsx', array(array('name' => $batch, 'columns' => $columns, 'rows' => $lines)));
exit;
}
public function ajax_batch_email() {
AIEO_DMM::verify_ajax_request();
$batch = isset($_POST['batch']) ? sanitize_text_field(wp_unslash($_POST['batch'])) : '';
if ('' === $batch) {
wp_send_json_error(array('message' => __('No batch named.', 'ai-eshop-optimizer')));
}
$data = self::batch_lines($batch);
if (empty($data['rows'])) {
wp_send_json_error(array('message' => __('That batch holds no lines.', 'ai-eshop-optimizer')));
}
$sid = (int) $data['supplier_term_id'];
$term = $sid > 0 ? get_term($sid, 'supplier') : null;
if (!$term || is_wp_error($term)) {
wp_send_json_error(array('message' => __('This batch has no resolved supplier, so there is nobody to email. Export the XLSX instead.', 'ai-eshop-optimizer')));
}
$address = '';
if (class_exists('AIEO_DMM_Supplier_Extras_Data')) {
$extras = AIEO_DMM_Supplier_Extras_Data::get('supplier', $sid);
$address = (string) ($extras['email'] ?? '');
if ('' === $address) { $address = (string) ($extras['corporate_email'] ?? ''); }
}
if ('' === $address || !is_email($address)) {
wp_send_json_error(array('message' => __('This supplier has no order-taking email on record. Add one on the Suppliers page first.', 'ai-eshop-optimizer')));
}
$shop = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
$lines = array();
$lines[] = sprintf(__('Dear %s,', 'ai-eshop-optimizer'), $term->name);
$lines[] = '';
$lines[] = sprintf(__('We would like to order the following items (order reference %s):', 'ai-eshop-optimizer'), $batch);
$lines[] = '';
foreach ($data['rows'] as $r) {
$lines[] = sprintf(
'- %1$s × %2$d — %3$s',
('' !== (string) $r['sku']) ? (string) $r['sku'] : __('(no SKU)', 'ai-eshop-optimizer'),
(int) $r['qty'],
(string) $r['name']
);
}
$lines[] = '';
$lines[] = sprintf(__('Please confirm availability and expected delivery, quoting reference %s.', 'ai-eshop-optimizer'), $batch);
$lines[] = '';
$lines[] = __('Kind regards,', 'ai-eshop-optimizer');
$lines[] = $shop;
$subject = sprintf(__('Restock order %1$s from %2$s', 'ai-eshop-optimizer'), $batch, $shop);
$sent = wp_mail($address, $subject, implode("\n", $lines));
if (!$sent) {
wp_send_json_error(array('message' => __('The email could not be sent — check the site\'s outgoing mail.', 'ai-eshop-optimizer')));
}
wp_send_json_success(array(
'message' => sprintf(
__('Sent %1$s item lines to %2$s.', 'ai-eshop-optimizer'),
number_format_i18n(count($data['rows'])),
$address
),
));
}
public static function fetch_preorder_summary_by_supplier() {
global $wpdb;
$table = $wpdb->prefix . 'aieo_dmm_supplier_preorders';
if (!function_exists('aieo_dmm_table_exists') || !aieo_dmm_table_exists($table)) { return array(); }
$rows = $wpdb->get_results(
"SELECT supplier_term_id,
COUNT(*) AS line_count,
SUM(requested_qty) AS total_qty,
MAX(created_at) AS most_recent_at
FROM {$table}
WHERE fulfilled = 0
GROUP BY supplier_term_id
ORDER BY total_qty DESC",
ARRAY_A
);
if (!is_array($rows)) { return array(); }
foreach ($rows as &$r) {
$r['supplier_name'] = $r['supplier_term_id']
? (function ($id) { $t = get_term((int) $id); return ($t && !is_wp_error($t)) ? $t->name : '— unresolved —'; })($r['supplier_term_id'])
: '— unresolved —';
}
return $rows;
}
public function render_subtab() {
if (class_exists('AIEO_DMM_Product_Picker')) {
AIEO_DMM_Product_Picker::enqueue_assets();
}
$this->render_template('supplier-orders.php', array(
'preorder_summary' => self::fetch_preorder_summary_by_supplier(),
'export_base' => wp_nonce_url(admin_url('admin-post.php?action=aieo_supplier_items_export'), 'aieo_supplier_items_export'),
'batch_export_base' => wp_nonce_url(admin_url('admin-post.php?action=aieo_preorder_batch_export'), 'aieo_preorder_batch_export'),
));
}
}