|
@@ -0,0 +1,492 @@
|
|
|
|
|
+# Implementation Plan — Auto "Done Print" on bulk "Change status to Completed"
|
|
|
|
|
+
|
|
|
|
|
+**File:** `docs/order-done-plan-01.md`
|
|
|
|
|
+**Target version:** 1.5.1
|
|
|
|
|
+**Status:** Draft / not yet implemented
|
|
|
|
|
+**Supersedes:** `docs/order-done-plan-00.md` (kept for history)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 0. Changes from plan-00 (review folded in)
|
|
|
|
|
+
|
|
|
|
|
+This revision incorporates the findings from the code-review of plan-00. Summary of
|
|
|
|
|
+what changed:
|
|
|
|
|
+
|
|
|
|
|
+1. **Per-order failure isolation** — each order is processed inside `try/catch` so one
|
|
|
|
|
+ bad order can no longer fatal the request and strand the whole batch (was: an uncaught
|
|
|
|
|
+ throwable in the loop aborts everything, including WooCommerce's own completion pass).
|
|
|
|
|
+2. **Terminal / already-completed skip** — the advance loop now skips any order already in
|
|
|
|
|
+ `Studiou_DB_Manager::TERMINAL_STATUSES` (`completed`/`cancelled`/`refunded`/`failed`).
|
|
|
|
|
+ This fixes the silent mutation of already-`completed` orders (whose items lazily default
|
|
|
|
|
+ to `pending-print`) and resolves the terminal-order question (Q2) toward the plugin's
|
|
|
|
|
+ established skip-terminal philosophy. See §6 for the precise resulting behaviour,
|
|
|
|
|
+ including one honest residual edge case.
|
|
|
|
|
+3. **Stronger hook-ordering / outcome test** — test §9 #8 now asserts the *end state*
|
|
|
|
|
+ (order `completed`, **no** "completion blocked" notice) rather than only log-line
|
|
|
|
|
+ ordering, so a future WooCommerce regression in the load-bearing assumption is actually
|
|
|
|
|
+ caught.
|
|
|
|
|
+4. **Two-instance/cache dependency documented** — §5 now states explicitly that the
|
|
|
|
|
+ advance and the guard run on two *different* `wc_get_order()` instances and why that is
|
|
|
|
|
+ expected to be coherent (and what would break it).
|
|
|
|
|
+5. **i18n corrected** — the sketch carries `/* translators */` comments on every new
|
|
|
|
|
+ string, and §8 states the real new-string count.
|
|
|
|
|
+6. **Double order hydration acknowledged** — §6 notes the two `wc_get_order()` loads per
|
|
|
|
|
+ order across the two callbacks.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 1. Goal
|
|
|
|
|
+
|
|
|
|
|
+On the **HPOS Orders list** (`wp-admin/admin.php?page=wc-orders`), when the operator runs
|
|
|
|
|
+the **built-in WooCommerce bulk action "Change status to Completed"**
|
|
|
|
|
+(`mark_completed`, Czech "Změnit stav na Dokončeno"), the plugin must — **before** the
|
|
|
|
|
+order status is flipped — advance every still-unresolved product line-item print status to
|
|
|
|
|
+**Done Print** (`done-print`) **for orders that are actually about to transition**.
|
|
|
|
|
+
|
|
|
|
|
+"Unresolved / unset" means any product item currently in `pending-print` or `in-print`.
|
|
|
|
|
+Items already in `done-print` or `skip-print` are left alone. This is exactly the semantics
|
|
|
|
|
+of the existing `Order_Item_Status_Manager::advance_remaining_to_done()` — we reuse it, we
|
|
|
|
|
+do not reimplement it.
|
|
|
|
|
+
|
|
|
|
|
+### Hard scope constraint
|
|
|
|
|
+
|
|
|
|
|
+This auto-advance is **proprietary to this one entry point only**:
|
|
|
|
|
+
|
|
|
|
|
+- the **`mark_completed` bulk action**, and
|
|
|
|
|
+- the **`woocommerce_page_wc-orders` (HPOS) screen**.
|
|
|
|
|
+
|
|
|
|
|
+It must **not** fire for:
|
|
|
|
|
+- single-order completion from the order-edit status dropdown,
|
|
|
|
|
+- programmatic / REST / cron status changes,
|
|
|
|
|
+- the InPrint or Delivered protocol imports (the Delivered import already does its own
|
|
|
|
|
+ advance),
|
|
|
|
|
+- any other bulk action (Prepare to Printing, In Printing, export, trash, etc.),
|
|
|
|
|
+- the legacy CPT orders screen (`edit-shop_order`) — out of scope; operator uses HPOS.
|
|
|
|
|
+
|
|
|
|
|
+Everywhere else, the existing **completion guard** behaviour is preserved: completing an
|
|
|
|
|
+order while items are still pending/in-print is blocked and reverted with an admin notice.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 2. Why this is needed (current behaviour)
|
|
|
|
|
+
|
|
|
|
|
+Today the only coupling on `→ wc-completed` is the **completion guard** in
|
|
|
|
|
+`Order_Item_Status_Manager::on_order_status_changed()` (hooked to
|
|
|
|
|
+`woocommerce_order_status_changed`, priority 5). It does the opposite of what we want here:
|
|
|
|
|
+if any product item is still `pending-print`/`in-print` it **reverts** the order back to its
|
|
|
|
|
+previous status and shows an error notice.
|
|
|
|
|
+
|
|
|
|
|
+So a bulk "mark completed" over orders whose items are still pending currently **fails** for
|
|
|
|
|
+those orders — WooCommerce sets them to `completed`, our guard immediately reverts them, and
|
|
|
|
|
+the operator sees N "completion blocked" notices. The operator's intent in this specific
|
|
|
|
|
+bulk action is the reverse: "I'm done — mark the items done and complete the orders."
|
|
|
|
|
+
|
|
|
|
|
+The fix is to advance the items **before** the status change reaches the guard, so the guard
|
|
|
|
|
+sees a ready order and lets the completion stand.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 3. How WooCommerce dispatches the bulk action
|
|
|
|
|
+
|
|
|
|
|
+WooCommerce routes **all** orders-list bulk actions — including the core `mark_*` ones —
|
|
|
|
|
+through the WordPress filter:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+handle_bulk_actions-woocommerce_page_wc-orders ( $redirect_to, $action, $ids )
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The plugin already proves this: `Bulk_Actions_Manager` registers its custom actions on this
|
|
|
|
|
+exact filter at priority 10 and they work. WooCommerce core's own handler for `mark_*`
|
|
|
|
|
+(which loops the selected IDs and calls `$order->update_status( 'completed', ... )`) is —
|
|
|
|
|
+on the WooCommerce versions in scope — **another callback on the same filter, registered at
|
|
|
|
|
+priority 10**.
|
|
|
|
|
+
|
|
|
|
|
+**Key lever:** a callback registered on the same filter at a **lower priority number runs
|
|
|
|
|
+first**. So a callback at **priority 9** runs *before* WooCommerce changes any status — the
|
|
|
|
|
+exact window we need to advance items.
|
|
|
|
|
+
|
|
|
|
|
+(Priority is what orders callbacks, not registration time. Our managers register at
|
|
|
|
|
+`plugins_loaded` 10; WC registers its list-table handler later in the request during screen
|
|
|
|
|
+setup. Using priority 9 guarantees we still run first regardless of registration order.)
|
|
|
|
|
+
|
|
|
|
|
+> **Load-bearing assumption.** The whole feature depends on WC core's `mark_completed`
|
|
|
|
|
+> handler being a priority-10 *filter callback* that priority 9 pre-empts. If a future WC
|
|
|
|
|
+> version processes `mark_*` *inline* (before applying the filter) the priority-9 window
|
|
|
|
|
+> disappears. This is why §9 #8 asserts the actual outcome on the installed WC, and why §7
|
|
|
|
|
+> documents a fallback.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 4. Chosen approach (Approach A — pre-flip in the bulk filter)
|
|
|
|
|
+
|
|
|
|
|
+Add one new callback on `handle_bulk_actions-woocommerce_page_wc-orders` at **priority 9**
|
|
|
|
|
+that:
|
|
|
|
|
+
|
|
|
|
|
+1. returns `$redirect_to` untouched unless `$action === 'mark_completed'`;
|
|
|
|
|
+2. bails (returns `$redirect_to` untouched) if the user lacks `edit_shop_orders` — let WC's
|
|
|
|
|
+ own handler reject the request; we never mutate without the cap;
|
|
|
|
|
+3. sanitises the ID list (`array_filter(array_map('intval', (array) $ids))`);
|
|
|
|
|
+4. for each ID, inside a `try/catch`: `wc_get_order($id)`, skip falsy, **skip any order
|
|
|
|
|
+ already in a terminal status** (see §6), then call `advance_remaining_to_done($order)`
|
|
|
|
|
+ (reused as-is);
|
|
|
|
|
+5. adds a per-order summary order note when count > 0
|
|
|
|
|
+ ("N line items advanced to Done Print before bulk completion.");
|
|
|
|
|
+6. accumulates totals and emits one `UtilsLog::message(..., 'success')` summary across the
|
|
|
|
|
+ batch (e.g. "Auto-advanced 12 items to Done Print across 4 orders before completion.");
|
|
|
|
|
+ if any order threw, emits one `warning` summarising the failure count;
|
|
|
|
|
+7. **returns `$redirect_to` unchanged** — it does *not* short-circuit. WooCommerce's
|
|
|
|
|
+ priority-10 handler then runs the actual status change, and because every item on the
|
|
|
|
|
+ advanced orders is now `done-print`/`skip-print`, the completion guard at priority 5 sees
|
|
|
|
|
+ a ready order and lets it through silently.
|
|
|
|
|
+
|
|
|
|
|
+No change is needed to the completion guard itself. The guard stays as the safety net for
|
|
|
|
|
+every *other* path — and, by design (§6), for the terminal orders we deliberately do not
|
|
|
|
|
+advance.
|
|
|
|
|
+
|
|
|
|
|
+### Where the code lives — recommendation
|
|
|
|
|
+
|
|
|
|
|
+Put the new hook + handler in **`Order_Item_Status_Manager`** (not `Bulk_Actions_Manager`):
|
|
|
|
|
+
|
|
|
|
|
+- It already owns `advance_remaining_to_done()`, `set_status()`, `get_status()`, the
|
|
|
|
|
+ completion guard, and the recursion-guard flag — all the moving parts of this feature are
|
|
|
|
|
+ in one class, so the author reasons about the guard/advance interaction in one place.
|
|
|
|
|
+- It already hooks WooCommerce actions in its constructor, so adding one
|
|
|
|
|
+ `add_filter(...)` line there is idiomatic.
|
|
|
|
|
+- It avoids wiring a new cross-manager dependency. (It *does* reference
|
|
|
|
|
+ `Studiou_DB_Manager::TERMINAL_STATUSES` — a constant on an always-loaded class — which is
|
|
|
|
|
+ acceptable: `Studiou_DB_Manager` itself already does `new Order_Item_Status_Manager()`, so
|
|
|
|
|
+ the two classes are already mutually aware.)
|
|
|
|
|
+
|
|
|
|
|
+Constructor addition:
|
|
|
|
|
+
|
|
|
|
|
+```php
|
|
|
|
|
+add_filter(
|
|
|
|
|
+ 'handle_bulk_actions-woocommerce_page_wc-orders',
|
|
|
|
|
+ array($this, 'bulk_complete_advance_items'),
|
|
|
|
|
+ 9, // before WooCommerce's own mark_completed handler (priority 10)
|
|
|
|
|
+ 3
|
|
|
|
|
+);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+New method (sketch — final code to match house style):
|
|
|
|
|
+
|
|
|
|
|
+```php
|
|
|
|
|
+/**
|
|
|
|
|
+ * 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. Runs at priority 9 (before WC's 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).
|
|
|
|
|
+ */
|
|
|
|
|
+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;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+(`\Throwable` is available on PHP 7.2+, the plugin's minimum.)
|
|
|
|
|
+
|
|
|
|
|
+### Alternative placement (if preferred)
|
|
|
|
|
+
|
|
|
|
|
+If the team would rather keep *all* bulk-action hooks in `Bulk_Actions_Manager`, the
|
|
|
|
|
+identical logic can live there instead, with the `Order_Item_Status_Manager` instance
|
|
|
|
|
+injected via the constructor. The main plugin (`initialize_managers()`) currently builds
|
|
|
|
|
+`Bulk_Actions_Manager` (line ~89) *before* `Order_Item_Status_Manager` (line ~92), so this
|
|
|
|
|
+would also require reordering those two instantiations and passing the item-status manager
|
|
|
|
|
+into `new Bulk_Actions_Manager($itemStatus)`. Functionally equivalent; more wiring.
|
|
|
|
|
+**Recommendation stays with `Order_Item_Status_Manager`.**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. Interaction with the existing completion guard
|
|
|
|
|
+
|
|
|
|
|
+Confirm during implementation:
|
|
|
|
|
+
|
|
|
|
|
+- The guard runs at `woocommerce_order_status_changed` **priority 5**; it fires *after* WC's
|
|
|
|
|
+ bulk handler calls `update_status('completed')`, i.e. *after* our priority-9 pre-advance.
|
|
|
|
|
+- Sequence for one non-terminal order in the bulk action:
|
|
|
|
|
+ 1. our priority-9 filter advances items → `done-print` and saves each one
|
|
|
|
|
+ (`$item->save()`);
|
|
|
|
|
+ 2. WC's priority-10 filter calls `$order->update_status('completed')`;
|
|
|
|
|
+ 3. that fires `woocommerce_order_status_changed` → guard (priority 5) →
|
|
|
|
|
+ `is_order_ready_for_completion()` now returns `ready` → guard returns without reverting.
|
|
|
|
|
+- The `$inside_self_revert` recursion flag is **not** involved (no revert happens), so no
|
|
|
|
|
+ conflict.
|
|
|
|
|
+
|
|
|
|
|
+### Two `WC_Order` instances — coherency note (review finding)
|
|
|
|
|
+
|
|
|
|
|
+Unlike the Delivered-import path — which advances items and completes on the **same**
|
|
|
|
|
+`$order` object — Approach A inherently involves **two different** `wc_get_order($id)`
|
|
|
|
|
+instances: ours at priority 9 (which advances + saves the item meta) and WooCommerce's own
|
|
|
|
|
+at priority 10 (which calls `update_status` and whose object the guard inspects).
|
|
|
|
|
+
|
|
|
|
|
+This is expected to be coherent because `WC_Order_Item::save()` writes `_print_status` to
|
|
|
|
|
+`woocommerce_order_itemmeta` **and** invalidates that item's meta entry in the WP object
|
|
|
|
|
+cache, so WC's fresh load at priority 10 reads the just-saved `done-print` value. The risk to
|
|
|
|
|
+watch: a persistent object cache / order cache layer that returns a stale item-meta view to
|
|
|
|
|
+the second instance would make the guard still see `pending-print` and revert — re-producing
|
|
|
|
|
+exactly the "completion blocked" notices this feature removes. The §9 #8 test asserts the
|
|
|
|
|
+real outcome precisely to exercise this on the live stack rather than trusting the
|
|
|
|
|
+invalidation in theory.
|
|
|
|
|
+
|
|
|
|
|
+(If a future change ever makes this unreliable, the simplest hardening is to advance and
|
|
|
|
|
+complete in a single pass ourselves — but that is the §7 fallback's territory, not Approach
|
|
|
|
|
+A.)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 6. Edge cases & decisions
|
|
|
|
|
+
|
|
|
|
|
+- **Terminal-status orders selected (completed / cancelled / refunded / failed):**
|
|
|
|
|
+ We **skip the advance** for any order already in `Studiou_DB_Manager::TERMINAL_STATUSES`.
|
|
|
|
|
+ Resulting behaviour:
|
|
|
|
|
+ - **already `completed`** (incl. legacy orders created before 1.5.0, whose items lazily
|
|
|
|
|
+ default to `pending-print`): we do **not** advance them. WC's `update_status('completed')`
|
|
|
|
|
+ is a no-op (no transition), so nothing else happens either. This fixes the silent
|
|
|
|
|
+ item-mutation + misleading note that an unconditional advance would cause on
|
|
|
|
|
+ non-transitioning orders.
|
|
|
|
|
+ - **`cancelled`/`refunded`/`failed` with unresolved items:** we do **not** advance them; WC
|
|
|
|
|
+ core still calls `update_status('completed')`, but the existing completion guard then
|
|
|
|
|
+ reverts the order to its prior status and shows its standard "completion blocked" notice.
|
|
|
|
|
+ Net effect: the order stays terminal — protected from being completed by an over-broad
|
|
|
|
|
+ selection, consistent with how the protocol imports skip `TERMINAL_STATUSES`.
|
|
|
|
|
+ - **Honest residual edge:** a `cancelled`/`refunded`/`failed` order whose product items are
|
|
|
|
|
+ *already* all `done-print`/`skip-print` will pass the guard and *will* be completed by WC
|
|
|
|
|
+ core. The guard only blocks on unresolved items, never on the order's terminal status, and
|
|
|
|
|
+ Approach A cannot remove IDs from WC's own pass. This matches existing 1.5.0 guard
|
|
|
|
|
+ behaviour (it is not a regression introduced here) but is documented so nobody is
|
|
|
|
|
+ surprised. If full terminal protection is required, it needs the §7 fallback or an
|
|
|
|
|
+ ID-filtering approach, not Approach A. Tracked as Q2.
|
|
|
|
|
+- **Empty selection:** handled (returns early).
|
|
|
|
|
+- **Per-order failure isolation:** each order's processing is wrapped in `try/catch`. A
|
|
|
|
|
+ single order that throws (e.g. a DB error during `$item->save()`) is logged and counted as
|
|
|
|
|
+ a failure; the loop continues so the rest of the batch is still pre-advanced and WC's
|
|
|
|
|
+ priority-10 pass still runs. Without this, an uncaught throwable would propagate out of the
|
|
|
|
|
+ filter, fatal the request, leave already-processed orders with advanced-but-uncompleted
|
|
|
|
|
+ items, and complete nothing.
|
|
|
|
|
+- **Capability:** mirror existing pattern (`edit_shop_orders`); bail without mutating if
|
|
|
|
|
+ absent rather than `wp_die()` — WC's handler already enforces caps and we don't want to
|
|
|
|
|
+ change the failure UX of the core action.
|
|
|
|
|
+- **Non-product items** (shipping/fee/coupon): `advance_remaining_to_done()` already skips
|
|
|
|
|
+ anything that isn't a `WC_Order_Item_Product`. No change.
|
|
|
|
|
+- **Performance:** two cost notes, both acceptable and not optimised now:
|
|
|
|
|
+ 1. `advance_remaining_to_done()` saves each changed item individually (`$item->save()`),
|
|
|
|
|
+ consistent with the existing import path — N item-saves for N pending items.
|
|
|
|
|
+ 2. Each selected order is hydrated **twice** per request: once by our priority-9 callback
|
|
|
|
|
+ and once by WC's priority-10 callback. For very large selections this doubles
|
|
|
|
|
+ order-object hydration. Acceptable; flagged for awareness.
|
|
|
|
|
+- **Idempotency:** re-running the bulk action over the same (non-terminal) orders is safe
|
|
|
|
|
+ (already-done items are skipped, count 0, no note).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 7. Fallback approach (only if Approach A proves unreliable)
|
|
|
|
|
+
|
|
|
|
|
+If a future WooCommerce version stops routing `mark_completed` through
|
|
|
|
|
+`handle_bulk_actions-woocommerce_page_wc-orders`, or our priority-9 callback cannot be made
|
|
|
|
|
+to run first, or the two-instance cache coherency in §5 proves unreliable, fall back to
|
|
|
|
|
+**context-aware guard relaxation**:
|
|
|
|
|
+
|
|
|
|
|
+- In `on_order_status_changed()`, when `$to === 'completed'`, detect the bulk-complete
|
|
|
|
|
+ request context — `is_admin()` and the request carries the wc-orders bulk action, e.g.
|
|
|
|
|
+ `($_REQUEST['page'] ?? '') === 'wc-orders'` and the resolved bulk action
|
|
|
|
|
+ (`$_REQUEST['action']` / `action2`) is `mark_completed`.
|
|
|
|
|
+- In that context only, instead of reverting, call `advance_remaining_to_done($order)` and
|
|
|
|
|
+ allow completion. Because this path runs inside the guard on the *same* `$order` object WC
|
|
|
|
|
+ is completing, it also sidesteps the two-instance concern in §5.
|
|
|
|
|
+- Everywhere else, keep the current block-and-revert.
|
|
|
|
|
+
|
|
|
|
|
+This is more coupled (inspects superglobals inside a domain listener) and is documented here
|
|
|
|
|
+only as a contingency. **Verify Approach A on staging first (see §9); prefer it.**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 8. Files to change
|
|
|
|
|
+
|
|
|
|
|
+- `includes/class-order-item-status-manager.php`
|
|
|
|
|
+ - add the `add_filter('handle_bulk_actions-woocommerce_page_wc-orders', ..., 9, 3)` in the
|
|
|
|
|
+ constructor;
|
|
|
|
|
+ - add `bulk_complete_advance_items()` method (with the terminal-skip guard and per-order
|
|
|
|
|
+ `try/catch`).
|
|
|
|
|
+- `studiou-wc-ord-print-statuses.php`
|
|
|
|
|
+ - bump `Version:` header and `STUDIOU_WC_OPS_VERSION` → `1.5.1`.
|
|
|
|
|
+- `README.md`
|
|
|
|
|
+ - extend §6 "Per-order-item print status" coupling notes with the new
|
|
|
|
|
+ bulk-complete-on-wc-orders behaviour (be explicit that it is screen- and action-scoped,
|
|
|
|
|
+ does NOT change single-order or programmatic completion, and skips terminal/completed
|
|
|
|
|
+ orders per §6 of this plan);
|
|
|
|
|
+ - add a `## 1.5.1` changelog entry.
|
|
|
|
|
+- `CLAUDE.md`
|
|
|
|
|
+ - add a "Version 1.5.1" entry under Recent Changes;
|
|
|
|
|
+ - extend the `Order_Item_Status_Manager` architecture bullet with the new bulk-complete
|
|
|
|
|
+ pre-advance hook (priority 9 on `handle_bulk_actions-woocommerce_page_wc-orders`,
|
|
|
|
|
+ scoped to `mark_completed`, terminal-skipping, per-order try/catch);
|
|
|
|
|
+ - add a one-line pointer to this plan in the **Documentation** list:
|
|
|
|
|
+ `- [`docs/order-done-plan-01.md`](docs/order-done-plan-01.md) — Plan for auto "Done Print" on the HPOS bulk "Change status to Completed" action (1.5.1).`
|
|
|
|
|
+- (No JS/CSS change → no asset cache-bust concern. Version bump still required by repo
|
|
|
|
|
+ release convention.)
|
|
|
|
|
+
|
|
|
|
|
+**New translatable strings** (correcting plan-00's undercount — three new source formats,
|
|
|
|
|
+two of them singular/plural pairs):
|
|
|
|
|
+1. per-order note — `_n('%d line item advanced to "Done Print" before bulk completion.', …)`;
|
|
|
|
|
+2. batch success notice — `__('Auto-advanced %1$d line item(s) to "Done Print" across %2$d order(s) before completion.', …)`;
|
|
|
|
|
+3. failure warning notice — `_n('%d order could not be pre-advanced and may not complete — check the debug log.', …)`.
|
|
|
|
|
+
|
|
|
|
|
+Add their English sources and refresh `cs_CZ` (`docs/translations.txt`, `languages/*.po`/`*.mo`)
|
|
|
|
|
+per the existing process. Each `_n()`/`__()` call carries a `/* translators */` comment in
|
|
|
|
|
+the sketch — keep them in the final code (matches the existing `_n()` at
|
|
|
|
|
+`on_order_status_changed()`).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 9. Test plan
|
|
|
|
|
+
|
|
|
|
|
+Manual, on staging with HPOS enabled and `WP_DEBUG`/`WP_DEBUG_LOG` on:
|
|
|
|
|
+
|
|
|
|
|
+1. **Happy path:** create an order with several product items in mixed statuses
|
|
|
|
|
+ (`pending-print`, `in-print`, one `skip-print`). On `wp-admin/admin.php?page=wc-orders`,
|
|
|
|
|
+ select it, run **Change status to Completed**. Expect: order becomes `completed`; the
|
|
|
|
|
+ pending/in items are now `done-print`; the `skip-print` item is untouched; one order note
|
|
|
|
|
+ "N line items advanced to Done Print before bulk completion."; one success admin notice;
|
|
|
|
|
+ **no** "completion blocked" notice.
|
|
|
|
|
+2. **Multi-order batch:** select several orders; verify per-order notes and a single batch
|
|
|
|
|
+ summary notice with correct totals.
|
|
|
|
|
+3. **Already-ready order:** items all `done-print`/`skip-print` → completes with no advance
|
|
|
|
|
+ note (count 0).
|
|
|
|
|
+4. **Already-`completed` / legacy order (Finding 2):** select an order already in
|
|
|
|
|
+ `completed` whose items are still `pending-print` (e.g. completed before 1.5.0, lazy
|
|
|
|
|
+ default). Expect: **no** item mutation, **no** advance note, **no** new transition — the
|
|
|
|
|
+ order is left exactly as it was.
|
|
|
|
|
+5. **Terminal-status order (Finding 4 / Q2):** select a `refunded` (or `cancelled`/`failed`)
|
|
|
|
|
+ order with pending items. Expect: it is **not** advanced and is **not** completed — WC's
|
|
|
|
|
+ flip is reverted by the existing guard and the order stays `refunded`, with the standard
|
|
|
|
|
+ "completion blocked" notice. (Also note the documented residual edge in §6: a terminal
|
|
|
|
|
+ order whose items are already all resolved *will* be completed.)
|
|
|
|
|
+6. **Scope — single order edit screen:** open one order with pending items, set status to
|
|
|
|
|
+ Completed via the order dropdown + Update. Expect the **old** behaviour: blocked +
|
|
|
|
|
+ reverted + error notice (auto-advance must NOT apply here).
|
|
|
|
|
+7. **Scope — other bulk actions:** run "Set Status to In Printing" / "Prepare to Printing
|
|
|
|
|
+ Export" — unchanged; no done-print advance.
|
|
|
|
|
+8. **Scope — programmatic:** trigger a completion via WP-CLI/REST (or a quick snippet) — guard
|
|
|
|
|
+ still blocks pending orders; no auto-advance.
|
|
|
|
|
+9. **Capability:** as a role without `edit_shop_orders`, confirm no item mutation occurs.
|
|
|
|
|
+10. **Hook-order + outcome verification (the load-bearing assumption, Findings 3 & 5):**
|
|
|
|
|
+ on the installed WC version, run the happy path (#1) and assert the **end state**: the
|
|
|
|
|
+ order row shows `Completed`, the items are `done-print`, and **no** "completion blocked"
|
|
|
|
|
+ notice appears. As supporting evidence, with `WP_DEBUG_LOG` log the item statuses at the
|
|
|
|
|
+ top of `bulk_complete_advance_items()` and inside the completion guard and confirm the
|
|
|
|
|
+ advance log line precedes the guard line for the same order in one request. (The
|
|
|
|
|
+ end-state assertion — not just log ordering — is what proves priority 9 actually wins and
|
|
|
|
|
+ that the guard's order instance observed the saved meta. Re-check after any WC upgrade.)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 10. Open questions
|
|
|
|
|
+
|
|
|
|
|
+- **Q1 (placement):** keep the handler in `Order_Item_Status_Manager` (recommended) or move
|
|
|
|
|
+ to `Bulk_Actions_Manager` with dependency injection (requires reordering manager
|
|
|
|
|
+ instantiation)? Default: `Order_Item_Status_Manager`.
|
|
|
|
|
+- **Q2 (terminal orders) — resolved toward skip:** this plan now **skips advancing**
|
|
|
|
|
+ terminal/completed orders (§6). For `cancelled`/`refunded`/`failed` with unresolved items
|
|
|
|
|
+ the existing guard reverts WC's completion, so they stay terminal; the one residual edge
|
|
|
|
|
+ (terminal order with already-resolved items still completes) is documented and would need
|
|
|
|
|
+ the §7 fallback / ID-filtering to close. If the team instead wants the original
|
|
|
|
|
+ "follow core, complete everything" behaviour, remove the `TERMINAL_STATUSES` skip — but
|
|
|
|
|
+ that re-introduces the silent refunded→completed + item-advance risk the review flagged.
|
|
|
|
|
+- **Q3 (legacy CPT screen):** confirm the legacy `edit-shop_order` screen is genuinely out of
|
|
|
|
|
+ scope (operator uses HPOS only). If it must be covered too, mirror the hook on
|
|
|
|
|
+ `handle_bulk_actions-edit-shop_order`.
|