diff --git a/order-import.php b/order-import.php index 61f4156..f9727e4 100644 --- a/order-import.php +++ b/order-import.php @@ -994,7 +994,7 @@ function get_current_lot_balance_for_update(PDO $pdo, int $productId): array ]; } -function switch_current_lot(PDO $pdo, int $productId, int $oldCurrentLotId): int +function switch_current_lot(PDO $pdo, int $productId, int $oldCurrentLotId, int $storageLocationId): int { $closeStmt = $pdo->prepare( "UPDATE public.stock_lot @@ -1030,6 +1030,23 @@ function switch_current_lot(PDO $pdo, int $productId, int $oldCurrentLotId): int ':auto_lot_number' => 'AUTO-' . $productId . '-' . (int) $newCurrentLotId, ]); + $balStmt = $pdo->prepare('SELECT qty_net FROM public.v_stock_lot_balance WHERE stock_lot_id = :lot_id'); + $balStmt->execute([':lot_id' => (int) $newCurrentLotId]); + $newCurrentQty = $balStmt->fetchColumn(); + $newCurrentQty = $newCurrentQty === false ? 0.0 : (float) $newCurrentQty; + + // Auto-seed newly promoted current lot so allocation can continue without manual stock-in. + if ($newCurrentQty <= 0.0000001) { + insert_stock_move_in( + $pdo, + $productId, + (int) $newCurrentLotId, + 200.0, + $storageLocationId, + "auto-seed-current-lot:product={$productId}:lot=" . (int) $newCurrentLotId + ); + } + $createOpenStmt = $pdo->prepare( "INSERT INTO public.stock_lot (product_id, lot_number, status, created_at, updated_at) VALUES (:product_id, NULL, 'open', NOW(), NOW())" @@ -1152,6 +1169,24 @@ function reverse_existing_allocations_for_order(PDO $pdo, int $orderId, int $fal ]; } +function has_available_stock_for_product(PDO $pdo, int $productId, float $epsilon = 0.0000001): bool +{ + $stmt = $pdo->prepare( + "SELECT 1 + FROM public.stock_lot sl + JOIN public.v_stock_lot_balance v ON v.stock_lot_id = sl.id + WHERE sl.product_id = :product_id + AND v.qty_net > :epsilon + LIMIT 1" + ); + $stmt->execute([ + ':product_id' => $productId, + ':epsilon' => $epsilon, + ]); + + return $stmt->fetchColumn() !== false; +} + function allocate_components_for_line( PDO $pdo, int $orderId, @@ -1169,6 +1204,9 @@ function allocate_components_for_line( ]; } + $savepoint = 'sp_alloc_line_' . $lineId; + $pdo->exec("SAVEPOINT {$savepoint}"); + $allocationInsert = $pdo->prepare( "INSERT INTO public.sales_order_line_lot_allocation ( sales_order_line_id, product_id, lot_id, qty, allocation_status, stock_move_id, created_at, updated_at @@ -1188,7 +1226,13 @@ function allocate_components_for_line( while ($remaining > 0.0000001) { $guard++; if ($guard > 100) { - throw new RuntimeException("Inventory allocation loop exceeded guard for product {$productId}"); + $pdo->exec("ROLLBACK TO SAVEPOINT {$savepoint}"); + $pdo->exec("RELEASE SAVEPOINT {$savepoint}"); + return [ + 'allocated' => false, + 'reason' => "loop_guard_exceeded:product={$productId}", + 'allocations' => [], + ]; } $current = get_current_lot_balance_for_update($pdo, $productId); @@ -1196,7 +1240,7 @@ function allocate_components_for_line( $available = (float) $current['qty_net']; if ($available <= 0.0000001) { - switch_current_lot($pdo, $productId, $lotId); + switch_current_lot($pdo, $productId, $lotId, (int) $locations['storage']); continue; } @@ -1231,6 +1275,8 @@ function allocate_components_for_line( } } + $pdo->exec("RELEASE SAVEPOINT {$savepoint}"); + return [ 'allocated' => true, 'reason' => '',