Complete lot activation and virtual shortage reconciliation
This commit is contained in:
1 parent
400b352522
commit
8f7fb9875a
3 files changed
+276
-34
No files matched your search
@@ -66,10 +66,22 @@ try {
|
||||
if (($updatedLot['status'] ?? '') === 'open') {
|
||||
sync_open_lot_activation_selection($pdo, $productId);
|
||||
$updatedLot['next_active'] = is_open_lot_selected_for_activation($pdo, (int) $updatedLot['id']);
|
||||
} else {
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
|
||||
'Verrechnung nach Pflege der aktiven Charge'
|
||||
);
|
||||
}
|
||||
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
|
||||
$forecast = get_lager_product_forecast($pdo, $productId);
|
||||
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0)), 'forecast' => $forecast]);
|
||||
$currentLot = get_current_lot_snapshot($pdo, $productId);
|
||||
json_response(200, ['ok' => true, 'productId' => $productId, 'lot' => $currentLot, 'balance' => [
|
||||
'qty_net' => $currentLot['qty_net'],
|
||||
'qty_in' => $currentLot['qty_in'],
|
||||
'qty_out' => $currentLot['qty_out'],
|
||||
], 'forecast' => $forecast]);
|
||||
}
|
||||
|
||||
if ($action === 'delete_open_lot') {
|
||||
@@ -79,7 +91,13 @@ try {
|
||||
(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)]);
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
|
||||
'Verrechnung nach Loeschung einer offenen Charge'
|
||||
);
|
||||
json_response(200, ['ok' => true, 'productId' => $productId, 'currentLot' => get_current_lot_snapshot($pdo, $productId), 'availableQty' => get_lager_product_available_qty($pdo, $productId)]);
|
||||
}
|
||||
|
||||
if ($action === 'prepare_lot') {
|
||||
@@ -141,8 +159,15 @@ try {
|
||||
$productId = (int) ($data['productId'] ?? 0);
|
||||
$lotId = (int) ($data['lotId'] ?? 0);
|
||||
select_open_lot_for_activation($pdo, $productId, $lotId);
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
(string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'),
|
||||
'Verrechnung nach Auswahl der naechsten aktiven Charge'
|
||||
);
|
||||
refresh_lager_stock_lot_forecast_for_product($pdo, $productId);
|
||||
json_response(200, ['ok' => true, 'productId' => $productId, 'selectedLotId' => $lotId, 'forecast' => get_lager_product_forecast($pdo, $productId)]);
|
||||
$currentLot = get_current_lot_snapshot($pdo, $productId);
|
||||
json_response(200, ['ok' => true, 'productId' => $productId, 'selectedLotId' => $lotId, 'currentLot' => $currentLot, 'forecast' => get_lager_product_forecast($pdo, $productId)]);
|
||||
}
|
||||
|
||||
json_response(422, ['ok' => false, 'error' => 'Unsupported action']);
|
||||
|
||||
+244
-27
@@ -452,7 +452,7 @@ function update_stock_lot_traceability(
|
||||
]);
|
||||
|
||||
if ($receiptQty > 0 || $adjustmentQty !== 0.0 || $outQty > 0) {
|
||||
book_stock_lot_movements($pdo, (int) $before['product_id'], $lotId, $receiptQty, $adjustmentQty, $outQty);
|
||||
book_stock_lot_movements($pdo, (int) $before['product_id'], $lotId, $receiptQty, $adjustmentQty, $outQty, $changedBy);
|
||||
}
|
||||
|
||||
if ($startedTransaction) {
|
||||
@@ -598,24 +598,6 @@ function promote_open_lot_when_virtual_shortage_is_current(PDO $pdo, int $produc
|
||||
}
|
||||
|
||||
try {
|
||||
$currentStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
WHERE product_id = :product_id
|
||||
AND status = 'virtual_shortage'
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$currentStmt->execute([':product_id' => $productId]);
|
||||
$virtualShortageLotId = $currentStmt->fetchColumn();
|
||||
if ($virtualShortageLotId === false || !is_qualified_open_lot($pdo, $lotId)) {
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$realCurrentStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
@@ -644,6 +626,24 @@ function promote_open_lot_when_virtual_shortage_is_current(PDO $pdo, int $produc
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
WHERE product_id = :product_id
|
||||
AND status = 'virtual_shortage'
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$currentStmt->execute([':product_id' => $productId]);
|
||||
$virtualShortageLotId = $currentStmt->fetchColumn();
|
||||
if (!is_qualified_open_lot($pdo, $lotId)) {
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$lotStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
@@ -676,13 +676,15 @@ function promote_open_lot_when_virtual_shortage_is_current(PDO $pdo, int $produc
|
||||
);
|
||||
$promoteStmt->execute([':lot_id' => $lotId]);
|
||||
|
||||
reconcile_virtual_shortage_allocations(
|
||||
$pdo,
|
||||
(int) $virtualShortageLotId,
|
||||
$lotId,
|
||||
$changedBy,
|
||||
'Automatische Verrechnung bei Aktivierung einer echten Charge'
|
||||
);
|
||||
if ($virtualShortageLotId !== false) {
|
||||
reconcile_virtual_shortage_allocations(
|
||||
$pdo,
|
||||
(int) $virtualShortageLotId,
|
||||
$lotId,
|
||||
$changedBy,
|
||||
'Automatische Verrechnung bei Aktivierung einer echten Charge'
|
||||
);
|
||||
}
|
||||
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
@@ -768,6 +770,156 @@ function reconcile_virtual_shortage_allocations(
|
||||
]);
|
||||
}
|
||||
|
||||
function ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
PDO $pdo,
|
||||
int $productId,
|
||||
string $changedBy,
|
||||
string $reason
|
||||
): void {
|
||||
$startedTransaction = !$pdo->inTransaction();
|
||||
if ($startedTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
$currentStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
WHERE product_id = :product_id
|
||||
AND status = 'current'
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$currentStmt->execute([':product_id' => $productId]);
|
||||
$currentLotId = $currentStmt->fetchColumn();
|
||||
if ($currentLotId === false) {
|
||||
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
|
||||
}
|
||||
|
||||
$balanceStmt = $pdo->prepare(
|
||||
"SELECT COALESCE(qty_net, 0)
|
||||
FROM v_stock_lot_balance
|
||||
WHERE stock_lot_id = :lot_id"
|
||||
);
|
||||
$balanceStmt->execute([':lot_id' => (int) $currentLotId]);
|
||||
$currentQty = (float) ($balanceStmt->fetchColumn() ?: 0);
|
||||
|
||||
if ($currentQty <= 0.0000001) {
|
||||
sync_open_lot_activation_selection($pdo, $productId);
|
||||
|
||||
$selectedStmt = $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 sl.next_active = TRUE
|
||||
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
|
||||
LIMIT 1"
|
||||
);
|
||||
$selectedStmt->execute([':product_id' => $productId]);
|
||||
$nextLotId = $selectedStmt->fetchColumn();
|
||||
|
||||
if ($nextLotId === false) {
|
||||
$countStmt = $pdo->prepare(
|
||||
"SELECT COUNT(*), MIN(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"
|
||||
);
|
||||
$countStmt->execute([':product_id' => $productId]);
|
||||
$candidate = $countStmt->fetch(PDO::FETCH_NUM);
|
||||
if (is_array($candidate) && (int) $candidate[0] === 1) {
|
||||
$nextLotId = $candidate[1];
|
||||
}
|
||||
}
|
||||
|
||||
if ($nextLotId !== false && $nextLotId !== null) {
|
||||
promote_open_lot_when_virtual_shortage_is_current(
|
||||
$pdo,
|
||||
$productId,
|
||||
(int) $nextLotId,
|
||||
$changedBy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for ($iteration = 0; $iteration < 100; $iteration++) {
|
||||
$currentStmt->execute([':product_id' => $productId]);
|
||||
$currentLotId = $currentStmt->fetchColumn();
|
||||
if ($currentLotId === false) {
|
||||
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
|
||||
}
|
||||
|
||||
$balanceStmt->execute([':lot_id' => (int) $currentLotId]);
|
||||
$currentQty = (float) ($balanceStmt->fetchColumn() ?: 0);
|
||||
if ($currentQty <= 0.0000001) {
|
||||
break;
|
||||
}
|
||||
|
||||
$virtualStmt = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
WHERE product_id = :product_id
|
||||
AND status = 'virtual_shortage'
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$virtualStmt->execute([':product_id' => $productId]);
|
||||
$virtualLotId = $virtualStmt->fetchColumn();
|
||||
if ($virtualLotId === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
reconcile_virtual_shortage_allocations(
|
||||
$pdo,
|
||||
(int) $virtualLotId,
|
||||
(int) $currentLotId,
|
||||
$changedBy,
|
||||
$reason
|
||||
);
|
||||
|
||||
$remainingStmt = $pdo->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM sales_order_line_lot_allocation
|
||||
WHERE lot_id = :lot_id
|
||||
AND allocation_status = 'reserved'"
|
||||
);
|
||||
$remainingStmt->execute([':lot_id' => (int) $virtualLotId]);
|
||||
if ((int) $remainingStmt->fetchColumn() === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($iteration >= 100) {
|
||||
throw new RuntimeException("Virtuelle Fehlbestandscharge fuer Produkt {$productId} konnte nicht vollstaendig verrechnet werden");
|
||||
}
|
||||
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
if ($startedTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
function select_open_lot_for_activation(PDO $pdo, int $productId, int $lotId): void
|
||||
{
|
||||
if ($productId <= 0 || $lotId <= 0) {
|
||||
@@ -869,6 +1021,46 @@ function get_stock_lot_balance(PDO $pdo, int $lotId): array
|
||||
];
|
||||
}
|
||||
|
||||
function get_current_lot_snapshot(PDO $pdo, int $productId): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT sl.id, sl.product_id, sl.lot_number, sl.status, sl.next_active,
|
||||
sl.manufacturer_party_id, sl.manufacturer_lot_number,
|
||||
mp.name AS manufacturer_name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
|
||||
COALESCE(v.qty_net, 0) AS qty_net,
|
||||
COALESCE(v.qty_in, 0) AS qty_in,
|
||||
COALESCE(v.qty_out, 0) AS qty_out
|
||||
FROM stock_lot sl
|
||||
LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id
|
||||
LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
|
||||
WHERE sl.product_id = :product_id
|
||||
AND sl.status = 'current'
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':product_id' => $productId]);
|
||||
$row = $stmt->fetch();
|
||||
if (!is_array($row)) {
|
||||
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $row['id'],
|
||||
'product_id' => (int) $row['product_id'],
|
||||
'lot_number' => $row['lot_number'],
|
||||
'status' => (string) $row['status'],
|
||||
'next_active' => (bool) $row['next_active'],
|
||||
'manufacturer_party_id' => $row['manufacturer_party_id'] === null ? null : (int) $row['manufacturer_party_id'],
|
||||
'manufacturer_lot_number' => $row['manufacturer_lot_number'],
|
||||
'manufacturer_name' => $row['manufacturer_name'],
|
||||
'mfg_date' => $row['mfg_date'],
|
||||
'expiry_date' => $row['expiry_date'],
|
||||
'purchase_date' => $row['purchase_date'],
|
||||
'qty_net' => (float) $row['qty_net'],
|
||||
'qty_in' => (float) $row['qty_in'],
|
||||
'qty_out' => (float) $row['qty_out'],
|
||||
];
|
||||
}
|
||||
|
||||
function insert_stock_move_out(
|
||||
PDO $pdo,
|
||||
int $productId,
|
||||
@@ -977,7 +1169,8 @@ function book_stock_lot_movements(
|
||||
int $lotId,
|
||||
float $receiptQty,
|
||||
float $adjustmentQty,
|
||||
float $outQty
|
||||
float $outQty,
|
||||
string $changedBy
|
||||
): void {
|
||||
$locations = get_default_location_ids($pdo);
|
||||
$balanceStmt = $pdo->prepare(
|
||||
@@ -1012,6 +1205,12 @@ function book_stock_lot_movements(
|
||||
insert_stock_move_adjustment($pdo, $productId, $lotId, $correctionQty, $locations['storage'], null, 'Manuelle Bestandskorrektur via Chargenformular');
|
||||
}
|
||||
$balance += $adjustmentQty;
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
$changedBy,
|
||||
'Verrechnung nach manueller Bestandskorrektur'
|
||||
);
|
||||
}
|
||||
|
||||
if ($outQty > $balance) {
|
||||
@@ -1019,6 +1218,12 @@ function book_stock_lot_movements(
|
||||
}
|
||||
if ($outQty > 0) {
|
||||
insert_stock_move_out($pdo, $productId, $lotId, $outQty, $locations['storage'], $locations['dispatch'], 'Manueller Lagerabgang via Chargenformular');
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
$changedBy,
|
||||
'Verrechnung nach manuellem Lagerabgang'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,6 +1626,12 @@ function allocate_components_for_line(
|
||||
throw new RuntimeException("Allokationsschutz ausgelost fuer Produkt {$productId}");
|
||||
}
|
||||
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
'system',
|
||||
"Verrechnung vor Verkaufsallokation: order={$orderId}:line={$lineNo}"
|
||||
);
|
||||
$current = get_current_lot_balance_for_update($pdo, $productId);
|
||||
$lotId = $current['lot_id'];
|
||||
$available = $current['qty_net'];
|
||||
@@ -1442,6 +1653,12 @@ function allocate_components_for_line(
|
||||
(int) $locations['dispatch'],
|
||||
"{$movementSource}:order={$orderId}:line={$lineNo}:product={$productId}"
|
||||
);
|
||||
ensure_current_lot_and_reconcile_virtual_shortage(
|
||||
$pdo,
|
||||
$productId,
|
||||
'system',
|
||||
"Verrechnung nach Verkaufsabgang: order={$orderId}:line={$lineNo}"
|
||||
);
|
||||
$allocationStatus = 'allocated';
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user