feat(재고): stock_transactions 입출고 거래 이력 테이블 추가

- stock_transactions 마이그레이션 생성 (type, qty, balance_qty, reference)
- StockTransaction 모델 (IN/OUT/RESERVE/RELEASE 타입, 사유 상수)
- StockService 5개 메서드에 거래 이력 기록 추가
  - increaseFromReceiving → IN
  - decreaseFIFO → OUT (LOT별)
  - reserve → RESERVE (LOT별)
  - releaseReservation → RELEASE (LOT별)
  - decreaseForShipment → OUT (LOT별)
- Stock 모델에 transactions() 관계 추가
- 기존 audit_logs 기록은 유지 (감사 로그와 거래 이력 목적 분리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 15:05:03 +09:00
parent 847717e631
commit f7ad9ae36e
4 changed files with 317 additions and 5 deletions

View File

@@ -6,6 +6,7 @@
use App\Models\Tenants\Receiving;
use App\Models\Tenants\Stock;
use App\Models\Tenants\StockLot;
use App\Models\Tenants\StockTransaction;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -277,7 +278,19 @@ public function increaseFromReceiving(Receiving $receiving): StockLot
// 4. Stock 정보 갱신 (LOT 기반)
$stock->refreshFromLots();
// 5. 감사 로그 기록
// 5. 거래 이력 기록
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_IN,
qty: $receiving->receiving_qty,
reason: StockTransaction::REASON_RECEIVING,
referenceType: 'receiving',
referenceId: $receiving->id,
lotNo: $receiving->lot_no,
stockLotId: $stockLot->id
);
// 6. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'stock_increase',
@@ -448,7 +461,21 @@ public function decreaseFIFO(int $itemId, float $qty, string $reason, int $refer
$stock->last_issue_date = now();
$stock->save();
// 6. 감사 로그 기록
// 6. 거래 이력 기록 (LOT별)
foreach ($deductedLots as $dl) {
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_OUT,
qty: -$dl['deducted_qty'],
reason: $reason,
referenceType: $reason,
referenceId: $referenceId,
lotNo: $dl['lot_no'],
stockLotId: $dl['lot_id']
);
}
// 7. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'stock_decrease',
@@ -591,7 +618,21 @@ public function reserve(int $itemId, float $qty, int $orderId): void
// 4. Stock 정보 갱신
$stock->refreshFromLots();
// 5. 감사 로그 기록
// 5. 거래 이력 기록 (LOT별)
foreach ($reservedLots as $rl) {
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_RESERVE,
qty: $rl['reserved_qty'],
reason: StockTransaction::REASON_ORDER_CONFIRM,
referenceType: 'order',
referenceId: $orderId,
lotNo: $rl['lot_no'],
stockLotId: $rl['lot_id']
);
}
// 6. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'stock_reserve',
@@ -680,7 +721,21 @@ public function releaseReservation(int $itemId, float $qty, int $orderId): void
// 3. Stock 정보 갱신
$stock->refreshFromLots();
// 4. 감사 로그 기록
// 4. 거래 이력 기록 (LOT별)
foreach ($releasedLots as $rl) {
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_RELEASE,
qty: -$rl['released_qty'],
reason: StockTransaction::REASON_ORDER_CANCEL,
referenceType: 'order',
referenceId: $orderId,
lotNo: $rl['lot_no'],
stockLotId: $rl['lot_id']
);
}
// 5. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'stock_release',
@@ -839,7 +894,21 @@ public function decreaseForShipment(int $itemId, float $qty, int $shipmentId, ?i
$stock->last_issue_date = now();
$stock->save();
// 5. 감사 로그 기록
// 5. 거래 이력 기록 (LOT별)
foreach ($deductedLots as $dl) {
$this->recordTransaction(
stock: $stock,
type: StockTransaction::TYPE_OUT,
qty: -$dl['deducted_qty'],
reason: StockTransaction::REASON_SHIPMENT,
referenceType: 'shipment',
referenceId: $shipmentId,
lotNo: $dl['lot_no'],
stockLotId: $dl['lot_id']
);
}
// 6. 감사 로그 기록
$this->logStockChange(
stock: $stock,
action: 'stock_decrease',
@@ -861,6 +930,58 @@ public function decreaseForShipment(int $itemId, float $qty, int $shipmentId, ?i
});
}
/**
* 재고 거래 이력 기록
*
* @param Stock $stock 재고
* @param string $type 거래유형 (IN, OUT, RESERVE, RELEASE)
* @param float $qty 변동 수량 (입고: 양수, 출고: 음수)
* @param string $reason 사유
* @param string $referenceType 참조 유형
* @param int $referenceId 참조 ID
* @param string|null $lotNo LOT번호
* @param int|null $stockLotId StockLot ID
* @param string|null $remark 비고
*/
private function recordTransaction(
Stock $stock,
string $type,
float $qty,
string $reason,
string $referenceType,
int $referenceId,
?string $lotNo = null,
?int $stockLotId = null,
?string $remark = null
): void {
try {
StockTransaction::create([
'tenant_id' => $stock->tenant_id,
'stock_id' => $stock->id,
'stock_lot_id' => $stockLotId,
'type' => $type,
'qty' => $qty,
'balance_qty' => (float) $stock->stock_qty,
'reference_type' => $referenceType,
'reference_id' => $referenceId,
'lot_no' => $lotNo,
'reason' => $reason,
'remark' => $remark,
'item_code' => $stock->item_code,
'item_name' => $stock->item_name,
'created_by' => $this->apiUserId(),
]);
} catch (\Exception $e) {
// 거래 이력 기록 실패는 비즈니스 로직에 영향을 주지 않음
Log::warning('Failed to record stock transaction', [
'stock_id' => $stock->id,
'type' => $type,
'qty' => $qty,
'error' => $e->getMessage(),
]);
}
}
/**
* 재고 변경 감사 로그 기록
*/