Erfasse Shopify-Bestellungen trotz Fehlbestand
This commit is contained in:
1 parent
ba23c58dea
commit
ed8927b37d
7 files changed
+266
-31
No files matched your search
@@ -197,8 +197,6 @@ function project_shopify_order(PDO $pdo, string $orderGid, callable $orderLoader
|
||||
if ($resolvedLines === []) {
|
||||
throw new RuntimeException('Shopify-Bestellung enthaelt keine Positionen');
|
||||
}
|
||||
shopify_projection_stock_check($pdo, $resolvedLines);
|
||||
|
||||
if ($dryRun) {
|
||||
$customer = is_array($order['customer'] ?? null) ? $order['customer'] : [];
|
||||
shopify_projection_customer($pdo, $customer, true);
|
||||
@@ -208,6 +206,7 @@ function project_shopify_order(PDO $pdo, string $orderGid, callable $orderLoader
|
||||
'sales_order_id' => null,
|
||||
'line_count' => count($resolvedLines),
|
||||
'allocation_count' => 0,
|
||||
'virtual_allocation_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -256,6 +255,7 @@ function project_shopify_order(PDO $pdo, string $orderGid, callable $orderLoader
|
||||
$locations = get_default_location_ids($pdo);
|
||||
$lineNo = 0;
|
||||
$allocationCount = 0;
|
||||
$virtualAllocationCount = 0;
|
||||
foreach ($resolvedLines as $resolvedLine) {
|
||||
$lineNo++;
|
||||
$line = $resolvedLine['line'];
|
||||
@@ -289,6 +289,7 @@ function project_shopify_order(PDO $pdo, string $orderGid, callable $orderLoader
|
||||
$lineId = (int) $lineStmt->fetchColumn();
|
||||
$allocation = allocate_line_inventory($pdo, $orderId, $lineId, $lineNo, $qty, (int) $resolvedLine['sellable_item_id'], $locations, 'shopify-order');
|
||||
$allocationCount += (int) ($allocation['allocationCount'] ?? 0);
|
||||
$virtualAllocationCount += (int) ($allocation['virtualAllocationCount'] ?? 0);
|
||||
}
|
||||
|
||||
shopify_projection_address($pdo, $partyId, (array) ($order['billingAddress'] ?? []), 'billing', false);
|
||||
@@ -307,5 +308,6 @@ function project_shopify_order(PDO $pdo, string $orderGid, callable $orderLoader
|
||||
'sales_order_id' => $orderId,
|
||||
'line_count' => count($resolvedLines),
|
||||
'allocation_count' => $allocationCount,
|
||||
'virtual_allocation_count' => $virtualAllocationCount,
|
||||
];
|
||||
}
|
||||
+216
-25
@@ -289,6 +289,196 @@ function has_available_stock_for_product(PDO $pdo, int $productId, float $epsilo
|
||||
return $qty > $epsilon;
|
||||
}
|
||||
|
||||
function get_or_create_virtual_shortage_lot(PDO $pdo, int $productId): int
|
||||
{
|
||||
$lotNumber = "VIRT-{$productId}-UNBEKANNT";
|
||||
$select = $pdo->prepare(
|
||||
"SELECT id
|
||||
FROM stock_lot
|
||||
WHERE product_id = :product_id
|
||||
AND status = 'virtual_shortage'
|
||||
AND lot_number = :lot_number
|
||||
FOR UPDATE"
|
||||
);
|
||||
$select->execute([
|
||||
':product_id' => $productId,
|
||||
':lot_number' => $lotNumber,
|
||||
]);
|
||||
$lotId = $select->fetchColumn();
|
||||
if ($lotId !== false) {
|
||||
return (int) $lotId;
|
||||
}
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
"INSERT INTO stock_lot (product_id, lot_number, status, created_at, updated_at)
|
||||
VALUES (:product_id, :lot_number, 'virtual_shortage', NOW(), NOW())
|
||||
RETURNING id"
|
||||
);
|
||||
$insert->execute([
|
||||
':product_id' => $productId,
|
||||
':lot_number' => $lotNumber,
|
||||
]);
|
||||
$newLotId = $insert->fetchColumn();
|
||||
if ($newLotId === false) {
|
||||
throw new RuntimeException("Virtuelle Fehlbestandscharge fuer Produkt {$productId} konnte nicht angelegt werden");
|
||||
}
|
||||
|
||||
return (int) $newLotId;
|
||||
}
|
||||
|
||||
function replace_virtual_lot_allocation(
|
||||
PDO $pdo,
|
||||
int $allocationId,
|
||||
int $realLotId,
|
||||
float $qty,
|
||||
string $changedBy,
|
||||
string $reason
|
||||
): int {
|
||||
if ($qty <= 0 || trim($changedBy) === '' || trim($reason) === '') {
|
||||
throw new InvalidArgumentException('Menge, Bediener und Grund sind erforderlich');
|
||||
}
|
||||
|
||||
$startedTransaction = !$pdo->inTransaction();
|
||||
if ($startedTransaction) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
$allocationStmt = $pdo->prepare(
|
||||
"SELECT a.id, a.sales_order_line_id, a.product_id, a.lot_id, a.qty, a.allocation_status,
|
||||
sol.sales_order_id, sol.line_no,
|
||||
source_lot.status AS source_lot_status,
|
||||
target_lot.product_id AS target_product_id,
|
||||
target_lot.status AS target_lot_status
|
||||
FROM sales_order_line_lot_allocation a
|
||||
INNER JOIN sales_order_line sol ON sol.id = a.sales_order_line_id
|
||||
INNER JOIN stock_lot source_lot ON source_lot.id = a.lot_id
|
||||
INNER JOIN stock_lot target_lot ON target_lot.id = :target_lot_id
|
||||
WHERE a.id = :allocation_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$allocationStmt->execute([
|
||||
':allocation_id' => $allocationId,
|
||||
':target_lot_id' => $realLotId,
|
||||
]);
|
||||
$allocation = $allocationStmt->fetch();
|
||||
if (!is_array($allocation)) {
|
||||
throw new RuntimeException("Chargenzuordnung {$allocationId} fehlt");
|
||||
}
|
||||
|
||||
if (($allocation['allocation_status'] ?? null) !== 'reserved' || $allocation['source_lot_status'] !== 'virtual_shortage') {
|
||||
throw new RuntimeException('Nur reservierte virtuelle Chargenzuordnungen koennen ersetzt werden');
|
||||
}
|
||||
if ((int) $allocation['product_id'] !== (int) $allocation['target_product_id']) {
|
||||
throw new RuntimeException('Echte Charge gehoert zu einem anderen Produkt');
|
||||
}
|
||||
if ($allocation['target_lot_status'] !== 'current') {
|
||||
throw new RuntimeException('Echte Zielcharge muss aktuell sein');
|
||||
}
|
||||
|
||||
$sourceQty = (float) $allocation['qty'];
|
||||
if ($qty > $sourceQty + 0.0000001) {
|
||||
throw new RuntimeException('Ersetzungsmenge uebersteigt virtuelle Zuordnung');
|
||||
}
|
||||
|
||||
$balanceStmt = $pdo->prepare(
|
||||
"SELECT COALESCE(v.qty_net, 0)
|
||||
FROM v_stock_lot_balance v
|
||||
WHERE v.stock_lot_id = :lot_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$balanceStmt->execute([':lot_id' => $realLotId]);
|
||||
$available = (float) ($balanceStmt->fetchColumn() ?: 0);
|
||||
if ($available + 0.0000001 < $qty) {
|
||||
throw new RuntimeException('Nicht genuegend Bestand in der echten Zielcharge');
|
||||
}
|
||||
|
||||
$locations = get_default_location_ids($pdo);
|
||||
$stockMoveId = insert_stock_move_out(
|
||||
$pdo,
|
||||
(int) $allocation['product_id'],
|
||||
$realLotId,
|
||||
$qty,
|
||||
(int) $locations['storage'],
|
||||
(int) $locations['dispatch'],
|
||||
"lot-replacement:order={$allocation['sales_order_id']}:line={$allocation['line_no']}:product={$allocation['product_id']}"
|
||||
);
|
||||
|
||||
$releaseStmt = $pdo->prepare(
|
||||
"UPDATE sales_order_line_lot_allocation
|
||||
SET allocation_status = 'released', released_at = NOW(), updated_at = NOW()
|
||||
WHERE id = :id"
|
||||
);
|
||||
$releaseStmt->execute([':id' => $allocationId]);
|
||||
|
||||
$remainingQty = $sourceQty - $qty;
|
||||
if ($remainingQty > 0.0000001) {
|
||||
$residualStmt = $pdo->prepare(
|
||||
"INSERT INTO sales_order_line_lot_allocation (
|
||||
sales_order_line_id, product_id, lot_id, qty, allocation_status, created_at, updated_at
|
||||
) VALUES (
|
||||
:sales_order_line_id, :product_id, :lot_id, :qty, 'reserved', NOW(), NOW()
|
||||
)"
|
||||
);
|
||||
$residualStmt->execute([
|
||||
':sales_order_line_id' => (int) $allocation['sales_order_line_id'],
|
||||
':product_id' => (int) $allocation['product_id'],
|
||||
':lot_id' => (int) $allocation['lot_id'],
|
||||
':qty' => $remainingQty,
|
||||
]);
|
||||
}
|
||||
|
||||
$newAllocationStmt = $pdo->prepare(
|
||||
"INSERT INTO sales_order_line_lot_allocation (
|
||||
sales_order_line_id, product_id, lot_id, qty, allocation_status, stock_move_id, created_at, updated_at
|
||||
) VALUES (
|
||||
:sales_order_line_id, :product_id, :lot_id, :qty, 'allocated', :stock_move_id, NOW(), NOW()
|
||||
) RETURNING id"
|
||||
);
|
||||
$newAllocationStmt->execute([
|
||||
':sales_order_line_id' => (int) $allocation['sales_order_line_id'],
|
||||
':product_id' => (int) $allocation['product_id'],
|
||||
':lot_id' => $realLotId,
|
||||
':qty' => $qty,
|
||||
':stock_move_id' => $stockMoveId,
|
||||
]);
|
||||
$newAllocationId = $newAllocationStmt->fetchColumn();
|
||||
if ($newAllocationId === false) {
|
||||
throw new RuntimeException('Neue reale Chargenzuordnung konnte nicht angelegt werden');
|
||||
}
|
||||
|
||||
$replacementStmt = $pdo->prepare(
|
||||
"INSERT INTO stock_lot_allocation_replacement (
|
||||
from_allocation_id, to_allocation_id, from_lot_id, to_lot_id,
|
||||
product_id, qty, changed_by, reason, created_at
|
||||
) VALUES (
|
||||
:from_allocation_id, :to_allocation_id, :from_lot_id, :to_lot_id,
|
||||
:product_id, :qty, :changed_by, :reason, NOW()
|
||||
)"
|
||||
);
|
||||
$replacementStmt->execute([
|
||||
':from_allocation_id' => $allocationId,
|
||||
':to_allocation_id' => (int) $newAllocationId,
|
||||
':from_lot_id' => (int) $allocation['lot_id'],
|
||||
':to_lot_id' => $realLotId,
|
||||
':product_id' => (int) $allocation['product_id'],
|
||||
':qty' => $qty,
|
||||
':changed_by' => trim($changedBy),
|
||||
':reason' => trim($reason),
|
||||
]);
|
||||
|
||||
if ($startedTransaction) {
|
||||
$pdo->commit();
|
||||
}
|
||||
return (int) $newAllocationId;
|
||||
} catch (Throwable $exception) {
|
||||
if ($startedTransaction && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
function allocate_components_for_line(
|
||||
PDO $pdo,
|
||||
int $orderId,
|
||||
@@ -304,14 +494,7 @@ function allocate_components_for_line(
|
||||
}
|
||||
|
||||
$allocationCount = 0;
|
||||
$allocationInsert = $pdo->prepare(
|
||||
"INSERT INTO sales_order_line_lot_allocation (
|
||||
sales_order_line_id, product_id, lot_id, qty, allocation_status, stock_move_id, created_at, updated_at
|
||||
) VALUES (
|
||||
:sales_order_line_id, :product_id, :lot_id, :qty, 'allocated', :stock_move_id, NOW(), NOW()
|
||||
)"
|
||||
);
|
||||
|
||||
$virtualAllocationCount = 0;
|
||||
foreach ($components as $component) {
|
||||
$productId = (int) $component['product_id'];
|
||||
$remaining = $lineQty * (float) $component['qty_per_item'];
|
||||
@@ -328,25 +511,31 @@ function allocate_components_for_line(
|
||||
$available = $current['qty_net'];
|
||||
|
||||
if ($available <= 0.0000001) {
|
||||
$lotId = switch_current_lot($pdo, $productId, $lotId, (int) $locations['storage']);
|
||||
$current = get_current_lot_balance_for_update($pdo, $productId);
|
||||
$available = $current['qty_net'];
|
||||
$lotId = $current['lot_id'];
|
||||
$take = $remaining;
|
||||
$lotId = get_or_create_virtual_shortage_lot($pdo, $productId);
|
||||
$stockMoveId = null;
|
||||
$allocationStatus = 'reserved';
|
||||
$virtualAllocationCount++;
|
||||
} else {
|
||||
$take = min($remaining, $available);
|
||||
$stockMoveId = insert_stock_move_out(
|
||||
$pdo,
|
||||
$productId,
|
||||
$lotId,
|
||||
$take,
|
||||
(int) $locations['storage'],
|
||||
(int) $locations['dispatch'],
|
||||
"{$movementSource}:order={$orderId}:line={$lineNo}:product={$productId}"
|
||||
);
|
||||
$allocationStatus = 'allocated';
|
||||
}
|
||||
|
||||
if ($available <= 0.0000001) {
|
||||
throw new RuntimeException("Kein verfuegbarer Bestand fuer Produkt {$productId}");
|
||||
}
|
||||
|
||||
$take = min($remaining, $available);
|
||||
$stockMoveId = insert_stock_move_out(
|
||||
$pdo,
|
||||
$productId,
|
||||
$lotId,
|
||||
$take,
|
||||
(int) $locations['storage'],
|
||||
(int) $locations['dispatch'],
|
||||
"{$movementSource}:order={$orderId}:line={$lineNo}:product={$productId}"
|
||||
$allocationInsert = $pdo->prepare(
|
||||
"INSERT INTO sales_order_line_lot_allocation (
|
||||
sales_order_line_id, product_id, lot_id, qty, allocation_status, stock_move_id, created_at, updated_at
|
||||
) VALUES (
|
||||
:sales_order_line_id, :product_id, :lot_id, :qty, :allocation_status, :stock_move_id, NOW(), NOW()
|
||||
)"
|
||||
);
|
||||
|
||||
$allocationInsert->execute([
|
||||
@@ -354,6 +543,7 @@ function allocate_components_for_line(
|
||||
':product_id' => $productId,
|
||||
':lot_id' => $lotId,
|
||||
':qty' => $take,
|
||||
':allocation_status' => $allocationStatus,
|
||||
':stock_move_id' => $stockMoveId,
|
||||
]);
|
||||
$allocationCount++;
|
||||
@@ -365,6 +555,7 @@ function allocate_components_for_line(
|
||||
return [
|
||||
'allocated' => true,
|
||||
'allocationCount' => $allocationCount,
|
||||
'virtualAllocationCount' => $virtualAllocationCount,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user