feat(WEB): 절곡품 선생산→재고적재 Phase 1 - 생산입고 기반 구축

- StockTransaction: REASON_PRODUCTION_OUTPUT 상수 및 '생산입고' 라벨 추가
- StockLot: work_order_id FK 컬럼 마이그레이션 + 모델 fillable/casts/relation 추가
- StockService: increaseFromProduction() 메서드 구현 (increaseFromReceiving 기반)
- WorkOrderService: 완료 시 sales_order_id 유무에 따라 출하/재고입고 분기
  - stockInFromProduction(): 품목별 양품 재고 입고 처리
  - shouldStockIn(): items.options 기반 입고 대상 판단

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 15:32:24 +09:00
parent ba49313ffa
commit 8be54c3b8b
5 changed files with 259 additions and 23 deletions

View File

@@ -3,6 +3,8 @@
namespace App\Services;
use App\Models\Items\Item;
use App\Models\Production\WorkOrder;
use App\Models\Production\WorkOrderItem;
use App\Models\Tenants\Receiving;
use App\Models\Tenants\Stock;
use App\Models\Tenants\StockLot;
@@ -313,6 +315,99 @@ public function increaseFromReceiving(Receiving $receiving): StockLot
});
}
/**
* 생산 완료 시 완성품 재고 입고
*
* increaseFromReceiving()을 기반으로 구현.
* 선생산(수주 없는 작업지시) 완료 시 양품을 재고로 적재.
*
* @param WorkOrder $workOrder 선생산 작업지시
* @param WorkOrderItem $woItem 작업지시 품목
* @param float $goodQty 양품 수량
* @param string $lotNo LOT 번호
* @return StockLot 생성된 StockLot
*/
public function increaseFromProduction(
WorkOrder $workOrder,
WorkOrderItem $woItem,
float $goodQty,
string $lotNo
): StockLot {
if (! $woItem->item_id) {
throw new \Exception(__('error.stock.item_id_required'));
}
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($workOrder, $woItem, $goodQty, $lotNo, $tenantId, $userId) {
// 1. Stock 조회 또는 생성
$stock = $this->getOrCreateStock($woItem->item_id);
// 2. FIFO 순서 계산
$fifoOrder = $this->getNextFifoOrder($stock->id);
// 3. StockLot 생성
$stockLot = new StockLot;
$stockLot->tenant_id = $tenantId;
$stockLot->stock_id = $stock->id;
$stockLot->lot_no = $lotNo;
$stockLot->fifo_order = $fifoOrder;
$stockLot->receipt_date = now()->toDateString();
$stockLot->qty = $goodQty;
$stockLot->reserved_qty = 0;
$stockLot->available_qty = $goodQty;
$stockLot->unit = $woItem->unit ?? 'EA';
$stockLot->supplier = null;
$stockLot->supplier_lot = null;
$stockLot->po_number = null;
$stockLot->location = null;
$stockLot->status = 'available';
$stockLot->receiving_id = null;
$stockLot->work_order_id = $workOrder->id;
$stockLot->created_by = $userId;
$stockLot->updated_by = $userId;
$stockLot->save();
// 4. Stock 합계 갱신
$stock->refreshFromLots();
// 5. 거래 이력 기록
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_IN,
qty: $goodQty,
reason: StockTransaction::REASON_PRODUCTION_OUTPUT,
referenceType: 'work_order',
referenceId: $workOrder->id,
lotNo: $lotNo,
stockLotId: $stockLot->id
);
// 6. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'production_in',
reason: 'production_output',
referenceType: 'work_order',
referenceId: $workOrder->id,
qtyChange: $goodQty,
lotNo: $lotNo
);
Log::info('Stock increased from production', [
'work_order_id' => $workOrder->id,
'item_id' => $woItem->item_id,
'stock_id' => $stock->id,
'stock_lot_id' => $stockLot->id,
'qty' => $goodQty,
'lot_no' => $lotNo,
]);
return $stockLot;
});
}
/**
* 입고 수정 시 재고 조정 (차이만큼 증감)
*