__('Branch Stock Scanner', 'ai-eshop-optimizer'), 'description' => __('Multi-branch / multi-warehouse stock visibility from inside the WooCommerce admin. Scan a SKU or barcode and see availability per location, transfer history, and per-branch sell-through — so staff in store #2 stop telling customers a SKU is out of stock when #4 has six on the shelf.', 'ai-eshop-optimizer'), 'bullets' => array( __('Per-branch stock counters with one-click rebalance.', 'ai-eshop-optimizer'), __('Barcode scanner UI optimised for handheld + tablet on the warehouse floor.', 'ai-eshop-optimizer'), __('Integrates with the ERP Bridge for live-from-truck updates.', 'ai-eshop-optimizer'), ), ); } const SCAN_NONCE_ACTION = 'aieo_dmm_scan'; public static function worksheet_slugs() { if (!class_exists('AIEO_DMM_Branches')) { $f = AIEO_DIR . 'includes/dmm/services/class-aieo-dmm-branches.php'; if (is_readable($f)) { require_once $f; } } if (!class_exists('AIEO_DMM_Branches')) { return array(); } $out = array(); foreach (AIEO_DMM_Branches::cached() as $b) { if ('' !== (string) $b['slug']) { $out[] = (string) $b['slug']; } } return $out; } public static function user_can_scan() { return current_user_can('aieo_use_stock_scanner') || current_user_can('manage_woocommerce') || current_user_can('edit_products'); } public static function worksheet_cap($slug) { return 'aieo_preorder_ws_' . $slug; } public static function seed_worksheet_caps_once() { if (!class_exists('AIEO_DMM_Branches')) { return; } $admin = get_role('administrator'); $seeded = get_option('aieo_preorder_ws_seeded_slugs', null); if (!is_array($seeded)) { $seeded = array(); if ((int) get_option('aieo_preorder_ws_seed', 0) >= 1) { foreach (AIEO_DMM_Branches::cached() as $b) { $ws = (string) $b['slug']; if ('' === $ws) { continue; } if (null !== get_role('rooster_branch_' . $ws) || ($admin && $admin->has_cap(self::worksheet_cap($ws)))) { $seeded[] = $ws; } } } } $dirty = false; foreach (AIEO_DMM_Branches::cached() as $b) { $ws = (string) $b['slug']; if ($ws === '' || in_array($ws, $seeded, true)) { continue; } if (null === get_role('rooster_branch_' . $ws)) { add_role( 'rooster_branch_' . $ws, sprintf(__('Branch staff: %s', 'ai-eshop-optimizer'), (string) $b['label']), array( 'read' => true, 'aieo_access' => true, 'aieo_use_stock_scanner' => true, self::worksheet_cap($ws) => true, ) ); } if ($admin) { $admin->add_cap(self::worksheet_cap($ws)); } $seeded[] = $ws; $dirty = true; } if ($dirty || null === get_option('aieo_preorder_ws_seeded_slugs', null)) { update_option('aieo_preorder_ws_seeded_slugs', array_values(array_unique($seeded)), false); } } public function register_hooks() { add_action('wp_ajax_aieo_dmm_branch_save', array($this, 'ajax_save_branch')); add_action('wp_ajax_aieo_dmm_branch_delete', array($this, 'ajax_delete_branch')); add_action('wp_ajax_aieo_dmm_branch_erp_settings', array($this, 'ajax_save_erp_settings')); add_action('wp_ajax_aieo_dmm_preorder_set_status', array($this, 'ajax_preorder_set_status')); add_action('wp_ajax_aieo_dmm_scanner_staff_lookup', array($this, 'ajax_staff_lookup')); add_action('wp_ajax_aieo_dmm_scanner_staff_update', array($this, 'ajax_staff_update')); add_action('wp_ajax_aieo_dmm_scanner_log_missing', array($this, 'ajax_log_missing')); add_action('wp_ajax_aieo_dmm_scanner_customer_lookup', array($this, 'ajax_customer_lookup')); add_action('wp_ajax_nopriv_aieo_dmm_scanner_customer_lookup', array($this, 'ajax_customer_lookup')); add_shortcode('aieo_dmm_branch_scanner', array($this, 'shortcode_staff_scanner')); add_shortcode('aieo_dmm_customer_scanner', array($this, 'shortcode_customer_scanner')); add_filter('aieo_report_save_cell', array($this, 'report_save_cell'), 10, 3); add_filter('aieo_report_bulk', array($this, 'report_bulk'), 10, 3); if (is_admin()) { add_action('admin_init', array(__CLASS__, 'ensure_role_registered')); } add_filter('woocommerce_prevent_admin_access', array(__CLASS__, 'keep_staff_admin')); add_filter('woocommerce_disable_admin_bar', array(__CLASS__, 'keep_staff_admin')); } public static function keep_staff_admin($prevent) { if (!$prevent || !is_user_logged_in()) { return $prevent; } if (current_user_can('aieo_use_stock_scanner')) { return false; } if (class_exists('AIEO_DMM_Branches')) { foreach ((array) AIEO_DMM_Branches::cached() as $b) { $ws = isset($b['slug']) ? (string) $b['slug'] : ''; if ($ws !== '' && current_user_can(self::worksheet_cap($ws))) { return false; } } } return $prevent; } public static function ensure_role_registered() { if (class_exists('AIEO_DMM_Branch_Provisioner')) { AIEO_DMM_Branch_Provisioner::ensure_scanner_role(); } self::seed_worksheet_caps_once(); } public function get_tab_arg() { return 'bs_section'; } public function get_tabs() { $tabs = array('scanning' => array('label' => __('Scanning', 'ai-eshop-optimizer'), 'icon' => 'activity')); if (class_exists('AIEO_DMM_Branches')) { foreach (AIEO_DMM_Branches::cached() as $b) { if ('' === (string) $b['slug']) { continue; } $tabs['worksheet_' . $b['slug']] = array('label' => sprintf(__('Worksheet: %s', 'ai-eshop-optimizer'), (string) $b['label']), 'icon' => 'clipboard-list'); } } return $tabs; } public function render_subtab() { $tabs = $this->get_tabs(); foreach (self::worksheet_slugs() as $aieo_ws) { if (!current_user_can(self::worksheet_cap($aieo_ws))) { unset($tabs['worksheet_' . $aieo_ws]); } } $section = isset($_GET['bs_section']) ? sanitize_key((string) wp_unslash($_GET['bs_section'])) : 'scanning'; if (!isset($tabs[$section])) { $section = 'scanning'; } AIEO_DMM_UI::tab_strip($this->get_id(), $tabs, $section, array( 'arg' => $this->get_tab_arg(), 'wrapper' => 'div', 'margin' => '14px', )); if (0 === strpos($section, 'worksheet_')) { $this->render_worksheet(substr($section, strlen('worksheet_')), $tabs[$section]); return; } $this->render_template('branch-scanner.php', array( 'branches' => $this->fetch_branches(), 'recent_log' => $this->fetch_recent_log(25), )); } private function render_worksheet($slug, $tab_label) { if (!in_array($slug, self::worksheet_slugs(), true)) { return; } if (!current_user_can(self::worksheet_cap($slug))) { echo '

' . esc_html__('You do not have access to this branch\'s worksheet.', 'ai-eshop-optimizer') . '

'; return; } $branch = $this->get_branch_by_slug($slug); if (!$branch) { echo '

' . esc_html__('This branch is not enabled in the branch registry, so its worksheet is read-only history. Enable it under Scanning to work the sheet.', 'ai-eshop-optimizer') . '

'; } if (class_exists('AIEO_Report_Render')) { AIEO_Report_Render::card('preorder_worksheet_' . $slug, array( 'anchor' => 'branch-scanner-worksheet-' . $slug, 'heading' => 'h2', 'icon' => 'clipboard-list', 'title' => $tab_label, 'description' => __( 'The weekly restock pass: the whole preorder-eligible catalogue with this branch\'s own 30/60-day sales. Type the shelf count and the quantity to request, then Submit requests — lines without a quantity are skipped, and a line with an open request updates that request instead of creating a duplicate. The scanner stays the fast path for ad-hoc requests.', 'ai-eshop-optimizer' ), 'footnote' => __('Quantities autosave as you type them and survive the nightly catalogue rebuild. Submitting clears the sheet and files the requests under Supplier Orders for approval.', 'ai-eshop-optimizer'), )); } } public function fetch_branches() { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_branches'; if (!aieo_dmm_table_exists($table)) { return array(); } $rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY sort_order ASC, label ASC", ARRAY_A); return is_array($rows) ? $rows : array(); } public function fetch_enabled_branches() { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_branches'; if (!aieo_dmm_table_exists($table)) { return array(); } $rows = $wpdb->get_results("SELECT * FROM {$table} WHERE enabled = 1 ORDER BY sort_order ASC, label ASC", ARRAY_A); return is_array($rows) ? $rows : array(); } private function fetch_recent_log($limit = 25) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_branch_scan_log'; if (!aieo_dmm_table_exists($table)) { return array(); } $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d", (int) $limit ), ARRAY_A); return is_array($rows) ? $rows : array(); } public function ajax_save_branch() { AIEO_DMM::verify_ajax_request(); global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_branches'; if (!aieo_dmm_table_exists($table)) { wp_send_json_error(array('message' => __('Table missing.', 'ai-eshop-optimizer'))); } $id = isset($_POST['id']) ? absint(wp_unslash($_POST['id'])) : 0; $slug = isset($_POST['slug']) ? sanitize_key(wp_unslash($_POST['slug'])) : ''; $label = isset($_POST['label']) ? sanitize_text_field(wp_unslash($_POST['label'])) : ''; $stock_table = isset($_POST['stock_table']) ? sanitize_text_field(wp_unslash($_POST['stock_table'])) : ''; $match_col = isset($_POST['match_col']) ? sanitize_key(wp_unslash($_POST['match_col'])) : 'gtin'; $enabled = !empty($_POST['enabled']) ? 1 : 0; $sort_order = isset($_POST['sort_order']) ? (int) $_POST['sort_order'] : 0; $erp_id_raw = isset($_POST['erp_id']) ? trim(sanitize_text_field((string) wp_unslash($_POST['erp_id']))) : ''; $erp_id = ($erp_id_raw === '') ? null : $erp_id_raw; $match_patterns_raw = isset($_POST['match_patterns']) ? trim(sanitize_text_field((string) wp_unslash($_POST['match_patterns']))) : ''; $match_patterns = ($match_patterns_raw === '') ? null : $match_patterns_raw; if ($slug === '' || $label === '') { wp_send_json_error(array('message' => __('Slug and label are required.', 'ai-eshop-optimizer'))); } if ($stock_table === '' && $id <= 0) { if (!class_exists('AIEO_DMM_Branch_Provisioner')) { wp_send_json_error(array('message' => __('Rooster One Pro is not installed. The branch provisioning engine ships with the Pro plugin — install and activate it and this screen comes alive.', 'ai-eshop-optimizer'))); } $res = AIEO_DMM_Branch_Provisioner::provision($slug, $label, array( 'sort_order' => $sort_order, 'erp_id' => $erp_id, 'match_patterns' => $match_patterns, )); if (is_wp_error($res)) { wp_send_json_error(array('message' => $res->get_error_message())); } if (!$enabled) { $wpdb->update($table, array('enabled' => 0), array('id' => (int) $res['branch_id'])); } wp_send_json_success(array( 'id' => (int) $res['branch_id'], 'message' => sprintf( __('Branch provisioned: board %1$s created and seeded with %2$s products; scanner page at %3$s.', 'ai-eshop-optimizer'), $res['board'], number_format_i18n((int) $res['seeded']), $res['page_url'] !== '' ? $res['page_url'] : '—' ), )); } if ($stock_table === '') { wp_send_json_error(array('message' => __('This branch has no stock table named. Type one, or delete and re-create the branch to auto-provision it.', 'ai-eshop-optimizer'))); } if (!aieo_dmm_table_exists($stock_table)) { wp_send_json_error(array('message' => sprintf( __('Table "%s" does not exist in the database.', 'ai-eshop-optimizer'), $stock_table ))); } $data = array( 'slug' => $slug, 'label' => $label, 'stock_table' => $stock_table, 'match_col' => $match_col, 'enabled' => $enabled, 'sort_order' => $sort_order, 'erp_id' => $erp_id, 'match_patterns' => $match_patterns, ); $formats = array('%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s'); if ($id > 0) { $wpdb->update($table, $data, array('id' => $id), $formats, array('%d')); } else { $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE slug = %s", $slug )); if ($existing) { wp_send_json_error(array('message' => __('A branch with that slug already exists.', 'ai-eshop-optimizer'))); } $wpdb->insert($table, $data, $formats); $id = (int) $wpdb->insert_id; } self::sync_branch_artifacts(); wp_send_json_success(array('id' => $id, 'message' => __('Branch saved.', 'ai-eshop-optimizer'))); } private static function sync_branch_artifacts() { if (!class_exists('AIEO_DMM_Branches')) { $f = AIEO_DIR . 'includes/dmm/services/class-aieo-dmm-branches.php'; if (is_readable($f)) { require_once $f; } } if (class_exists('AIEO_DMM_Branches')) { AIEO_DMM_Branches::sync(); } self::seed_worksheet_caps_once(); } public function ajax_save_erp_settings() { AIEO_DMM::verify_ajax_request(); $v = isset($_POST['eshop_invoice_type']) ? trim(sanitize_text_field((string) wp_unslash($_POST['eshop_invoice_type']))) : ''; update_option(AIEO_DMM_Branches::OPT_ESHOP_INVOICE_TYPE, $v, false); wp_send_json_success(array('message' => __('ERP feed settings saved.', 'ai-eshop-optimizer'))); } public function ajax_delete_branch() { AIEO_DMM::verify_ajax_request(); $id = isset($_POST['id']) ? absint(wp_unslash($_POST['id'])) : 0; if ($id <= 0) { wp_send_json_error(array('message' => __('Invalid id.', 'ai-eshop-optimizer'))); } global $wpdb; $wpdb->delete($wpdb->prefix . 'aieo_dmm_branches', array('id' => $id), array('%d')); self::sync_branch_artifacts(); wp_send_json_success(); } public function ajax_preorder_set_status() { AIEO_DMM::verify_ajax_request(); $id = isset($_POST['id']) ? absint(wp_unslash($_POST['id'])) : 0; $status = isset($_POST['status']) ? sanitize_key(wp_unslash($_POST['status'])) : ''; $qty = isset($_POST['qty']) && $_POST['qty'] !== '' ? (int) $_POST['qty'] : null; $result = self::apply_preorder_status($id, $status, $qty, get_current_user_id()); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success($result); } public static function apply_preorder_status($id, $status, $qty, $user_id, array $args = array()) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_supplier_preorders'; if (!aieo_dmm_table_exists($table)) { return new WP_Error('aieo_preorder_table', __('The pre-order table is missing.', 'ai-eshop-optimizer')); } $id = (int) $id; $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE id = %d", $id), ARRAY_A); if (!$row) { return new WP_Error('aieo_preorder_missing', __('That pre-order line no longer exists.', 'ai-eshop-optimizer')); } $now = current_time('mysql', true); $user_id = (int) $user_id; switch ($status) { case 'approved': $patch = array('status' => 'approved', 'approved_by' => $user_id, 'approved_at' => $now); break; case 'ordered': $patch = array('status' => 'ordered', 'ordered_at' => $now); if (empty($row['approved_at'])) { $patch['approved_by'] = $user_id; $patch['approved_at'] = $now; } break; case 'received': $received = ($qty !== null) ? max(0, (int) $qty) : (int) $row['requested_qty']; $patch = array( 'status' => 'received', 'received_at' => $now, 'received_qty' => $received, 'fulfilled' => 1, 'fulfilled_at' => $now, 'fulfilled_qty' => $received, ); break; case 'rejected': $patch = array( 'status' => 'rejected', 'approved_by' => $user_id, 'approved_at' => $now, 'fulfilled' => 1, 'fulfilled_at' => $now, 'fulfilled_qty' => 0, ); break; default: return new WP_Error('aieo_preorder_status', __('Unknown status.', 'ai-eshop-optimizer')); } if ('ordered' === $status && !empty($args['batch_ref'])) { $patch['batch_ref'] = substr((string) $args['batch_ref'], 0, 32); } $wpdb->update($table, $patch, array('id' => $id)); self::log_preorder_event($id, array( 'user_id' => $user_id, 'source' => isset($args['source']) ? (string) $args['source'] : 'queue', 'from_status' => (string) $row['status'], 'to_status' => (string) $patch['status'], 'qty_before' => (int) $row['requested_qty'], 'qty_after' => ('received' === $status) ? (int) $patch['received_qty'] : null, 'note' => trim((isset($args['note']) ? (string) $args['note'] : '') . (isset($patch['batch_ref']) ? ' batch ' . $patch['batch_ref'] : '')), )); return array('id' => $id, 'status' => (string) $patch['status'], 'fulfilled' => (int) ($patch['fulfilled'] ?? (int) $row['fulfilled'])); } public static function log_preorder_event($preorder_id, array $e) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_preorder_log'; if (!function_exists('aieo_dmm_table_exists') || !aieo_dmm_table_exists($table)) { return; } $wpdb->insert($table, array( 'preorder_id' => (int) $preorder_id, 'created_at' => current_time('mysql', true), 'user_id' => isset($e['user_id']) ? (int) $e['user_id'] : (int) get_current_user_id(), 'source' => isset($e['source']) && '' !== $e['source'] ? substr((string) $e['source'], 0, 32) : 'queue', 'from_status' => isset($e['from_status']) && '' !== (string) $e['from_status'] ? substr((string) $e['from_status'], 0, 20) : null, 'to_status' => isset($e['to_status']) && '' !== (string) $e['to_status'] ? substr((string) $e['to_status'], 0, 20) : null, 'qty_before' => (isset($e['qty_before']) && null !== $e['qty_before']) ? (int) $e['qty_before'] : null, 'qty_after' => (isset($e['qty_after']) && null !== $e['qty_after']) ? (int) $e['qty_after'] : null, 'note' => isset($e['note']) && '' !== trim((string) $e['note']) ? substr(trim((string) $e['note']), 0, 255) : null, )); } private static function worksheet_slug_from_report($report_id) { if (0 !== strpos($report_id, 'preorder_worksheet_')) { return ''; } $slug = substr($report_id, strlen('preorder_worksheet_')); return in_array($slug, self::worksheet_slugs(), true) ? $slug : ''; } public function report_save_cell($handled, $ctx, $def) { if (false !== $handled) { return $handled; } $slug = self::worksheet_slug_from_report((string) $ctx['report']); if ('' === $slug) { return $handled; } $column = (string) $ctx['column']; if (!in_array($column, array('existing_stock', 'desired_qty'), true)) { return __('Only the shelf-stock and requested-quantity cells are editable.', 'ai-eshop-optimizer'); } $value = (int) $ctx['value']; if ($value < 0) { return __('Quantities cannot be negative.', 'ai-eshop-optimizer'); } $product_id = (int) $ctx['row_key']; if ($product_id <= 0) { return __('That row names no product.', 'ai-eshop-optimizer'); } $branch = $this->get_branch_by_slug($slug); if (!$branch) { return __('This branch is not enabled, so its worksheet is read-only.', 'ai-eshop-optimizer'); } global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_preorder_worksheet'; if (!aieo_dmm_table_exists($table)) { return __('The worksheet table is missing. Re-run the plugin schema ensure.', 'ai-eshop-optimizer'); } $done = $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (branch_id, product_id, `{$column}`, updated_by, updated_at) VALUES (%d, %d, %d, %d, %s) ON DUPLICATE KEY UPDATE `{$column}` = VALUES(`{$column}`), updated_by = VALUES(updated_by), updated_at = VALUES(updated_at)", (int) $branch['id'], $product_id, $value, (int) get_current_user_id(), current_time('mysql', true) )); return (false === $done) ? __('The database refused that change.', 'ai-eshop-optimizer') : true; } public function report_bulk($result, $ctx, $def) { if (false !== $result) { return $result; } $slug = self::worksheet_slug_from_report((string) $ctx['report']); if ('' === $slug) { return $result; } $action = (string) $ctx['action']; if (!in_array($action, array('submit', 'clear'), true)) { return $result; } $branch = $this->get_branch_by_slug($slug); if (!$branch) { return __('This branch is not enabled, so its worksheet cannot submit requests.', 'ai-eshop-optimizer'); } $ids = array_values(array_unique(array_filter(array_map('intval', (array) $ctx['ids'])))); if (empty($ids)) { return array('updated' => 0, 'skipped' => 0); } return ('clear' === $action) ? $this->worksheet_clear((int) $branch['id'], $ids) : $this->worksheet_submit($branch, $ids, $slug); } private function worksheet_clear($branch_id, array $ids) { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_preorder_worksheet'; if (!aieo_dmm_table_exists($table)) { return array('updated' => 0, 'skipped' => count($ids)); } $in = implode(',', array_map('intval', $ids)); $updated = (int) $wpdb->query($wpdb->prepare( "UPDATE {$table} SET desired_qty = NULL, updated_by = %d, updated_at = %s WHERE branch_id = %d AND product_id IN ({$in}) AND desired_qty IS NOT NULL", (int) get_current_user_id(), current_time('mysql', true), $branch_id )); return array('updated' => $updated, 'skipped' => count($ids) - $updated); } private function worksheet_submit(array $branch, array $ids, $slug) { global $wpdb; $wt = $wpdb->prefix . 'aieo_dmm_preorder_worksheet'; $pt = $wpdb->prefix . 'aieo_dmm_supplier_preorders'; if (!aieo_dmm_table_exists($wt) || !aieo_dmm_table_exists($pt)) { return __('The worksheet or pre-order table is missing. Re-run the plugin schema ensure.', 'ai-eshop-optimizer'); } $branch_id = (int) $branch['id']; $in = implode(',', array_map('intval', $ids)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wt} WHERE branch_id = %d AND product_id IN ({$in}) AND COALESCE(desired_qty, 0) > 0", $branch_id ), ARRAY_A); $rows = is_array($rows) ? $rows : array(); $skipped = count($ids) - count($rows); if (empty($rows)) { return array('updated' => 0, 'skipped' => $skipped); } if (class_exists('AIEO_DMM_Supplier_Orders')) { AIEO_DMM_Supplier_Orders::warm_supplier_memo(array_map('intval', wp_list_pluck($rows, 'product_id'))); } $now = current_time('mysql', true); $user_id = (int) get_current_user_id(); $updated = 0; foreach ($rows as $w) { $product_id = (int) $w['product_id']; $qty = (int) $w['desired_qty']; $existing = (null !== $w['existing_stock']) ? (int) $w['existing_stock'] : null; $open = $wpdb->get_row($wpdb->prepare( "SELECT id, requested_qty FROM {$pt} WHERE product_id = %d AND branch_id = %d AND status = 'requested' AND fulfilled = 0 ORDER BY id DESC LIMIT 1", $product_id, $branch_id ), ARRAY_A); if ($open) { $request_id = (int) $open['id']; $patch = array('requested_qty' => $qty); if (null !== $existing) { $patch['prior_existing'] = $existing; } $wpdb->update($pt, $patch, array('id' => $request_id)); self::log_preorder_event($request_id, array( 'user_id' => $user_id, 'source' => 'worksheet', 'from_status' => 'requested', 'to_status' => 'requested', 'qty_before' => (int) $open['requested_qty'], 'qty_after' => $qty, 'note' => 'worksheet ' . $slug . ': open request updated', )); } else { $supplier_id = class_exists('AIEO_DMM_Supplier_Orders') ? (int) AIEO_DMM_Supplier_Orders::resolve_supplier($product_id) : 0; $brand_id = null; $brand_terms = wp_get_object_terms($product_id, 'product_brand', array('fields' => 'ids')); if (!is_wp_error($brand_terms) && !empty($brand_terms)) { $brand_id = (int) $brand_terms[0]; } $wpdb->insert($pt, array( 'supplier_term_id' => $supplier_id > 0 ? $supplier_id : null, 'brand_term_id' => $brand_id, 'product_id' => $product_id, 'branch_id' => $branch_id, 'scanned_user_id' => $user_id, 'requested_qty' => $qty, 'prior_existing' => $existing, 'fulfilled' => 0, 'status' => 'requested', 'notes' => 'worksheet ' . $slug, 'created_at' => $now, )); $request_id = (int) $wpdb->insert_id; self::log_preorder_event($request_id, array( 'user_id' => $user_id, 'source' => 'worksheet', 'from_status' => null, 'to_status' => 'requested', 'qty_before' => null, 'qty_after' => $qty, 'note' => 'worksheet ' . $slug, )); } $wpdb->update($wt, array( 'desired_qty' => null, 'last_request_id' => $request_id, 'last_submitted_at' => $now, 'updated_by' => $user_id, 'updated_at' => $now, ), array('id' => (int) $w['id'])); $updated++; } return array( 'updated' => $updated, 'skipped' => $skipped, 'message' => sprintf( __('%1$s requests submitted for approval; %2$s lines had no quantity and were skipped.', 'ai-eshop-optimizer'), number_format_i18n($updated), number_format_i18n($skipped) ), ); } public function shortcode_staff_scanner($atts) { if (!is_user_logged_in()) { return '

' . esc_html__('Please log in to use the staff scanner.', 'ai-eshop-optimizer') . '

'; } if (!self::user_can_scan()) { return '

' . esc_html__('This area is for store staff. Ask an administrator for the Rooster Stock Scanner role.', 'ai-eshop-optimizer') . '

'; } $atts = shortcode_atts(array('branch' => '', 'fullscreen' => 'yes'), $atts, 'aieo_dmm_branch_scanner'); $locked = sanitize_key((string) $atts['branch']); $fullscreen = ('no' !== strtolower((string) $atts['fullscreen'])); $branches = $this->fetch_enabled_branches(); if ($locked !== '') { $branches = array_values(array_filter($branches, function ($b) use ($locked) { return (string) $b['slug'] === $locked; })); if (!$branches) { return '

' . esc_html__('This branch is not available for scanning.', 'ai-eshop-optimizer') . '

'; } } $this->enqueue_staff_scanner_assets(); ob_start(); if ($fullscreen) { echo '