feat: [shipment] MES 데이터 정합성 개선 — can_ship 검증, ShipmentItem FK, 재고차감 비활성화
- ShipmentService::updateStatus()에 can_ship 검증 추가 (ready/shipping/completed 전환 시) - shipment_items에 order_item_id, work_order_item_id 컬럼+인덱스 추가 (마이그레이션) - ShipmentItem 모델에 orderItem(), workOrderItem() 관계 추가 - createShipmentFromOrder()에서 order_item_id, work_order_item_id 자동 매핑 - decreaseStockForShipment() 호출 비활성화 (수주생산=재고 미경유, 선생산=자재 투입 시 차감)
This commit is contained in:
@@ -25,6 +25,8 @@ class ShipmentItem extends Model
|
||||
'unit',
|
||||
'lot_no',
|
||||
'stock_lot_id',
|
||||
'order_item_id',
|
||||
'work_order_item_id',
|
||||
'remarks',
|
||||
];
|
||||
|
||||
@@ -34,6 +36,8 @@ class ShipmentItem extends Model
|
||||
'quantity' => 'decimal:2',
|
||||
'shipment_id' => 'integer',
|
||||
'stock_lot_id' => 'integer',
|
||||
'order_item_id' => 'integer',
|
||||
'work_order_item_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -52,6 +56,22 @@ public function stockLot(): BelongsTo
|
||||
return $this->belongsTo(StockLot::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 품목 관계
|
||||
*/
|
||||
public function orderItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Orders\OrderItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업지시 품목 관계
|
||||
*/
|
||||
public function workOrderItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\WorkOrders\WorkOrderItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 순번 가져오기
|
||||
*/
|
||||
|
||||
@@ -309,6 +309,13 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n
|
||||
|
||||
$shipment = Shipment::where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
// 출하 가능 여부 검증 (scheduled → ready 이상 전환 시)
|
||||
if (in_array($status, ['ready', 'shipping', 'completed']) && ! $shipment->can_ship) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException(
|
||||
__('error.shipment.cannot_ship')
|
||||
);
|
||||
}
|
||||
|
||||
$updateData = [
|
||||
'status' => $status,
|
||||
'updated_by' => $userId,
|
||||
@@ -344,10 +351,8 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n
|
||||
$previousStatus = $shipment->status;
|
||||
$shipment->update($updateData);
|
||||
|
||||
// 🆕 출하완료 시 재고 차감 (FIFO)
|
||||
if ($status === 'completed' && $previousStatus !== 'completed') {
|
||||
$this->decreaseStockForShipment($shipment);
|
||||
}
|
||||
// 재고 차감 비활성화: 수주생산은 재고 미경유, 선생산 완성품은 자재 투입 시 차감됨
|
||||
// TODO: 선생산 로직 검증 후 재검토 (decreaseStockForShipment)
|
||||
|
||||
// 연결된 수주(Order) 상태 동기화
|
||||
$this->syncOrderStatus($shipment, $tenantId);
|
||||
@@ -357,10 +362,21 @@ public function updateStatus(int $id, string $status, ?array $additionalData = n
|
||||
|
||||
/**
|
||||
* 출하 완료 시 재고 차감
|
||||
*
|
||||
* 수주 연결 출하(order_id 있음)는 재고를 거치지 않으므로 차감 skip.
|
||||
* 재고 출고(order_id 없음)만 재고 차감 수행.
|
||||
*
|
||||
* @return array 실패 내역 (빈 배열이면 전체 성공)
|
||||
*/
|
||||
private function decreaseStockForShipment(Shipment $shipment): void
|
||||
private function decreaseStockForShipment(Shipment $shipment): array
|
||||
{
|
||||
// 수주 연결 출하는 재고 입고 없이 바로 출하하므로 차감하지 않음
|
||||
if ($shipment->order_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stockService = app(StockService::class);
|
||||
$failures = [];
|
||||
|
||||
// 출하 품목 조회
|
||||
$items = $shipment->items;
|
||||
@@ -389,15 +405,23 @@ private function decreaseStockForShipment(Shipment $shipment): void
|
||||
stockLotId: $item->stock_lot_id
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 재고 부족 등의 에러는 로그만 기록하고 계속 진행
|
||||
\Illuminate\Support\Facades\Log::warning('Failed to decrease stock for shipment item', [
|
||||
'shipment_id' => $shipment->id,
|
||||
'item_code' => $item->item_code,
|
||||
'quantity' => $item->quantity,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$failures[] = [
|
||||
'item_code' => $item->item_code,
|
||||
'item_name' => $item->item_name,
|
||||
'quantity' => $item->quantity,
|
||||
'reason' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $failures;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -764,6 +764,8 @@ private function createShipmentFromOrder(Order $order, $mainWorkOrders, int $ten
|
||||
'quantity' => $result['good_qty'] ?? $woItem->quantity,
|
||||
'unit' => $woItem->unit,
|
||||
'lot_no' => $lotNo,
|
||||
'order_item_id' => $woItem->source_order_item_id,
|
||||
'work_order_item_id' => $woItem->id,
|
||||
'remarks' => null,
|
||||
]);
|
||||
}
|
||||
@@ -784,6 +786,8 @@ private function createShipmentFromOrder(Order $order, $mainWorkOrders, int $ten
|
||||
'quantity' => $orderItem->quantity,
|
||||
'unit' => $orderItem->unit,
|
||||
'lot_no' => null,
|
||||
'order_item_id' => $orderItem->id,
|
||||
'work_order_item_id' => null,
|
||||
'remarks' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shipment_items', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('order_item_id')->nullable()->after('stock_lot_id')
|
||||
->comment('수주품목 ID (출처 추적용)');
|
||||
$table->unsignedBigInteger('work_order_item_id')->nullable()->after('order_item_id')
|
||||
->comment('작업지시품목 ID (출처 추적용)');
|
||||
|
||||
$table->index('order_item_id');
|
||||
$table->index('work_order_item_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shipment_items', function (Blueprint $table) {
|
||||
$table->dropIndex(['work_order_item_id']);
|
||||
$table->dropIndex(['order_item_id']);
|
||||
$table->dropColumn(['work_order_item_id', 'order_item_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user