Sfoglia il codice sorgente

Merge branch 'refs/heads/1.5.0.studiou-wc-ord-print-statuses'
sync from v1.5.1

Dalibor Votruba 4 settimane fa
parent
commit
20df564480

+ 14 - 1
studiou-wc-ord-print-statuses/CLAUDE.md

@@ -8,7 +8,7 @@ This is a WordPress plugin that extends WooCommerce functionality to manage prin
 
 **Plugin Details:**
 - Text Domain: `studiou-wc-ord-print-statuses`
-- Current Version: 1.5.0
+- Current Version: 1.5.1
 - Requires: WordPress 5.8+, WooCommerce 3.0+, PHP 7.2+
 - Tested with: WordPress 6.9.4, WooCommerce 10.7.0 (HPOS enabled)
 
@@ -26,11 +26,22 @@ User-facing and historical documentation lives at the project root and under `do
 - [`docs/revise-1.4.2.md`](docs/revise-1.4.2.md) — Fresh-eye analytical review of v1.4.2. All in-scope findings are resolved in 1.4.3; per-item status annotations inside.
 - [`docs/revise-1.4.3.md`](docs/revise-1.4.3.md) — Fresh-eye analytical review of v1.4.3. All in-scope findings are resolved in 1.4.4; per-item status annotations inside.
 - [`docs/revise-1.4.4.md`](docs/revise-1.4.4.md) — Fresh-eye analytical review of v1.4.4. Mediums and lows are addressed in 1.4.5; per-item status annotations inside.
+- [`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 (implemented in 1.5.1). Supersedes [`docs/order-done-plan-00.md`](docs/order-done-plan-00.md), kept for history.
 
 When adding new documentation files, place them in `docs/` and add a one-line pointer here.
 
 ## Recent Changes
 
