Auto-seed new current lot on lot switch to avoid allocation loop

This commit is contained in:
2026-03-29 22:07:16 +02:00
parent 68fb6e138c
commit ef06b4dd90

View File

@@ -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( $closeStmt = $pdo->prepare(
"UPDATE public.stock_lot "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, ':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( $createOpenStmt = $pdo->prepare(
"INSERT INTO public.stock_lot (product_id, lot_number, status, created_at, updated_at) "INSERT INTO public.stock_lot (product_id, lot_number, status, created_at, updated_at)
VALUES (:product_id, NULL, 'open', NOW(), NOW())" 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( function allocate_components_for_line(
PDO $pdo, PDO $pdo,
int $orderId, int $orderId,
@@ -1169,6 +1204,9 @@ function allocate_components_for_line(
]; ];
} }
$savepoint = 'sp_alloc_line_' . $lineId;
$pdo->exec("SAVEPOINT {$savepoint}");
$allocationInsert = $pdo->prepare( $allocationInsert = $pdo->prepare(
"INSERT INTO public.sales_order_line_lot_allocation ( "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 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) { while ($remaining > 0.0000001) {
$guard++; $guard++;
if ($guard > 100) { 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); $current = get_current_lot_balance_for_update($pdo, $productId);
@@ -1196,7 +1240,7 @@ function allocate_components_for_line(
$available = (float) $current['qty_net']; $available = (float) $current['qty_net'];
if ($available <= 0.0000001) { if ($available <= 0.0000001) {
switch_current_lot($pdo, $productId, $lotId); switch_current_lot($pdo, $productId, $lotId, (int) $locations['storage']);
continue; continue;
} }
@@ -1231,6 +1275,8 @@ function allocate_components_for_line(
} }
} }
$pdo->exec("RELEASE SAVEPOINT {$savepoint}");
return [ return [
'allocated' => true, 'allocated' => true,
'reason' => '', 'reason' => '',