|
|
@@ -11,6 +11,21 @@
|
|
|
* line item is still `pending-print` or `in-print`.
|
|
|
* - All other order-status transitions: no automatic item change.
|
|
|
* - Item status changed manually: never propagates to the order.
|
|
|
+ *
|
|
|
+ * One scoped exception to the gate above — the HPOS Orders list "Change status
|
|
|
+ * to Completed" bulk action ONLY: a priority-9 pre-advance moves every still
|
|
|
+ * pending/in-print item on each *non-terminal* selected order to `done-print`
|
|
|
+ * BEFORE WooCommerce flips the status, so the completion guard then passes
|
|
|
+ * instead of reverting. Terminal/already-completed orders are skipped. This does
|
|
|
+ * NOT apply to single-order, programmatic, or import completions. See
|
|
|
+ * bulk_complete_advance_items() and docs/order-done-plan-01.md.
|
|
|
+ *
|
|
|
+ * Belt-and-suspenders: the completion guard itself carries a fallback for the
|
|
|
+ * same bulk entry point (is_bulk_complete_request()). If the priority-9
|
|
|
+ * pre-advance ever fails to run before WooCommerce's status flip, the guard
|
|
|
+ * advances the remaining items on the order it just completed instead of
|
|
|
+ * reverting — priority-independent, and on the same WC_Order instance. Terminal
|
|
|
+ * source statuses are still reverted, preserving the skip above.
|
|
|
*/
|
|
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
@@ -45,6 +60,10 @@ class Order_Item_Status_Manager {
|
|
|
add_action('woocommerce_admin_order_item_values', array($this, 'render_column_value'), 10, 3);
|
|
|
add_action('woocommerce_process_shop_order_meta', array($this, 'save_item_statuses_from_post'));
|
|
|
add_action('woocommerce_order_status_changed', array($this, 'on_order_status_changed'), 5, 4);
|
|
|
+ // Proprietary pre-advance for the HPOS Orders list "Change status to
|
|
|
+ // Completed" bulk action. Priority 9 runs before WooCommerce's own
|
|
|
+ // priority-10 mark_completed handler on the same filter.
|
|
|
+ add_filter('handle_bulk_actions-woocommerce_page_wc-orders', array($this, 'bulk_complete_advance_items'), 9, 3);
|
|
|
}
|
|
|
|
|
|
public static function valid_statuses() {
|
|
|
@@ -324,6 +343,40 @@ class Order_Item_Status_Manager {
|
|
|
if ($check['ready']) {
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
+ // §7 fallback (belt-and-suspenders for the priority-9 pre-advance in
|
|
|
+ // bulk_complete_advance_items()). If this completion is the HPOS
|
|
|
+ // Orders-list "Change status to Completed" bulk action and the order
|
|
|
+ // was NOT previously terminal, advance the remaining items here —
|
|
|
+ // priority-independent and operating on the SAME $order instance WC
|
|
|
+ // just completed, so it survives a future WC change to mark_*
|
|
|
+ // handling and any two-instance object-cache skew. Terminal source
|
|
|
+ // statuses (cancelled/refunded/failed) are still reverted below,
|
|
|
+ // matching the bulk handler's terminal-skip — see the class-level
|
|
|
+ // coupling notes and docs/order-done-plan-01.md §6/§7.
|
|
|
+ if ($this->is_bulk_complete_request()
|
|
|
+ && !in_array($from, Studiou_DB_Manager::TERMINAL_STATUSES, true)) {
|
|
|
+ $advanced = $this->advance_remaining_to_done($order);
|
|
|
+ if ($advanced > 0) {
|
|
|
+ $order->add_order_note(sprintf(
|
|
|
+ /* translators: %d is the number of line items advanced. */
|
|
|
+ _n(
|
|
|
+ '%d line item advanced to "Done Print" before bulk completion.',
|
|
|
+ '%d line items advanced to "Done Print" before bulk completion.',
|
|
|
+ $advanced,
|
|
|
+ 'studiou-wc-ord-print-statuses'
|
|
|
+ ),
|
|
|
+ $advanced
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ UtilsLog::log(sprintf(
|
|
|
+ 'Order #%d completed via bulk action; guard-fallback advanced %d item(s) to done-print (priority-9 pre-advance did not run first).',
|
|
|
+ $order_id,
|
|
|
+ $advanced
|
|
|
+ ));
|
|
|
+ return; // allow completion to stand
|
|
|
+ }
|
|
|
+
|
|
|
self::$inside_self_revert = true;
|
|
|
try {
|
|
|
$order->update_status(
|
|
|
@@ -346,4 +399,148 @@ class Order_Item_Status_Manager {
|
|
|
UtilsLog::log('Order #' . $order_id . ' completion blocked: ' . count($check['blocking']) . ' item(s) not ready');
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * True when the current request is the HPOS Orders-list "Change status to
|
|
|
+ * Completed" bulk action (`page=wc-orders` + the resolved bulk action is
|
|
|
+ * `mark_completed`). Mirrors WP_List_Table::current_action() — the active
|
|
|
+ * action is `action` unless it is the "-1" placeholder, in which case it is
|
|
|
+ * `action2` (the bottom-of-table dropdown).
|
|
|
+ *
|
|
|
+ * Scopes the §7 guard-fallback to exactly the same entry point as
|
|
|
+ * bulk_complete_advance_items(): a single-order edit-screen save posts
|
|
|
+ * `action=edit` (not `mark_completed`), and REST/cron/WP-CLI are not
|
|
|
+ * `is_admin()`, so none of them trip this.
|
|
|
+ */
|
|
|
+ private function is_bulk_complete_request() {
|
|
|
+ if (!is_admin()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!isset($_REQUEST['page']) || $_REQUEST['page'] !== 'wc-orders') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $action = '';
|
|
|
+ if (isset($_REQUEST['action']) && $_REQUEST['action'] !== '-1') {
|
|
|
+ $action = sanitize_text_field(wp_unslash($_REQUEST['action']));
|
|
|
+ } elseif (isset($_REQUEST['action2']) && $_REQUEST['action2'] !== '-1') {
|
|
|
+ $action = sanitize_text_field(wp_unslash($_REQUEST['action2']));
|
|
|
+ }
|
|
|
+ return $action === 'mark_completed';
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ---------------------------------------------------------------- *
|
|
|
+ * Bulk "Change status to Completed" pre-advance (HPOS Orders list)
|
|
|
+ * ---------------------------------------------------------------- */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Proprietary behaviour for the HPOS Orders list "Change status to Completed"
|
|
|
+ * bulk action ONLY: advance every still-pending/in-print product item to
|
|
|
+ * done-print BEFORE WooCommerce flips the order, so the completion guard is
|
|
|
+ * satisfied instead of reverting. Hooked to
|
|
|
+ * `handle_bulk_actions-woocommerce_page_wc-orders` at priority 9 (before WC's
|
|
|
+ * own priority-10 mark_completed handler). Returns the redirect unchanged so
|
|
|
+ * WC still performs the status change.
|
|
|
+ *
|
|
|
+ * Orders already in a terminal status (completed/cancelled/refunded/failed) are
|
|
|
+ * deliberately NOT advanced — see the class-level coupling notes and
|
|
|
+ * docs/order-done-plan-01.md §6:
|
|
|
+ * - already-completed: WC's flip is a no-op, so advancing would silently
|
|
|
+ * mutate items (and add a misleading note) with no transition;
|
|
|
+ * - cancelled/refunded/failed with unresolved items: leaving them unadvanced
|
|
|
+ * lets the existing completion guard revert WC's flip, protecting them from
|
|
|
+ * being completed by an over-broad selection — matching the protocol-import
|
|
|
+ * skip-terminal philosophy.
|
|
|
+ *
|
|
|
+ * Each order is processed inside try/catch: a single failing order must not
|
|
|
+ * abort the whole batch (which would also stop WC's priority-10 handler from
|
|
|
+ * completing ANY order and leave earlier orders with advanced-but-uncompleted
|
|
|
+ * items).
|
|
|
+ *
|
|
|
+ * @param string $redirect_to
|
|
|
+ * @param string $action
|
|
|
+ * @param int[] $ids
|
|
|
+ * @return string The (unchanged) redirect URL.
|
|
|
+ */
|
|
|
+ public function bulk_complete_advance_items($redirect_to, $action, $ids) {
|
|
|
+ if ($action !== 'mark_completed') {
|
|
|
+ return $redirect_to;
|
|
|
+ }
|
|
|
+ if (!current_user_can('edit_shop_orders')) {
|
|
|
+ return $redirect_to; // WC's handler enforces caps; we don't mutate without it.
|
|
|
+ }
|
|
|
+ $ids = array_filter(array_map('intval', (array) $ids));
|
|
|
+ if (empty($ids)) {
|
|
|
+ return $redirect_to;
|
|
|
+ }
|
|
|
+
|
|
|
+ $total_items = 0;
|
|
|
+ $total_orders = 0;
|
|
|
+ $failures = 0;
|
|
|
+
|
|
|
+ foreach ($ids as $id) {
|
|
|
+ try {
|
|
|
+ $order = wc_get_order($id);
|
|
|
+ if (!$order) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // Do not touch terminal/already-completed orders (see docblock).
|
|
|
+ if ($order->has_status(Studiou_DB_Manager::TERMINAL_STATUSES)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $advanced = $this->advance_remaining_to_done($order);
|
|
|
+ if ($advanced > 0) {
|
|
|
+ $order->add_order_note(sprintf(
|
|
|
+ /* translators: %d is the number of line items advanced. */
|
|
|
+ _n(
|
|
|
+ '%d line item advanced to "Done Print" before bulk completion.',
|
|
|
+ '%d line items advanced to "Done Print" before bulk completion.',
|
|
|
+ $advanced,
|
|
|
+ 'studiou-wc-ord-print-statuses'
|
|
|
+ ),
|
|
|
+ $advanced
|
|
|
+ ));
|
|
|
+ $total_items += $advanced;
|
|
|
+ $total_orders++;
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $failures++;
|
|
|
+ UtilsLog::log(sprintf(
|
|
|
+ 'bulk_complete_advance_items: order #%d failed to pre-advance: %s',
|
|
|
+ $id,
|
|
|
+ $e->getMessage()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($total_items > 0) {
|
|
|
+ UtilsLog::message(
|
|
|
+ sprintf(
|
|
|
+ /* translators: 1: item count, 2: order count */
|
|
|
+ __('Auto-advanced %1$d line item(s) to "Done Print" across %2$d order(s) before completion.', 'studiou-wc-ord-print-statuses'),
|
|
|
+ $total_items,
|
|
|
+ $total_orders
|
|
|
+ ),
|
|
|
+ 'success'
|
|
|
+ );
|
|
|
+ UtilsLog::log(sprintf('bulk mark_completed: advanced %d items across %d orders', $total_items, $total_orders));
|
|
|
+ }
|
|
|
+ if ($failures > 0) {
|
|
|
+ UtilsLog::message(
|
|
|
+ sprintf(
|
|
|
+ /* translators: %d is the number of orders that errored during pre-advance. */
|
|
|
+ _n(
|
|
|
+ '%d order could not be pre-advanced and may not complete — check the debug log.',
|
|
|
+ '%d orders could not be pre-advanced and may not complete — check the debug log.',
|
|
|
+ $failures,
|
|
|
+ 'studiou-wc-ord-print-statuses'
|
|
|
+ ),
|
|
|
+ $failures
|
|
|
+ ),
|
|
|
+ 'warning'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return $redirect_to;
|
|
|
+ }
|
|
|
}
|