diff --git a/modules/erp/lager/api/lot.php b/modules/erp/lager/api/lot.php index b1351af..1ff552f 100644 --- a/modules/erp/lager/api/lot.php +++ b/modules/erp/lager/api/lot.php @@ -46,6 +46,9 @@ try { $manufacturerPartyId = $manufacturerPartyId === null || $manufacturerPartyId === '' ? null : (int) $manufacturerPartyId; + $receiptQty = isset($data['receiptQty']) && $data['receiptQty'] !== '' ? (float) $data['receiptQty'] : 0.0; + $adjustmentQty = isset($data['adjustmentQty']) && $data['adjustmentQty'] !== '' ? (float) $data['adjustmentQty'] : 0.0; + $outQty = isset($data['outQty']) && $data['outQty'] !== '' ? (float) $data['outQty'] : 0.0; $updatedLot = update_stock_lot_traceability( $pdo, (int) ($data['lotId'] ?? 0), @@ -54,9 +57,12 @@ try { isset($data['mfgDate']) ? (string) $data['mfgDate'] : null, isset($data['expiryDate']) ? (string) $data['expiryDate'] : null, isset($data['purchaseDate']) ? (string) $data['purchaseDate'] : null, - (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown') + (string) ($currentUser['username'] ?? $currentUser['email'] ?? 'unknown'), + $receiptQty, + $adjustmentQty, + $outQty ); - json_response(200, ['ok' => true, 'lot' => $updatedLot]); + json_response(200, ['ok' => true, 'lot' => $updatedLot, 'balance' => get_stock_lot_balance($pdo, (int) ($data['lotId'] ?? 0))]); } json_response(422, ['ok' => false, 'error' => 'Unsupported action']); diff --git a/modules/erp/lager/service.php b/modules/erp/lager/service.php index b89894d..c88f836 100644 --- a/modules/erp/lager/service.php +++ b/modules/erp/lager/service.php @@ -192,7 +192,10 @@ function update_stock_lot_traceability( ?string $mfgDate, ?string $expiryDate, ?string $purchaseDate, - string $changedBy + string $changedBy, + float $receiptQty = 0.0, + float $adjustmentQty = 0.0, + float $outQty = 0.0 ): array { if ($lotId <= 0) { throw new InvalidArgumentException('Chargen-ID fehlt'); @@ -203,6 +206,16 @@ function update_stock_lot_traceability( $manufacturerLotNumber = null; } + if (!is_finite($receiptQty) || !is_finite($adjustmentQty) || !is_finite($outQty)) { + throw new InvalidArgumentException('Ungueltige Bewegungsmenge'); + } + if ($receiptQty < 0 || $outQty < 0) { + throw new InvalidArgumentException('Zugang und Abgang duerfen nicht negativ sein'); + } + if ($adjustmentQty !== 0.0 && abs($adjustmentQty) < 0.0001) { + $adjustmentQty = 0.0; + } + $dates = [ 'mfg_date' => $mfgDate, 'expiry_date' => $expiryDate, @@ -261,6 +274,9 @@ function update_stock_lot_traceability( if ($manufacturerPartyId !== null && $manufacturerLotNumber === null && (string) $before['status'] !== 'open') { throw new InvalidArgumentException('Hersteller-Chargennummer fehlt'); } + if (($receiptQty > 0 || $adjustmentQty !== 0.0 || $outQty > 0) && (string) $before['status'] !== 'current') { + throw new InvalidArgumentException('Lagerbewegungen koennen nur auf der aktiven Charge gebucht werden'); + } $updateStmt = $pdo->prepare( "UPDATE stock_lot @@ -291,6 +307,10 @@ function update_stock_lot_traceability( 'module' => 'erp.lager', ]); + if ($receiptQty > 0 || $adjustmentQty !== 0.0 || $outQty > 0) { + book_stock_lot_movements($pdo, (int) $before['product_id'], $lotId, $receiptQty, $adjustmentQty, $outQty); + } + if ($startedTransaction) { $pdo->commit(); } @@ -333,6 +353,30 @@ function get_current_lot_balance_for_update(PDO $pdo, int $productId): array ]; } +function get_stock_lot_balance(PDO $pdo, int $lotId): array +{ + $stmt = $pdo->prepare( + "SELECT + 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 v_stock_lot_balance v ON v.stock_lot_id = sl.id + WHERE sl.id = :lot_id" + ); + $stmt->execute([':lot_id' => $lotId]); + $row = $stmt->fetch(); + if (!is_array($row)) { + throw new RuntimeException('Charge nicht gefunden'); + } + + return [ + '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, @@ -401,6 +445,91 @@ function insert_stock_move_in( return (int) $id; } +function insert_stock_move_adjustment( + PDO $pdo, + int $productId, + int $lotId, + float $qty, + ?int $fromLocationId, + ?int $toLocationId, + string $note +): int { + $stmt = $pdo->prepare( + "INSERT INTO stock_move ( + product_id, lot_id, from_location_id, to_location_id, qty, move_type, move_date, note, created_at, updated_at + ) VALUES ( + :product_id, :lot_id, :from_location_id, :to_location_id, :qty, 'adjustment', NOW(), :note, NOW(), NOW() + ) + RETURNING id" + ); + $stmt->execute([ + ':product_id' => $productId, + ':lot_id' => $lotId, + ':from_location_id' => $fromLocationId, + ':to_location_id' => $toLocationId, + ':qty' => $qty, + ':note' => $note, + ]); + + $id = $stmt->fetchColumn(); + if ($id === false) { + throw new RuntimeException('Konnte Bestandskorrektur nicht schreiben'); + } + + return (int) $id; +} + +function book_stock_lot_movements( + PDO $pdo, + int $productId, + int $lotId, + float $receiptQty, + float $adjustmentQty, + float $outQty +): void { + $locations = get_default_location_ids($pdo); + $balanceStmt = $pdo->prepare( + "SELECT COALESCE( + SUM(CASE + WHEN move_type = 'in' THEN qty + WHEN move_type = 'out' THEN -qty + WHEN move_type = 'adjustment' AND to_location_id IS NOT NULL AND from_location_id IS NULL THEN qty + WHEN move_type = 'adjustment' AND from_location_id IS NOT NULL AND to_location_id IS NULL THEN -qty + ELSE 0 + END), 0 + ) + FROM stock_move + WHERE lot_id = :lot_id" + ); + $balanceStmt->execute([':lot_id' => $lotId]); + $balance = (float) $balanceStmt->fetchColumn(); + + if ($receiptQty > 0) { + insert_stock_move_in($pdo, $productId, $lotId, $receiptQty, $locations['storage'], $locations['storage'], 'Manueller Lagerzugang via Chargenformular'); + $balance += $receiptQty; + } + + if ($adjustmentQty !== 0.0) { + if ($adjustmentQty > 0) { + insert_stock_move_adjustment($pdo, $productId, $lotId, $adjustmentQty, null, $locations['storage'], 'Manuelle Bestandskorrektur via Chargenformular'); + } else { + $correctionQty = abs($adjustmentQty); + if ($correctionQty > $balance) { + throw new InvalidArgumentException('Bestandskorrektur wuerde den Chargensaldo negativ machen'); + } + insert_stock_move_adjustment($pdo, $productId, $lotId, $correctionQty, $locations['storage'], null, 'Manuelle Bestandskorrektur via Chargenformular'); + } + $balance += $adjustmentQty; + } + + if ($outQty > $balance) { + throw new InvalidArgumentException('Lagerabgang ist groesser als der aktuelle Chargenbestand'); + } + if ($outQty > 0) { + insert_stock_move_out($pdo, $productId, $lotId, $outQty, $locations['storage'], $locations['dispatch'], 'Manueller Lagerabgang via Chargenformular'); + } +} + function switch_current_lot(PDO $pdo, int $productId, int $oldCurrentLotId, int $storageLocationId): int { $closeStmt = $pdo->prepare( diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 93c950f..8f5bf8c 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -353,6 +353,11 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar . '
' . '' . '' + . 'Buchungen werden zusätzlich zur Rückverfolgbarkeit als Lagerbewegung gespeichert.
' + . '' + . '' + . '' . '