feat: 견적확정 밸리데이션, 작업지시 통계 공정별 카운트, 입고/재고 개선

- 견적확정 시 업체명/현장명/담당자/연락처 필수 검증 추가 (QuoteService)
- 작업지시 stats API에 by_process 공정별 카운트 반환 추가
- 작업지시 목록/상세 쿼리에 수주 개소(rootNodes) 연관 로딩
- 작업지시 품목에 sourceOrderItem.node 관계 추가
- 입고관리 완료건 수정 허용 및 재고 차이 조정
- work_order_step_progress 테이블 마이그레이션
- receivings 테이블 options 컬럼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 03:27:07 +09:00
parent 6b3e5c3e87
commit 487e651845
22 changed files with 1422 additions and 72 deletions

View File

@@ -313,6 +313,107 @@ public function increaseFromReceiving(Receiving $receiving): StockLot
});
}
/**
* 입고 수정 시 재고 조정 (차이만큼 증감)
*
* - completed→completed 수량변경: 차이만큼 조정 (50→60 = +10)
* - completed→대기: 전량 차감 (newQty = 0)
*
* @param Receiving $receiving 입고 레코드
* @param float $newQty 새 수량 (상태가 completed가 아니면 0)
*/
public function adjustFromReceiving(Receiving $receiving, float $newQty): void
{
if (! $receiving->item_id) {
return;
}
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
DB::transaction(function () use ($receiving, $newQty, $tenantId, $userId) {
// 1. 해당 입고로 생성된 StockLot 조회
$stockLot = StockLot::where('tenant_id', $tenantId)
->where('receiving_id', $receiving->id)
->first();
if (! $stockLot) {
Log::warning('StockLot not found for receiving adjustment', [
'receiving_id' => $receiving->id,
]);
return;
}
$stock = Stock::where('id', $stockLot->stock_id)
->lockForUpdate()
->first();
if (! $stock) {
return;
}
$oldQty = (float) $stockLot->qty;
$diff = $newQty - $oldQty;
// 차이가 없으면 스킵
if (abs($diff) < 0.001) {
return;
}
// 2. StockLot 수량 조정
$stockLot->qty = $newQty;
$stockLot->available_qty = max(0, $newQty - $stockLot->reserved_qty);
$stockLot->updated_by = $userId;
if ($newQty <= 0) {
$stockLot->qty = 0;
$stockLot->available_qty = 0;
$stockLot->reserved_qty = 0;
$stockLot->status = 'used';
} else {
$stockLot->status = 'available';
}
$stockLot->save();
// 3. Stock 정보 갱신
$stock->refreshFromLots();
// 4. 거래 이력 기록
$this->recordTransaction(
stock: $stock,
type: $diff > 0 ? StockTransaction::TYPE_IN : StockTransaction::TYPE_OUT,
qty: $diff,
reason: StockTransaction::REASON_RECEIVING,
referenceType: 'receiving',
referenceId: $receiving->id,
lotNo: $receiving->lot_no,
stockLotId: $stockLot->id
);
// 5. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: $diff > 0 ? 'stock_increase' : 'stock_decrease',
reason: 'receiving_adjustment',
referenceType: 'receiving',
referenceId: $receiving->id,
qtyChange: $diff,
lotNo: $receiving->lot_no
);
Log::info('Stock adjusted from receiving modification', [
'receiving_id' => $receiving->id,
'item_id' => $receiving->item_id,
'stock_id' => $stock->id,
'old_qty' => $oldQty,
'new_qty' => $newQty,
'diff' => $diff,
]);
});
}
/**
* Stock 조회 또는 생성
*