Add manufacturer lot traceability

This commit is contained in:
gitea_admin committed 2026-09-06 10:16:47 +02:00
1 parent 2aadce17bd
commit 19ce29c3d0
10 files changed
+418 -14

No files matched your search

+137 -4
View File
@@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/forecast.php';
require_once __DIR__ . '/../../system/audit.php';
function get_default_location_ids(PDO $pdo): array
{
@@ -76,8 +77,13 @@ function get_lager_product_overview(PDO $pdo): array
p.uom,
COALESCE(SUM(v.qty_net), 0) AS available_qty,
sl.lot_number,
sl.supplier_lot_number,
sl.id AS lot_id,
sl.manufacturer_party_id,
sl.manufacturer_lot_number,
mp.name AS manufacturer_name,
sl.mfg_date,
sl.expiry_date,
sl.purchase_date,
sl.sellout_date,
sl.sellout_forecast_status,
(
@@ -92,11 +98,13 @@ function get_lager_product_overview(PDO $pdo): array
LEFT JOIN stock_lot sl
ON sl.product_id = p.id
AND sl.status = 'current'
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 p.status = 'active'
GROUP BY p.id, p.name, p.sku, p.uom, sl.lot_number, sl.supplier_lot_number,
sl.expiry_date, sl.sellout_date, sl.sellout_forecast_status
GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id,
sl.manufacturer_lot_number, mp.name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
sl.sellout_date, sl.sellout_forecast_status
ORDER BY p.name ASC, p.id ASC"
);
@@ -107,9 +115,14 @@ function get_lager_product_overview(PDO $pdo): array
'sku' => (string) $row['sku'],
'uom' => (string) $row['uom'],
'available_qty' => (float) $row['available_qty'],
'lot_id' => $row['lot_id'] === null ? null : (int) $row['lot_id'],
'lot_number' => $row['lot_number'],
'supplier_lot_number' => $row['supplier_lot_number'],
'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'],
'sellout_date' => $row['sellout_date'],
'sellout_forecast_status' => (string) ($row['sellout_forecast_status'] ?? 'no_demand'),
'virtual_lot_number' => $row['virtual_lot_number'],
@@ -118,6 +131,126 @@ function get_lager_product_overview(PDO $pdo): array
);
}
function update_stock_lot_traceability(
PDO $pdo,
int $lotId,
?int $manufacturerPartyId,
?string $manufacturerLotNumber,
?string $mfgDate,
?string $expiryDate,
?string $purchaseDate,
string $changedBy
): array {
if ($lotId <= 0) {
throw new InvalidArgumentException('Chargen-ID fehlt');
}
$manufacturerLotNumber = $manufacturerLotNumber !== null ? trim($manufacturerLotNumber) : null;
if ($manufacturerLotNumber === '') {
$manufacturerLotNumber = null;
}
$dates = [
'mfg_date' => $mfgDate,
'expiry_date' => $expiryDate,
'purchase_date' => $purchaseDate,
];
foreach ($dates as $field => $value) {
if ($value !== null && $value !== '') {
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
$errors = DateTimeImmutable::getLastErrors();
if ($parsed === false || (is_array($errors) && (($errors['warning_count'] ?? 0) > 0 || ($errors['error_count'] ?? 0) > 0))) {
throw new InvalidArgumentException("Ungueltiges Datum fuer {$field}");
}
}
$dates[$field] = $value !== null && $value !== '' ? $value : null;
}
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$lotStmt = $pdo->prepare(
"SELECT id, product_id, lot_number, manufacturer_party_id, manufacturer_lot_number,
mfg_date, expiry_date, purchase_date, status
FROM stock_lot
WHERE id = :id
FOR UPDATE"
);
$lotStmt->execute([':id' => $lotId]);
$before = $lotStmt->fetch();
if (!is_array($before)) {
throw new RuntimeException('Charge nicht gefunden');
}
if ((string) $before['status'] === 'virtual_shortage') {
throw new InvalidArgumentException('Virtuelle Fehlbestandschargen koennen nicht gepflegt werden');
}
if ($manufacturerPartyId !== null) {
$manufacturerStmt = $pdo->prepare(
"SELECT p.id
FROM party p
INNER JOIN party_role pr ON pr.party_id = p.id AND pr.role_code = 'manufacturer'
WHERE p.id = :id AND p.status = 'active'
LIMIT 1"
);
$manufacturerStmt->execute([':id' => $manufacturerPartyId]);
if ($manufacturerStmt->fetchColumn() === false) {
throw new InvalidArgumentException('Hersteller ist nicht aktiv oder nicht als Hersteller gepflegt');
}
}
if ($manufacturerPartyId === null && $manufacturerLotNumber !== null) {
throw new InvalidArgumentException('Hersteller-Chargennummer benoetigt einen Hersteller');
}
if ($manufacturerPartyId !== null && $manufacturerLotNumber === null && (string) $before['status'] !== 'open') {
throw new InvalidArgumentException('Hersteller-Chargennummer fehlt');
}
$updateStmt = $pdo->prepare(
"UPDATE stock_lot
SET manufacturer_party_id = :manufacturer_party_id,
manufacturer_lot_number = :manufacturer_lot_number,
mfg_date = :mfg_date,
expiry_date = :expiry_date,
purchase_date = :purchase_date,
updated_at = NOW()
WHERE id = :id"
);
$updateStmt->execute([
':manufacturer_party_id' => $manufacturerPartyId,
':manufacturer_lot_number' => $manufacturerLotNumber,
':mfg_date' => $dates['mfg_date'],
':expiry_date' => $dates['expiry_date'],
':purchase_date' => $dates['purchase_date'],
':id' => $lotId,
]);
$after = $before;
$after['manufacturer_party_id'] = $manufacturerPartyId;
$after['manufacturer_lot_number'] = $manufacturerLotNumber;
$after['mfg_date'] = $dates['mfg_date'];
$after['expiry_date'] = $dates['expiry_date'];
$after['purchase_date'] = $dates['purchase_date'];
system_write_audit_log($pdo, 'stock_lot', (string) $lotId, 'traceability_updated', $changedBy, $before, $after, [
'module' => 'erp.lager',
]);
if ($startedTransaction) {
$pdo->commit();
}
return $after;
} 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(