Correct independent stock lot activation flow

This commit is contained in:
gitea_admin committed 2026-09-08 14:38:54 +02:00
1 parent 9eb9cd6e0c
commit 3f15756596
5 files changed
+324 -48

No files matched your search

+30 -11
View File
@@ -74,6 +74,7 @@ try {
(int) ($data['lotId'] ?? 0),
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown')
);
sync_open_lot_activation_selection($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
}
@@ -81,8 +82,22 @@ try {
$productId = (int) ($data['productId'] ?? 0);
$lotNumber = trim((string) ($data['lotNumber'] ?? ''));
$lotId = (int) ($data['lotId'] ?? 0);
if ($lotNumber === '' && $lotId <= 0) {
throw new InvalidArgumentException('Naurua-Chargennummer fehlt');
$manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
$manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId;
$manufacturerLotNumber = trim((string) ($data['manufacturerLotNumber'] ?? ''));
$mfgDate = trim((string) ($data['mfgDate'] ?? ''));
$expiryDate = trim((string) ($data['expiryDate'] ?? ''));
$purchaseDate = trim((string) ($data['purchaseDate'] ?? ''));
$receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0;
$targetQty = isset($data['targetQty']) && $data['targetQty'] !== '' ? (float) $data['targetQty'] : null;
if ($lotNumber === '' || $manufacturerPartyId === null || $manufacturerLotNumber === '' || $mfgDate === '' || $expiryDate === '' || $purchaseDate === '') {
throw new InvalidArgumentException('Alle Chargenfelder sind Pflichtfelder');
}
if ($lotId <= 0 && $receiptQty <= 0) {
throw new InvalidArgumentException('Eingangsmenge muss groesser als 0 sein');
}
if ($lotId > 0 && $targetQty !== null && $targetQty <= 0) {
throw new InvalidArgumentException('Menge muss groesser als 0 sein');
}
if ($lotId > 0) {
$lotStmt = $pdo->prepare(
@@ -97,20 +112,16 @@ try {
throw new InvalidArgumentException('Offene Charge nicht gefunden');
}
} else {
$lotId = get_or_create_open_lot($pdo, $productId);
$lotId = create_open_lot($pdo, $productId);
}
$manufacturerPartyId = $data['manufacturerPartyId'] ?? null;
$manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId;
$receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0;
$targetQty = isset($data['targetQty']) && $data['targetQty'] !== '' ? (float) $data['targetQty'] : null;
$preparedLot = update_stock_lot_traceability(
$pdo,
$lotId,
$manufacturerPartyId,
isset($data['manufacturerLotNumber']) ? (string) $data['manufacturerLotNumber'] : null,
isset($data['mfgDate']) ? (string) $data['mfgDate'] : null,
isset($data['expiryDate']) ? (string) $data['expiryDate'] : null,
isset($data['purchaseDate']) ? (string) $data['purchaseDate'] : null,
$manufacturerLotNumber,
$mfgDate,
$expiryDate,
$purchaseDate,
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
$receiptQty,
0.0,
@@ -119,11 +130,19 @@ try {
$targetQty,
$lotId > 0 && $lotNumber === ''
);
sync_open_lot_activation_selection($pdo, $productId);
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
$forecast = get_lager_product_forecast($pdo, $productId);
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $preparedLot, 'balance' => get_stock_lot_balance($pdo, $lotId), 'availableQty' => get_lager_product_available_qty($pdo, $productId), 'forecast' => $forecast]);
}
if ($action === 'select_open_lot') {
$productId = (int) ($data['productId'] ?? 0);
$lotId = (int) ($data['lotId'] ?? 0);
select_open_lot_for_activation($pdo, $productId, $lotId);
json_response(200, ['ok' => true, 'productId' => $productId, 'selectedLotId' => $lotId]);
}
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
} catch (JsonException) {
json_response(400, ['ok' => false, 'error' => 'Invalid JSON payload']);
+145 -19
View File
@@ -205,6 +205,7 @@ function get_lager_product_overview(PDO $pdo): array
sl.mfg_date,
sl.expiry_date,
sl.purchase_date,
sl.next_active,
COALESCE(v.qty_in, 0) AS qty_in,
COALESCE(v.qty_net, 0) AS qty_net
FROM stock_lot sl
@@ -223,9 +224,10 @@ function get_lager_product_overview(PDO $pdo): array
'manufacturer_name' => $openLot['manufacturer_name'],
'mfg_date' => $openLot['mfg_date'],
'expiry_date' => $openLot['expiry_date'],
'purchase_date' => $openLot['purchase_date'],
'qty_in' => (float) $openLot['qty_net'],
'qty_net' => (float) $openLot['qty_net'],
'purchase_date' => $openLot['purchase_date'],
'qty_in' => (float) $openLot['qty_net'],
'qty_net' => (float) $openLot['qty_net'],
'next_active' => (bool) $openLot['next_active'],
];
}
@@ -466,6 +468,145 @@ function update_stock_lot_traceability(
}
}
function is_qualified_open_lot(PDO $pdo, int $lotId): bool
{
$stmt = $pdo->prepare(
"SELECT EXISTS (
SELECT 1
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.id = :lot_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
)"
);
$stmt->execute([':lot_id' => $lotId]);
return (bool) $stmt->fetchColumn();
}
function sync_open_lot_activation_selection(PDO $pdo, int $productId): void
{
$qualifiedStmt = $pdo->prepare(
"SELECT sl.id
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = :product_id
AND sl.status = 'open'
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
ORDER BY sl.id
FOR UPDATE"
);
$qualifiedStmt->execute([':product_id' => $productId]);
$qualifiedIds = array_map('intval', $qualifiedStmt->fetchAll(PDO::FETCH_COLUMN));
if (count($qualifiedIds) === 1) {
$selectedId = $qualifiedIds[0];
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
$selectStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = TRUE, updated_at = NOW()
WHERE id = :lot_id"
);
$selectStmt->execute([':lot_id' => $selectedId]);
return;
}
if ($qualifiedIds === []) {
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
return;
}
$selectedStmt = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'open'
AND next_active = TRUE
LIMIT 1
FOR UPDATE"
);
$selectedStmt->execute([':product_id' => $productId]);
$selectedId = $selectedStmt->fetchColumn();
if ($selectedId !== false && in_array((int) $selectedId, $qualifiedIds, true)) {
return;
}
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
}
function select_open_lot_for_activation(PDO $pdo, int $productId, int $lotId): void
{
if ($productId <= 0 || $lotId <= 0 || !is_qualified_open_lot($pdo, $lotId)) {
throw new InvalidArgumentException('Nur eine qualifizierte offene Charge kann ausgewaehlt werden');
}
$lotStmt = $pdo->prepare(
"SELECT product_id, status
FROM stock_lot
WHERE id = :lot_id
FOR UPDATE"
);
$lotStmt->execute([':lot_id' => $lotId]);
$lot = $lotStmt->fetch();
if (!is_array($lot) || (int) $lot['product_id'] !== $productId || (string) $lot['status'] !== 'open') {
throw new InvalidArgumentException('Offene Charge nicht gefunden');
}
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$clearStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = FALSE, updated_at = NOW()
WHERE product_id = :product_id AND status = 'open'"
);
$clearStmt->execute([':product_id' => $productId]);
$selectStmt = $pdo->prepare(
"UPDATE stock_lot
SET next_active = TRUE, updated_at = NOW()
WHERE id = :lot_id"
);
$selectStmt->execute([':lot_id' => $lotId]);
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function get_current_lot_balance_for_update(PDO $pdo, int $productId): array
{
$stmt = $pdo->prepare(
@@ -820,23 +961,8 @@ function get_or_create_virtual_shortage_lot(PDO $pdo, int $productId): int
return (int) $newLotId;
}
function get_or_create_open_lot(PDO $pdo, int $productId): int
function create_open_lot(PDO $pdo, int $productId): int
{
$select = $pdo->prepare(
"SELECT id
FROM stock_lot
WHERE product_id = :product_id
AND status = 'open'
ORDER BY id
LIMIT 1
FOR UPDATE"
);
$select->execute([':product_id' => $productId]);
$lotId = $select->fetchColumn();
if ($lotId !== false) {
return (int) $lotId;
}
$insert = $pdo->prepare(
"INSERT INTO stock_lot (product_id, status, created_at, updated_at)
VALUES (:product_id, 'open', NOW(), NOW())