+### Version 1.5.1 (2026-06-15)
+
+**New feature:** auto "Done Print" on the HPOS Orders-list **Change status to Completed** bulk action. See `README.md` §"Per-order-item print status" and `docs/order-done-plan-01.md`.
+
+- **New method:** `Order_Item_Status_Manager::bulk_complete_advance_items()`, hooked to `handle_bulk_actions-woocommerce_page_wc-orders` at **priority 9** (before WooCommerce's own priority-10 `mark_completed` handler on the same filter). For each selected non-terminal order it runs `advance_remaining_to_done()` — advancing every `pending-print`/`in-print` product item to `done-print` — *before* WC flips the status, so the completion guard passes instead of reverting.
+- **Scope:** fires only for the `mark_completed` action on the HPOS Orders screen. Single-order completion (order-edit dropdown), programmatic/REST/cron changes, the protocol imports, and every other bulk action are unchanged — the standard completion guard still applies. The legacy CPT `edit-shop_order` screen is intentionally not hooked.
+- **Terminal-skip:** orders already in `Studiou_DB_Manager::TERMINAL_STATUSES` (`completed`/`cancelled`/`refunded`/`failed`) are not advanced — already-completed orders are left untouched (no silent item mutation), and terminal orders with unresolved items stay blocked by the guard. Residual edge (unchanged from 1.5.0): a terminal order whose items are already all resolved is still completed by WC core, since the guard never blocks on terminal status.
+- **Resilience:** each order is processed in a `try/catch`; a failing order is logged and skipped so the batch (and WC's own priority-10 completion pass) is not aborted. A per-order activity note records the advance; one batch success notice summarises totals; a warning notice reports any per-order failures.
+- **Guard fallback (belt-and-suspenders):** `on_order_status_changed()` carries a `→ completed` fallback for the same bulk entry point, gated by `is_bulk_complete_request()` (`page=wc-orders` + resolved bulk action `mark_completed`). If the priority-9 pre-advance ever fails to run before WC's status flip, the guard advances remaining items on the same `$order` instance instead of reverting — priority-independent, and closes the two-instance object-cache concern. Orders whose *previous* status (`$from`) was terminal are still reverted, preserving the terminal-skip. No new translatable strings (reuses the bulk advance note).
+
 ### Version 1.5.0 (2026-05-12)
 
 **New feature:** per-order-item print status. See `README.md` §"Per-order-item print status" and `docs/feature-order-item-status-analysis.md` for the full design rationale.
@@ -218,6 +229,8 @@ The plugin uses a manager-based architecture where the main plugin class (`Studi
      - Order → `wc-completed`: gated; blocked if any product item is still `pending-print` or `in-print`.
      - All other order transitions: no automatic item change.
    - Exposes `advance_pending_to_in()` and `advance_remaining_to_done()` for protocol imports.
+   - `bulk_complete_advance_items()` (new in 1.5.1) — hooked to `handle_bulk_actions-woocommerce_page_wc-orders` at **priority 9**, before WooCommerce's own priority-10 `mark_completed` handler. Scoped to that one bulk action on the HPOS Orders screen: for each selected non-terminal order it pre-advances remaining items to `done-print` so the completion guard passes. Terminal/already-completed orders are skipped; each order runs in a `try/catch` so one failure doesn't abort the batch.
+   - `is_bulk_complete_request()` (new in 1.5.1) — detects the HPOS `mark_completed` bulk-action request (mirrors `WP_List_Table::current_action()`). Used by the completion guard's belt-and-suspenders fallback: if the priority-9 pre-advance fails to win the ordering race, the guard advances remaining items on the same `$order` it just completed instead of reverting (still reverting orders whose `$from` was terminal).
 
 ### Key Database Operations
 

+ 14 - 1
studiou-wc-ord-print-statuses/README.md

@@ -3,7 +3,7 @@
 WordPress plugin that extends WooCommerce with a dedicated workflow for managing print orders sent to third-party print providers. It adds custom order statuses, tracking fields, CSV export, protocol-based CSV imports, and search/filter capabilities tailored to a print fulfillment pipeline.
 
 - **Plugin slug / text domain:** `studiou-wc-ord-print-statuses`
-- **Current version:** 1.5.0
+- **Current version:** 1.5.1
 - **Author:** Dalibor Votruba — [quadarax.com](https://www.quadarax.com)
 - **License:** GPL v2 or later
 
@@ -97,6 +97,8 @@ The status is editable per item from the **Items** table on the order edit scree
 
 Item-level status changes never propagate back to the order. The operator drives the order status independently — the only constraint is the completion guard.
 
+**Bulk "Change status to Completed" (HPOS Orders list only, 1.5.1+):** when you run WooCommerce's own **Change status to Completed** bulk action on the Orders screen (`wp-admin/admin.php?page=wc-orders`), the plugin first advances every still-`pending-print`/`in-print` item on each selected order to `done-print`, *then* lets WooCommerce complete the order — so the completion guard is satisfied instead of blocking. This is scoped strictly to that one bulk action on that one screen: single-order completion from the order-edit dropdown, programmatic/REST changes, and every other bulk action keep the standard guard behaviour. Orders already in a terminal status (`completed`/`cancelled`/`refunded`/`failed`) are **not** advanced — already-completed orders are left untouched, and terminal orders with unresolved items are still blocked by the guard.
+
 Protocol imports interact with this feature:
 
 - **InPrint Protocol** sets the order to `wc-in-print`; items propagate automatically via the rule above (no special code in the importer).
@@ -252,6 +254,17 @@ Project-internal guidance for AI-assisted development is documented in [`CLAUDE.
 
 ## Changelog
 
+### 1.5.1 — 2026-06-15
+
+**New feature: auto "Done Print" on the bulk "Change status to Completed" action.** Scoped to WooCommerce's own *Change status to Completed* bulk action on the HPOS Orders list (`wp-admin/admin.php?page=wc-orders`) only.
+
+- **Added:** `Order_Item_Status_Manager::bulk_complete_advance_items()`, hooked to `handle_bulk_actions-woocommerce_page_wc-orders` at priority 9 (before WooCommerce's own priority-10 `mark_completed` handler). For each selected non-terminal order it advances every still-`pending-print`/`in-print` product item to `done-print` *before* the status flip, so the completion guard passes instead of reverting.
+- **Scope:** fires only for the `mark_completed` bulk action on the HPOS Orders screen. Single-order completion from the order-edit dropdown, programmatic/REST/cron changes, the protocol imports, and all other bulk actions are unchanged — the standard completion guard still applies.
+- **Terminal orders skipped:** orders already in `completed`/`cancelled`/`refunded`/`failed` are not advanced. Already-completed orders are left untouched (no silent item mutation); terminal orders with unresolved items remain blocked by the guard. Residual edge: a terminal order whose items are *already* all resolved is still completed by WooCommerce core (the guard never blocks on terminal status) — unchanged from 1.5.0.
+- **Resilience:** each order is processed inside `try/catch`, so one failing order is logged and skipped without aborting the batch or WooCommerce's own completion pass. A per-order activity note records the advance; a single batch admin notice summarises totals, and a warning notice reports any failures.
+- **Added (belt-and-suspenders):** a fallback inside the completion guard itself. If the priority-9 pre-advance ever fails to run before WooCommerce's status flip (e.g. a future WC version changes how it processes `mark_*`), the guard detects the same bulk-complete request context and advances the remaining items on the order it just completed — instead of reverting. It is priority-independent and runs on the same order object, and still reverts orders whose *previous* status was terminal, so the terminal-skip behaviour is preserved. Reuses the existing advance note (no new strings).
+- **Doc:** implementation plan at [`docs/order-done-plan-01.md`](docs/order-done-plan-01.md) (§7 documents the fallback).
+
 ### 1.5.0 — 2026-05-12
 
 **New feature: per-order-item print status.** Each product line item now carries its own print status independent of the order's status, with explicit coupling rules between the two.

+ 332 - 0
studiou-wc-ord-print-statuses/docs/order-done-plan-00.md

@@ -0,0 +1,332 @@
+# Implementation Plan — Auto "Done Print" on bulk "Change status to Completed"
+
+**File:** `docs/order-done-plan-00.md`
+**Target version:** 1.5.1
+**Status:** Draft / not yet implemented
+
+---
+
+## 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`).
+
+"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 just
+another callback on the same filter, also 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.)
+
+---
+
+## 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: `wc_get_order($id)`, skip falsy, 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.");
+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 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.
+
+### 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.
+
+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.
+ */
+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 will reject; we don't mutate.
+    }
+    $ids = array_filter(array_map('intval', (array) $ids));
+    if (empty($ids)) {
+        return $redirect_to;
+    }
+
+    $total_items  = 0;
+    $total_orders = 0;
+    foreach ($ids as $id) {
+        $order = wc_get_order($id);
+        if (!$order) {
+            continue;
+        }
+        $advanced = $this->advance_remaining_to_done($order);
+        if ($advanced > 0) {
+            $order->add_order_note(sprintf(
+                _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++;
+        }
+    }
+
+    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));
+    }
+
+    return $redirect_to;
+}
+```
+
+### 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 would then instantiate
+`Order_Item_Status_Manager` first and pass it 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 order in the bulk action:
+  1. our priority-9 filter advances items → `done-print` and saves them;
+  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.
+- Orders already `completed` before the action: `update_status('completed')` is a no-op (no
+  `status_changed`), so the guard never fires — harmless. Our advance still runs and is also
+  effectively a no-op if items are already resolved.
+
+---
+
+## 6. Edge cases & decisions
+
+- **Terminal-status orders selected (cancelled / refunded / failed):** WooCommerce core's
+  `mark_completed` will move them to `completed` regardless (it has no terminal guard for
+  bulk mark-complete). Our pre-advance will advance their items too. This matches the
+  operator's explicit selection + explicit "complete" choice. **Decision: do not special-case
+  terminal statuses here** — note it as expected behaviour. (Contrast: the protocol *imports*
+  deliberately skip `Studiou_DB_Manager::TERMINAL_STATUSES`, because there the order set is
+  data-driven, not an explicit operator click.) If the team wants to protect these, add a
+  `TERMINAL_STATUSES` skip in the loop — flagged as Open Question Q2.
+- **Empty selection:** handled (returns early).
+- **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:** `advance_remaining_to_done()` saves each changed item individually
+  (`$item->save()`), consistent with the existing import path. For very large bulk sets this
+  is N item-saves; acceptable and matches current code. Not optimising now.
+- **Idempotency:** re-running the bulk action over the same 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, 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.
+- 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.
+- `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
+    and does NOT change single-order or programmatic completion);
+  - 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`).
+- (No JS/CSS change → no asset cache-bust concern. Version bump still required by repo
+  release convention.)
+
+Add a one-line pointer to this plan in CLAUDE.md's **Documentation** list:
+`- [`docs/order-done-plan-00.md`](docs/order-done-plan-00.md) — Plan for auto "Done Print" on the HPOS bulk "Change status to Completed" action (1.5.1).`
+
+No new translation strings beyond the two new notice/note formats — add their English
+sources and refresh `cs_CZ` (`docs/translations.txt`, `.po`/`.mo`) per the existing process.
+
+---
+
+## 9. Test plan
+
+Manual, on staging with HPOS enabled and `WP_DEBUG` 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. **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).
+5. **Scope — other bulk actions:** run "Set Status to In Printing" / "Prepare to Printing
+   Export" — unchanged; no done-print advance.
+6. **Scope — programmatic:** trigger a completion via WP-CLI/REST (or a quick snippet) — guard
+   still blocks pending orders; no auto-advance.
+7. **Capability:** as a role without `edit_shop_orders`, confirm no item mutation occurs.
+8. **Hook-order verification (the load-bearing assumption):** with `WP_DEBUG_LOG`, log the
+   item statuses at the top of `bulk_complete_advance_items()` and inside the completion
+   guard; confirm the advance log line precedes the guard line for the same order in one
+   request. This proves priority 9 runs before WC's status flip on the installed WC version.
+
+---
+
+## 10. Open questions
+
+- **Q1 (placement):** keep the handler in `Order_Item_Status_Manager` (recommended) or move
+  to `Bulk_Actions_Manager` with dependency injection? Default: `Order_Item_Status_Manager`.
+- **Q2 (terminal orders):** should cancelled/refunded/failed orders selected in the bulk
+  action be skipped (not advanced, not completed) like the protocol imports do, or follow
+  WooCommerce core and complete them? Default: follow core (no skip), since the operator
+  explicitly chose them.
+- **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`.

