*/ private $order_cache = array(); public function __construct() { add_filter('manage_woocommerce_page_wc-orders_columns', array($this, 'add_custom_shop_order_column')); add_action('manage_woocommerce_page_wc-orders_custom_column', array($this, 'add_custom_shop_order_column_content'), 10, 2); } public function add_custom_shop_order_column($columns) { $new_columns = array(); foreach ($columns as $column_name => $column_info) { $new_columns[$column_name] = $column_info; if ($column_name === 'order_total') { $new_columns['to_print_date'] = __('To Print Date', 'studiou-wc-ord-print-statuses'); $new_columns['in_print_date'] = __('In Print Date', 'studiou-wc-ord-print-statuses'); $new_columns['external_order_number'] = __('External Order Number', 'studiou-wc-ord-print-statuses'); } } return $new_columns; } /** * Render cell content. Under HPOS WC passes the WC_Order object as the * second argument; under legacy CPT it passes the post ID. Use the * object directly when WC hands us one (skipping the wc_get_order * lookup entirely), and memoise by ID for the legacy path so we don't * re-resolve the same order for each of the three custom columns. */ public function add_custom_shop_order_column_content($column, $order_or_id) { if ($order_or_id instanceof WC_Abstract_Order) { $order = $order_or_id; } else { $id = (int) $order_or_id; if (!array_key_exists($id, $this->order_cache)) { $this->order_cache[$id] = wc_get_order($id); } $order = $this->order_cache[$id]; } if (!$order) { echo '—'; return; } switch ($column) { case 'to_print_date': echo $this->format_meta_date($order->get_meta('to_print_date')); break; case 'in_print_date': echo $this->format_meta_date($order->get_meta('in_print_date')); break; case 'external_order_number': $value = $order->get_meta('external_ref_ord_no'); echo $value !== '' ? esc_html($value) : '—'; break; } } /** * Format a MySQL datetime meta value for display. * * The stored value (written by current_time('mysql')) is already in WP * local time. Previously this method used strtotime()/date_i18n() which * applied the timezone offset twice on hosts where the PHP server * timezone differs from the WP site timezone (e.g. UTC server + * Europe/Prague site → display drifted by 1–2 hours). * * We now parse the string as WP-local time explicitly, then format via * wp_date() which respects WP timezone. This keeps locale-aware * formatting while eliminating the offset drift. */ private function format_meta_date($value) { if (empty($value)) { return '—'; } $value = trim((string) $value); // Accept both 'YYYY-MM-DD HH:MM:SS' and 'YYYY-MM-DDTHH:MM[:SS]'. $normalised = str_replace('T', ' ', $value); if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $normalised)) { return '—'; } if (strlen($normalised) === 16) { // missing seconds $normalised .= ':00'; } try { $dt = new DateTimeImmutable($normalised, wp_timezone()); } catch (Exception $e) { return '—'; } $fmt = get_option('date_format') . ' ' . get_option('time_format'); return esc_html(wp_date($fmt, $dt->getTimestamp())); } }