feat: [order] 재고생산관리(STOCK) 타입 추가

- Order 모델에 TYPE_STOCK = 'STOCK' 상수 추가
- StoreOrderRequest/UpdateOrderRequest에 STOCK 타입 validation 추가
- options에 production_reason, target_stock_qty 필드 추가
- 재고생산 채번: STK{YYYYMMDD}{NNNN} 형식
- stats()에 order_type 필터 파라미터 추가
- STOCK 타입 확정 시 매출 자동 생성 스킵
This commit is contained in:
김보곤
2026-03-16 21:27:13 +09:00
parent a7f98ccdf5
commit 407afe38e4
5 changed files with 49 additions and 12 deletions

View File

@@ -30,10 +30,10 @@ public function index(Request $request)
/**
* 통계 조회
*/
public function stats()
public function stats(Request $request)
{
return ApiResponse::handle(function () {
return $this->service->stats();
return ApiResponse::handle(function () use ($request) {
return $this->service->stats($request->input('order_type'));
}, __('message.order.fetched'));
}

View File

@@ -18,7 +18,7 @@ public function rules(): array
return [
// 기본 정보
'quote_id' => 'nullable|integer|exists:quotes,id',
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])],
'status_code' => ['nullable', Rule::in([
Order::STATUS_DRAFT,
Order::STATUS_CONFIRMED,
@@ -55,6 +55,8 @@ public function rules(): array
'options.shipping_address' => 'nullable|string|max:500',
'options.shipping_address_detail' => 'nullable|string|max:500',
'options.manager_name' => 'nullable|string|max:100',
'options.production_reason' => 'nullable|string|max:500',
'options.target_stock_qty' => 'nullable|numeric|min:0',
// 품목 배열
'items' => 'nullable|array',

View File

@@ -17,7 +17,7 @@ public function rules(): array
{
return [
// 기본 정보 (order_no는 수정 불가)
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])],
'category_code' => 'nullable|string|max:50',
// 거래처 정보
@@ -49,6 +49,8 @@ public function rules(): array
'options.shipping_address' => 'nullable|string|max:500',
'options.shipping_address_detail' => 'nullable|string|max:500',
'options.manager_name' => 'nullable|string|max:100',
'options.production_reason' => 'nullable|string|max:500',
'options.target_stock_qty' => 'nullable|numeric|min:0',
// 품목 배열 (전체 교체)
'items' => 'nullable|array',

View File

@@ -78,6 +78,8 @@ class Order extends Model
public const TYPE_PURCHASE = 'PURCHASE'; // 발주
public const TYPE_STOCK = 'STOCK'; // 재고생산
// 매출 인식 시점
public const SALES_ON_ORDER_CONFIRM = 'on_order_confirm'; // 수주확정 시

View File

@@ -109,17 +109,22 @@ public function index(array $params)
/**
* 통계 조회
*/
public function stats(): array
public function stats(?string $orderType = null): array
{
$tenantId = $this->tenantId();
$counts = Order::where('tenant_id', $tenantId)
$baseQuery = Order::where('tenant_id', $tenantId);
if ($orderType !== null) {
$baseQuery->where('order_type_code', $orderType);
}
$counts = (clone $baseQuery)
->select('status_code', DB::raw('count(*) as count'))
->groupBy('status_code')
->pluck('count', 'status_code')
->toArray();
$amounts = Order::where('tenant_id', $tenantId)
$amounts = (clone $baseQuery)
->select('status_code', DB::raw('sum(total_amount) as total'))
->groupBy('status_code')
->pluck('total', 'status_code')
@@ -162,10 +167,13 @@ public function store(array $data)
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
// 수주번호 자동 생성
// 수주번호 자동 생성 (재고생산은 STK 접두사)
$pairCode = $data['pair_code'] ?? null;
unset($data['pair_code']);
$data['order_no'] = $this->generateOrderNo($tenantId, $pairCode);
$isStock = ($data['order_type_code'] ?? null) === Order::TYPE_STOCK;
$data['order_no'] = $isStock
? $this->generateStockOrderNo($tenantId)
: $this->generateOrderNo($tenantId, $pairCode);
$data['tenant_id'] = $tenantId;
$data['created_by'] = $userId;
$data['updated_by'] = $userId;
@@ -629,8 +637,8 @@ public function updateStatus(int $id, string $status)
$createdSale = null;
$previousStatus = $order->status_code;
// 수주확정 시 매출 자동 생성 (sales_recognition = on_order_confirm인 경우)
if ($status === Order::STATUS_CONFIRMED && $order->shouldCreateSaleOnConfirm()) {
// 수주확정 시 매출 자동 생성 (재고생산은 매출 생성 불필요)
if ($status === Order::STATUS_CONFIRMED && $order->order_type_code !== Order::TYPE_STOCK && $order->shouldCreateSaleOnConfirm()) {
$createdSale = $this->createSaleFromOrder($order, $userId);
$order->sale_id = $createdSale->id;
}
@@ -776,6 +784,29 @@ private function generateOrderNoLegacy(int $tenantId): string
return sprintf('%s%s%04d', $prefix, $date, $seq);
}
/**
* 재고생산 번호 생성 (STK{YYYYMMDD}{NNNN})
*/
private function generateStockOrderNo(int $tenantId): string
{
$prefix = 'STK';
$date = now()->format('Ymd');
$lastNo = Order::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('order_no', 'like', "{$prefix}{$date}%")
->orderByDesc('order_no')
->value('order_no');
if ($lastNo) {
$seq = (int) substr($lastNo, -4) + 1;
} else {
$seq = 1;
}
return sprintf('%s%s%04d', $prefix, $date, $seq);
}
/**
* 견적에서 수주 생성
*/