+ 492 - 0
studiou-wc-ord-print-statuses/docs/order-done-plan-01.md

@@ -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`.

+ 11 - 1
studiou-wc-ord-print-statuses/docs/translations.txt

@@ -1,5 +1,5 @@
 Translations: all user-facing strings (English → Czech).
-Last reviewed: v1.5.0 (2026-05-12).
+Last reviewed: v1.5.1 (2026-06-15).
 
 Note: After editing the .po file, regenerate the .mo binary with one of:
   wp i18n make-mo languages/
@@ -109,3 +109,13 @@ Order #%1$d completion blocked — %2$d product items not yet "Done Print" or "S
 # Delivered Protocol summary notice (across all orders in one import)
 %d line item advanced to "Done Print" to satisfy completion guard.                          %d položka přesunuta na "Vytištěno" pro splnění podmínky dokončení.
 %d line items advanced to "Done Print" to satisfy completion guard.                         %d položek přesunuto na "Vytištěno" pro splnění podmínky dokončení.
+
+# New in 1.5.1 — auto "Done Print" on the bulk "Change status to Completed" action
+# Per-order advance note
+%d line item advanced to "Done Print" before bulk completion.                               %d položka přesunuta na "Vytištěno" před hromadným dokončením.
+%d line items advanced to "Done Print" before bulk completion.                              %d položek přesunuto na "Vytištěno" před hromadným dokončením.
+# Batch success summary notice
+Auto-advanced %1$d line item(s) to "Done Print" across %2$d order(s) before completion.      Automaticky přesunuto %1$d položek na "Vytištěno" v %2$d objednávkách před dokončením.
+# Per-order failure warning notice
+%d order could not be pre-advanced and may not complete — check the debug log.               %d objednávku se nepodařilo předem připravit a nemusí být dokončena — zkontrolujte ladicí log.
+%d orders could not be pre-advanced and may not complete — check the debug log.              %d objednávek se nepodařilo předem připravit a nemusí být dokončeno — zkontrolujte ladicí log.

+ 197 - 0
studiou-wc-ord-print-statuses/includes/class-order-item-status-manager.php

@@ -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;
+    }
 }

BIN
studiou-wc-ord-print-statuses/languages/studiou-wc-ord-print-statuses-cs_CZ.mo


+ 21 - 1
studiou-wc-ord-print-statuses/languages/studiou-wc-ord-print-statuses-cs_CZ.po

@@ -2,7 +2,7 @@
 # This file is distributed under the GPL2 license.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Order Print Statuses 1.5.0\n"
+"Project-Id-Version: QDR - Studiou WC Order Print Statuses 1.5.1\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com/plugins/studiou-wc-ord-print-statuses\n"
 "POT-Creation-Date: 2026-05-12 00:00:00+00:00\n"
 "PO-Revision-Date: 2026-05-12 00:00:00+00:00\n"
@@ -248,3 +248,23 @@ msgid_plural "%d line items advanced to \"Done Print\" to satisfy completion gua
 msgstr[0] "%d položka přesunuta na \"Vytištěno\" pro splnění podmínky dokončení."
 msgstr[1] "%d položky přesunuty na \"Vytištěno\" pro splnění podmínky dokončení."
 msgstr[2] "%d položek přesunuto na \"Vytištěno\" pro splnění podmínky dokončení."
+
+# --- Bulk "Change status to Completed" pre-advance (new in 1.5.1) ---
+
+# Per-order advance note
+msgid "%d line item advanced to \"Done Print\" before bulk completion."
+msgid_plural "%d line items advanced to \"Done Print\" before bulk completion."
+msgstr[0] "%d položka přesunuta na \"Vytištěno\" před hromadným dokončením."
+msgstr[1] "%d položky přesunuty na \"Vytištěno\" před hromadným dokončením."
+msgstr[2] "%d položek přesunuto na \"Vytištěno\" před hromadným dokončením."
+
+# Batch success summary notice
+msgid "Auto-advanced %1$d line item(s) to \"Done Print\" across %2$d order(s) before completion."
+msgstr "Automaticky přesunuto %1$d položek na \"Vytištěno\" v %2$d objednávkách před dokončením."
+
+# Per-order failure warning notice
+msgid "%d order could not be pre-advanced and may not complete — check the debug log."
+msgid_plural "%d orders could not be pre-advanced and may not complete — check the debug log."
+msgstr[0] "%d objednávku se nepodařilo předem připravit a nemusí být dokončena — zkontrolujte ladicí log."
+msgstr[1] "%d objednávky se nepodařilo předem připravit a nemusí být dokončeny — zkontrolujte ladicí log."
+msgstr[2] "%d objednávek se nepodařilo předem připravit a nemusí být dokončeno — zkontrolujte ladicí log."

+ 2 - 2
studiou-wc-ord-print-statuses/studiou-wc-ord-print-statuses.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Order Print Statuses
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-ord-print-statuses
  * Description: Adds custom order statuses (wc-to-print, wc-in-print), bulk actions, and status change timestamps to WooCommerce for print orders to third party providers
- * Version: 1.5.0
+ * Version: 1.5.1
  * Requires at least: 5.8
  * Tested up to: 6.9.4
  * Requires PHP: 7.2
@@ -19,7 +19,7 @@
 
 defined('ABSPATH') || exit;
 
-define('STUDIOU_WC_OPS_VERSION', '1.5.0');
+define('STUDIOU_WC_OPS_VERSION', '1.5.1');
 define('STUDIOU_WC_OPS_FILE', __FILE__);
 
 // Declare HPOS (High-Performance Order Storage) compatibility.