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:
2026-03-13 22:45:43 +09:00
parent 54686cfc8a
commit d7c096b615
4 changed files with 84 additions and 6 deletions

View File

@@ -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);
}
/**
* 다음 순번 가져오기
*/

View File

@@ -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;
}
/**

View File

@@ -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,
]);
}

View File

@@ -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']);
});
}
};