88
99use Magento \Sales \Model \Order ;
1010use Magento \Sales \Model \Order \Invoice ;
11+ use Magento \Catalog \Model \Product \Type ;
12+ use Magento \Catalog \Model \Product \Type \AbstractType ;
1113
1214/**
1315 * Checking order status and adjusting order status before saving
@@ -60,7 +62,9 @@ public function check(Order $order)
6062 */
6163 private function checkForCompleteState (Order $ order , ?string $ currentState ): bool
6264 {
63- if ($ currentState === Order::STATE_PROCESSING && !$ order ->canShip ()) {
65+ if ($ currentState === Order::STATE_PROCESSING
66+ && (!$ order ->canShip () || $ this ->areAllItemsFulfilled ($ order ))
67+ ) {
6468 return true ;
6569 }
6670
@@ -96,7 +100,7 @@ private function checkForClosedState(Order $order, ?string $currentState): bool
96100 {
97101 if (in_array ($ currentState , [Order::STATE_PROCESSING , Order::STATE_COMPLETE ])
98102 && !$ order ->canCreditmemo ()
99- && !$ order ->canShip ()
103+ && ( !$ order ->canShip () || $ this -> areAllItemsFulfilled ( $ order ) )
100104 && $ order ->getIsNotVirtual ()
101105 ) {
102106 return true ;
@@ -109,6 +113,49 @@ private function checkForClosedState(Order $order, ?string $currentState): bool
109113 return false ;
110114 }
111115
116+ /**
117+ * Determine whether all shippable items have been fulfilled by shipment, refund, or cancellation.
118+ *
119+ * @param Order $order
120+ * @return bool
121+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
122+ */
123+ private function areAllItemsFulfilled (Order $ order ): bool
124+ {
125+ foreach ($ order ->getAllItems () as $ item ) {
126+ if ($ item ->getIsVirtual () || $ item ->getLockedDoShip ()) {
127+ continue ;
128+ }
129+
130+ // For bundle shipped together, evaluate fulfillment using the parent only
131+ $ parentItem = $ item ->getParentItem ();
132+ if ($ parentItem && $ parentItem ->getProductType () === Type::TYPE_BUNDLE ) {
133+ $ parentProduct = $ parentItem ->getProduct ();
134+ if ($ parentProduct && $ parentProduct ->getShipmentType () == AbstractType::SHIPMENT_TOGETHER ) {
135+ continue ;
136+ }
137+ }
138+
139+ $ subject = $ parentItem && $ parentItem ->getProductType () === Type::TYPE_BUNDLE
140+ && $ parentItem ->getProduct ()
141+ && $ parentItem ->getProduct ()->getShipmentType () == AbstractType::SHIPMENT_TOGETHER
142+ ? $ parentItem
143+ : $ item ;
144+
145+ $ qtyOrdered = (int ) $ subject ->getQtyOrdered ();
146+ $ qtyCanceled = (int ) $ subject ->getQtyCanceled ();
147+ $ qtyShipped = (int ) $ subject ->getQtyShipped ();
148+ $ qtyRefunded = (int ) $ subject ->getQtyRefunded ();
149+
150+ $ openQty = $ qtyOrdered - $ qtyCanceled - $ qtyShipped - $ qtyRefunded ;
151+ if ($ openQty > 0 ) {
152+ return false ;
153+ }
154+ }
155+
156+ return true ;
157+ }
158+
112159 /**
113160 * Check if order can be automatically switched to processing state
114161 *
0 commit comments