Date: 2026-05-12 (revised after dropping the "independent statuses" requirement) Target plugin version after implementation: 1.5.0 (minor bump — new feature, additive, non-breaking). Status: Analysis / planning document. No code has been written yet.
- Add new status for order item called
order-item-status:
pending-print— order item is waiting for printing process — initialin-print— order item is currently processing for printdone-print— order item was printedskip-print— order item is skipped by some custom / system reason- Add to order detail to order item possibility (combo box) to change order-item-status manually.
- When Order status is going to change to
done, all order items must be!= pending-printand!= in-print.(Previous requirement #3 — "Order status and order-item-status are independent" — was withdrawn by the stakeholder; this analysis assumes order ↔ item coupling is allowed and beneficial. See §3 for the proposed coupling rules.)
done" — clarify what "done" meansWooCommerce has no wc-done status. The plugin already adds wc-to-print and wc-in-print. The natural interpretation is the WC built-in wc-completed.
Proposed revision: "When the order status is going to change to wc-completed".
Orders contain product items but also fee items, shipping items, coupon items, tax items, and refund items. None of these are printed.
Proposed revision: the completion guard inspects only product line items (WC_Order_Item_Product). Other item types are not assigned a print status and never block completion.
pending-print — when is it set?The spec implies new items default to pending-print. Two strategies:
get_status() helper returns 'pending-print' when the meta is missing. Migration of existing orders is unnecessary.pending-print to every new item via woocommerce_new_order_item. Existing orders need a one-time backfill migration.Proposed revision: lazy default. Missing meta = pending-print. This avoids touching every existing order at activation time.
With the "independence" constraint dropped, the design space opens up. The cleanest model is one-way coupling, order → items: changing an order status can propagate to items; changing an item status never propagates back to the order. Within that, we need to decide which order-status transitions propagate and how.
The proposed rules:
| Order status transition | Effect on product items |
|---|---|
→ wc-to-print (Prepare to Printing) |
No automatic change. Items typically already pending-print (lazy default). |
→ wc-in-print (In Printing) |
Items in pending-print advance to in-print. done-print / skip-print items are untouched. |
→ wc-completed |
Gated, not propagating — the transition is blocked if any product item is still pending-print or in-print. Operator (or protocol import) must advance items first. |
→ wc-cancelled / wc-refunded / wc-failed |
No automatic change. Items stay where they are (historical record preserved). |
→ wc-processing / wc-pending / wc-on-hold |
No automatic change. |
| Item status changed manually | Never propagates to order. The operator drives the order status separately. |
Two distinct kinds of coupling are at play:
→ in-print): the order transition drives item state. The listener that handles woocommerce_order_status_changed runs the propagation.→ completed): the order transition requires items to already be in the right state. If not, the transition is reverted. The same listener implements the guard.Both behaviours live in a single hook on woocommerce_order_status_changed. The protocol imports become natural consumers of these rules:
$order->update_status('in-print', ...). The propagation rule advances pending items to in-print automatically. No special-case code in the protocol importer.$order->update_status('completed', ...). The guard would block this if items are still pending-print or in-print. The protocol importer must therefore call advance_remaining_to_done($order) before the status flip.Before implementation begins, the following decisions should be confirmed:
→ in-print (§3): confirm "advance pending-print items to in-print" is the desired behaviour for manual order-status changes from the admin dropdown, bulk actions, and the InPrint Protocol import. Alternative: leave items untouched, require explicit per-item operations.skip-print reason: is there a free-text "reason" field, or just the status? Recommendation: status only in v1.5.0; reason field as a future enhancement.pending-print items, or include everything? Recommendation: include everything, add prod_print_status column. Filtering can happen downstream.wc-completed. Already-completed orders are unaffected even if their items are pending-print (lazy default). Confirm this is acceptable.After applying §2's revisions and §3's coupling rules:
WC_Order_Item_Product) has a print status with one of four values, persisted to per-item meta key _print_status. Missing meta defaults to pending-print.<select> per product row. Saving the order persists changes.wc-in-print advances every pending-print item to in-print. done-print and skip-print items are untouched.wc-completed is blocked if any product item is pending-print or in-print. Blocked transition reverts to the previous status with an admin notice listing the blocking items.wc-in-print; items auto-propagate per rule (3a). No explicit per-item code in the importer.pending-print / in-print item to done-print before setting the order to wc-completed. The guard is satisfied; the operator gets one summary notice.prod_print_status column reflecting each item's current status._print_status (underscore prefix per WC convention for internal meta).WC_Order_Item::update_meta_data() / get_meta().{$wpdb->prefix}woocommerce_order_itemmeta. This table exists under both HPOS and legacy CPT — WC uses the same item-meta storage in both modes.class Order_Item_Status_Manager {
const STATUS_PENDING = 'pending-print';
const STATUS_IN = 'in-print';
const STATUS_DONE = 'done-print';
const STATUS_SKIP = 'skip-print';
const STATUSES_THAT_SATISFY_COMPLETION = array(
self::STATUS_DONE,
self::STATUS_SKIP,
);
public static function labels() {
return array(
self::STATUS_PENDING => __('Pending Print', 'studiou-wc-ord-print-statuses'),
self::STATUS_IN => __('In Print', 'studiou-wc-ord-print-statuses'),
self::STATUS_DONE => __('Done Print', 'studiou-wc-ord-print-statuses'),
self::STATUS_SKIP => __('Skip Print', 'studiou-wc-ord-print-statuses'),
);
}
}
A new Order_Item_Status_Manager class exposes:
| Method | Purpose |
|---|---|
get_status(WC_Order_Item_Product $item): string |
Returns the item's print status; defaults to STATUS_PENDING when the meta is missing. |
set_status(WC_Order_Item_Product $item, string $status, bool $audit = true): bool |
Validates the status, writes meta, optionally appends an order note. |
valid_statuses(): array |
Returns the four constants. |
is_order_ready_for_completion(WC_Order $order): array{ready:bool, blocking:WC_Order_Item_Product[]} |
Used by the completion guard and admin UI. |
advance_pending_to_in(WC_Order $order): int |
Used by the propagation listener on → in-print. Returns count advanced. |
advance_remaining_to_done(WC_Order $order): int |
Used by Delivered Protocol import before completing. Returns count advanced. |
includes/class-order-item-status-manager.phpHooks registered in constructor:
| Hook | Purpose |
|---|---|
woocommerce_admin_order_item_headers |
Inject the "Print Status" column header. |
woocommerce_admin_order_item_values |
Render the <select> per line item. |
woocommerce_process_shop_order_meta |
Read $_POST['print_status'][$item_id], persist via set_status(). Runs after WC's own item processing. |
woocommerce_order_status_changed (priority 5) |
Combined propagation + completion guard (see §8). |
includes/class-db-manager.phpimport_inprint_protocol(): no functional change — propagation happens automatically via the listener when $order->update_status('in-print', ...) fires. Optionally surface the advanced count in the admin notice: "3 line items advanced to In Print."import_delivered_protocol(): before calling $order->update_status('completed', ...), call Order_Item_Status_Manager::advance_remaining_to_done($order) so the guard is satisfied. Surface the advanced count in the import-summary notice.get_orders_for_printing(): extend the SELECT to surface item-level print status. Join {$wpdb->prefix}woocommerce_order_items (the item row) and {$wpdb->prefix}woocommerce_order_itemmeta (the _print_status meta) on order_item_id. Add prod_print_status to the CSV output. Note: the existing query groups by (order, product, variation) — multiple line items in one group would need a deterministic pick (e.g., MIN() again, or join via order_products.order_item_id if the lookup table exposes it).studiou-wc-ord-print-statuses.phpAdd the new manager to load_dependencies() and initialize_managers(), identical to the existing pattern. Bump STUDIOU_WC_OPS_VERSION to 1.5.0.
includes/class-bulk-actions-manager.phpNo code change. Bulk actions "Set Status to Prepare to Printing" and "Set Status to In Printing" already call $order->update_status(...) — the propagation listener handles items automatically when the transition is to in-print. The "Prepare to Printing" path doesn't propagate (per §3).
README.md, CLAUDE.md, docs/translations.txt, languages/studiou-wc-ord-print-statuses-cs_CZ.poA single listener on woocommerce_order_status_changed handles both behaviours. Pseudocode:
public function on_order_status_changed($order_id, $from, $to, $order) {
// Re-entrancy guard: a guard-revert below fires this hook again. Skip
// the recursive invocation.
if (self::$inside_self_revert) {
return;
}
if ($to === 'in-print') {
$advanced = $this->advance_pending_to_in($order);
if ($advanced > 0) {
UtilsLog::log("Order #{$order_id}: advanced {$advanced} item(s) to in-print on order → in-print");
}
return;
}
if ($to === 'completed') {
$check = $this->is_order_ready_for_completion($order);
if ($check['ready']) {
return;
}
self::$inside_self_revert = true;
try {
$order->update_status(
$from,
__('Cannot complete: product line items still pending print. Resolve item statuses first.', 'studiou-wc-ord-print-statuses')
);
} finally {
self::$inside_self_revert = false;
}
UtilsLog::message(
sprintf(
__('Order #%1$d completion blocked — %2$d product items not yet "Done Print" or "Skip Print".', 'studiou-wc-ord-print-statuses'),
$order_id,
count($check['blocking'])
),
'error'
);
}
}
Caveats:
woocommerce_order_status_changed fires after the change is committed. Reverting via update_status is post-hoc — the order shows the reverted state on the next page load. Slightly disorienting but workable, and the admin notice explains it.$inside_self_revert flag prevents infinite recursion when the revert itself fires the hook.woocommerce_admin_process_order_object can validate before save. The post-hoc revert still covers programmatic / API / protocol paths. Belt-and-braces.In the order edit screen's Items meta box, the table grows by one column:
| Item | Cost | Qty | Total | Print Status |
|-----------------------|--------|-----|--------|----------------------|
| Product A (variation) | 100.00 | 2 | 200.00 | [Pending Print ▼] |
| Product B (variation) | 50.00 | 1 | 50.00 | [Done Print ▼] |
| Shipping | — | — | 10.00 | — |
| Coupon: SUMMER10 | -20.00 | — | -20.00 | — |
Non-product items show — and aren't editable.
Optional v1.5.0 sweetener: a one-line summary above the table — "Print readiness: 3 of 4 items ready (1 pending)."
| Scenario | Proposed behaviour |
|---|---|
| Order has 0 product items | Completion allowed (vacuously satisfies the guard). |
| Order has only refunds / shipping / fees | Completion allowed. |
Existing wc-completed order with items in pending-print |
No effect — the guard only fires on the transition into completed, not on stored state. |
| Manual order status flip Pending → Completed (skipping in-print) | Guard fires. Items are still pending-print → blocked. Operator must advance items or use protocol imports. |
Order in wc-in-print reverted to wc-processing then back to wc-in-print |
First revert: no item change (per §3). Second forward: pending items advance again (no-op since they're already in-print). Done items stay done. |
Item is removed from order after being set to done-print |
The meta is deleted along with the item. No orphan record. |
Item status changed after order is completed |
Allowed. Order doesn't revert. |
| Bulk action "Set Status to In Printing" on 100 orders | Each order's pending items propagate to in-print via the listener. |
Multiple wc-in-print ↔ wc-to-print ping-pong |
Each → in-print advances any remaining pending-print items. Items previously moved stay in in-print. |
_print_status meta; get_status() returns 'pending-print'.set_status($item, 'in-print') writes the meta and (with $audit=true) appends an order note.set_status($item, 'foo') rejects and returns false without writing.done-print or skip-print — is_order_ready_for_completion() returns {ready:true}.pending-print or in-print — returns {ready:false, blocking:[…]}.done-print) and one shipping item — guard returns ready.advance_pending_to_in: order with items [pending, in, done, skip] → result has items [in, in, done, skip]. Count = 1.advance_remaining_to_done: order with items [pending, in, done, skip] → result [done, done, done, skip]. Count = 2.done-print, click Update, reload → meta persisted, audit note added.wc-in-print via the admin dropdown → all pending-print items in the order are now in-print. Order note from update_status plus per-item audit notes.pending-print, change order status to wc-completed, click Update → order reverts to previous status (visible after reload), admin notice shows the block reason.wp wc shop_order update <id> --status=completed on an order with pending-print items → status reverts.order_no, run the import → order is in in-print, all the order's pending-print items advanced to in-print via the listener, done-print and skip-print items untouched. Import notice mentions advanced count.order_no, run the import → items advance to done-print first, then order flips to completed. Guard is satisfied. Import notice mentions advanced count.prod_print_status column with per-item values.→ to-print).STUDIOU_WC_OPS_VERSION constant updated accordingly.woocommerce_order_itemmeta table is already created by WC.~12 new translatable strings:
Pending Print, In Print, Done Print, Skip Print.Print Status.— (literal, not translated).Cannot complete: product line items still pending print. Resolve item statuses first.Order #%1$d completion blocked — %2$d product items not yet "Done Print" or "Skip Print".Item #%1$d (%2$s): %3$s → %4$s.%d items advanced to In Print by order-status change., %d items advanced to Done Print by Delivered Protocol import.Print readiness: %1$d of %2$d items ready (%3$d pending).docs/revise-1.5.0.md once the feature lands.docs/feature-order-item-status-analysis.md (this document) → archive or supersede after implementation.skip-print reason field, block-based order edit screen support.Rough sizing for Phase 1:
| Area | Effort |
|---|---|
Order_Item_Status_Manager class (data model + admin UI + combined propagation/guard listener) |
~6–10 hours |
Studiou_DB_Manager integration (Delivered Protocol advance, CSV column with new join) |
~2–3 hours |
| Bootstrap + manager wiring + version bump | ~30 minutes |
| README / CLAUDE.md / translations / .po | ~2 hours |
| Test pass (manual scenarios in §11) | ~3–4 hours |
| Total | ~14–20 hours |
Phase 2 items add ~6–10 hours each depending on scope.
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Listener recursion (propagation → save → propagation, or guard-revert → revert → revert) | Medium | High | Static $inside_self_revert flag; propagation only fires on → in-print not on every save. |
| Existing orders break on first edit because meta is missing | Low | Low | Lazy default handles this — get_status returns pending-print for missing meta. |
| Operator surprise: clicking "Set Status to In Printing" silently advances items | Low | Low | Documented in README; per-item audit notes show what happened. |
| Block-based order edit screen doesn't render the new column | Medium | Low | Documented as a known limitation since 1.4.1; carried forward. |
_print_status meta key collides with another plugin |
Very low | Medium | If collision found, fall back to a namespaced key (_studiou_print_status). |
Delivered Protocol "advance to done" advances items the operator intended to leave in skip-print |
None | — | skip-print already satisfies the completion guard; the advance helper only touches pending-print / in-print. |
Implement Phase 1 as described above, subject to stakeholder confirmation of the §4 open questions — particularly:
→ in-print). The proposed design assumes "yes, advance pending items". This is the workflow-consistent choice, but it does change the behaviour of the existing bulk action "Set Status to In Printing" (previously order-only, now also touches items). If the stakeholder prefers the bulk action to remain strictly order-only, the propagation rule can be limited to specific paths (e.g., only the InPrint Protocol import) — but that re-introduces some of the awkwardness the old §2.5 grappled with.The shape of the design with requirement #3 dropped is materially cleaner than the original analysis: a single listener implements both behaviours, the protocol-import code paths are minimally changed, and the rules can be summarised in one table (§3).
End of analysis.